Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 83 additions & 6 deletions typescript/packages/jumble/src/iframe-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,76 @@ const serializeProxyObjects = (proxy: any) => {
return proxy == undefined ? undefined : JSON.parse(JSON.stringify(proxy));
};

// Type for tracking write operations per context+key
type TimeoutId = ReturnType<typeof setTimeout>;
type WriteTracking = {
pendingTimeout: TimeoutId | null;
pendingCallback: (() => void) | null; // Store the callback to execute when timeout fires
writeCount: number;
lastResetTime: number;
};

// Map to store write tracking by context and key
const writeTrackers = new Map<any, Map<string, WriteTracking>>();

// Configuration
const MAX_IMMEDIATE_WRITES_PER_SECOND = 20; // Allow 20 immediate writes per second
const THROTTLED_WRITE_INTERVAL_MS = 100; // 0.1s interval after threshold

// Throttle function that handles write rate limiting
function throttle(context: any, key: string, callback: () => void): void {
// Get or create context map for this specific context
if (!writeTrackers.has(context)) {
writeTrackers.set(context, new Map());
}
const contextMap = writeTrackers.get(context)!;

// Get or initialize tracking info for this key
if (!contextMap.has(key)) {
contextMap.set(key, {
pendingTimeout: null,
pendingCallback: null,
writeCount: 0,
lastResetTime: Date.now(),
});
}

const tracking = contextMap.get(key)!;
const now = Date.now();

// Reset counter if a second has passed
if (now - tracking.lastResetTime > 1000) {
tracking.writeCount = 0;
tracking.lastResetTime = now;
if (tracking.pendingTimeout) {
clearTimeout(tracking.pendingTimeout);
tracking.pendingTimeout = null;
}
}

// If we're under the threshold, process immediately
if (tracking.writeCount < MAX_IMMEDIATE_WRITES_PER_SECOND) {
tracking.writeCount++;
// Execute callback immediately
callback();
} else {
// Update the callback to be executed when the timeout fires
tracking.pendingCallback = callback;

// Only set a new timeout if there isn't one already
if (!tracking.pendingTimeout) {
tracking.pendingTimeout = setTimeout(() => {
// Execute the latest callback
tracking.pendingCallback?.();

// Clear the timeout reference
tracking.pendingTimeout = null;
tracking.pendingCallback = null;
}, THROTTLED_WRITE_INTERVAL_MS);
}
}
}

export const setupIframe = () =>
setIframeContextHandler({
read(context: any, key: string): any {
Expand All @@ -25,23 +95,30 @@ export const setupIframe = () =>
return serialized;
},
write(context: any, key: string, value: any) {
if (isCell(context)) {
context.key(key).setRaw(value);
} else {
context[key] = value;
}
throttle(context, key, () => {
if (isCell(context)) {
context.key(key).setRaw(value);
} else {
context[key] = value;
}
});
},
subscribe(
context: any,
key: string,
callback: (key: string, value: any) => void,
): any {
let previousValue: any;

const action: Action = (log: ReactivityLog) => {
const data = isCell(context)
? context.withLog(log).key(key).get()
: context?.[key];
const serialized = serializeProxyObjects(data);
callback(key, serialized);
if (serialized !== previousValue) {
previousValue = serialized;
callback(key, serialized);
}
};

addAction(action);
Expand Down