| name | obsidian-rate-limits |
| description | Handle Obsidian file system operations and throttling patterns.
Use when processing many files, handling bulk operations,
or preventing performance issues from excessive operations.
Trigger with phrases like "obsidian rate limit", "obsidian bulk operations",
"obsidian file throttling", "obsidian performance limits".
|
| allowed-tools | Read, Write, Edit |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Obsidian Rate Limits
Overview
Manage file system operations and implement throttling to prevent performance issues in Obsidian plugins.
Prerequisites
- Understanding of async JavaScript
- Familiarity with Obsidian vault operations
- Knowledge of file system performance considerations
Key Concepts
Obsidian Operation Limits
| Operation | Recommended Limit | Risk if Exceeded |
|---|
| File reads | 100/second | UI freeze |
| File writes | 10/second | Data corruption risk |
| Metadata cache reads | 1000/second | Memory pressure |
| DOM updates | 60/second | Visual lag |
| Event emissions | 100/second | Event queue backup |
Instructions
Step 1: Implement Async Queue
export class AsyncQueue {
private queue: Array<() => Promise<void>> = [];
private processing = false;
private concurrency: number;
private activeCount = 0;
constructor(concurrency: number = 1) {
this.concurrency = concurrency;
}
async add<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
}
});
this.process();
});
}
private async process(): Promise<> {
(. >= .) ;
task = ..();
(!task) ;
.++;
{
();
} {
.--;
.();
}
}
(): {
..;
}
(): {
.;
}
}
Step 2: Implement Rate Limiter
export class RateLimiter {
private timestamps: number[] = [];
private limit: number;
private window: number;
constructor(limit: number, windowMs: number) {
this.limit = limit;
this.window = windowMs;
}
async acquire(): Promise<void> {
const now = Date.now();
this.timestamps = this.timestamps.filter(t => now - t < this.window);
if (this.timestamps.length >= this.limit) {
const oldestTimestamp = this.timestamps[0];
waitTime = . - (now - oldestTimestamp);
.(waitTime);
.();
}
..(now);
}
(: ): <> {
( (resolve, ms));
}
}
writeLimiter = (, );
() {
writeLimiter.();
...(file, content);
}
Step 3: Batch Processing Pattern
export interface BatchOptions {
batchSize: number;
delayBetweenBatches: number;
onProgress?: (processed: number, total: number) => void;
}
export async function processBatches<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
options: BatchOptions
): Promise<R[]> {
const results: R[] = [];
const total = items.length;
for (let i = 0; i < items.length; i += options.batchSize) {
const batch = items.slice(i, i + options.batchSize);
const batchResults = await Promise.all(
batch.map(item => processor(item))
);
results.push(...batchResults);
options.?.(.(i + options., total), total);
(i + options. < items.) {
(options.);
}
}
results;
}
(): <> {
( (resolve, ms));
}
files = ...();
results = (
files,
(file) => {
content = ...(file);
{ : file., : content. };
},
{
: ,
: ,
: {
.();
},
}
);
Step 4: Debounced Operations
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
options: { leading?: boolean; trailing?: boolean; maxWait?: number } = {}
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
let lastCallTime: number | null = null;
let lastInvokeTime = 0;
let result: ReturnType<T>;
const { leading = false, trailing = true, maxWait } = options;
function invokeFunc(time: number, args: Parameters<T>) {
lastInvokeTime = time;
result = func(...args);
return result;
}
return function (...args: <T>) {
time = .();
isInvoking = (time);
lastCallTime = time;
(isInvoking) {
(!timeout && leading) {
(time, args);
}
}
(!timeout) {
timeout = ( {
timeout = ;
(trailing && lastCallTime) {
(.(), args);
}
}, wait);
}
(maxWait !== ) {
timeSinceLastInvoke = time - lastInvokeTime;
(timeSinceLastInvoke >= maxWait) {
(time, args);
}
}
};
(): {
timeSinceLastCall = lastCallTime ? time - lastCallTime : ;
!lastCallTime || timeSinceLastCall >= wait;
}
}
debouncedSearch = (
(: ) => {
results = (query);
(results);
},
,
{ : , : }
);
inputEl.(, {
(e..);
});
Step 5: Throttled Event Handling
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle = false;
let lastArgs: Parameters<T> | null = null;
return function (...args: Parameters<T>) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
if (lastArgs) {
func(...lastArgs);
lastArgs = null;
}
}, limit);
} else {
lastArgs = args;
}
};
}
const throttledOnScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.(, throttledOnScroll);
Output
- Async queue for sequential operations
- Rate limiter for controlled throughput
- Batch processor for bulk operations
- Debounce for user input
- Throttle for frequent events
Error Handling
| Issue | Cause | Solution |
|---|
| UI freezes | Too many sync operations | Use async with batching |
| Data loss | Write conflicts | Use async queue |
| Memory pressure | Too many cached reads | Use generators |
| Event storms | No debouncing | Debounce user input |
| Missed updates | Over-throttling | Reduce throttle time |
Examples
Progress Modal for Long Operations
async function processAllFiles(app: App, plugin: Plugin) {
const files = app.vault.getMarkdownFiles();
const progressModal = new ProgressModal(app);
progressModal.open();
try {
await processBatches(
files,
async (file) => {
return processFile(file);
},
{
batchSize: 20,
delayBetweenBatches: 50,
onProgress: (processed, total) => {
const percent = Math.round((processed / total) * 100);
progressModal.setProgress(percent, `Processing ${processed}/${total} files`);
},
}
);
new Notice('Processing complete!');
} finally {
progressModal.close();
}
}
Generator for Large File Sets
async function* iterateFilesWithPause(
files: TFile[],
pauseEvery: number = 100,
pauseMs: number = 10
): AsyncGenerator<TFile> {
for (let i = 0; i < files.length; i++) {
yield files[i];
if (i > 0 && i % pauseEvery === 0) {
await new Promise(r => setTimeout(r, pauseMs));
}
}
}
for await (const file of iterateFilesWithPause(files)) {
await processFile(file);
}
Resources
Next Steps
For security practices, see obsidian-security-basics.