From e6b4ef9bf05917a509ebcfe429f2ac7e169bb566 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Thu, 6 Mar 2025 17:26:01 -0800 Subject: [PATCH 1/3] after 20 writes per second, only write the last value every 0.1s --- typescript/packages/jumble/src/iframe-ctx.ts | 78 +++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/typescript/packages/jumble/src/iframe-ctx.ts b/typescript/packages/jumble/src/iframe-ctx.ts index 0449d0b6c..a9c2a874c 100644 --- a/typescript/packages/jumble/src/iframe-ctx.ts +++ b/typescript/packages/jumble/src/iframe-ctx.ts @@ -17,6 +17,22 @@ const serializeProxyObjects = (proxy: any) => { return proxy == undefined ? undefined : JSON.parse(JSON.stringify(proxy)); }; +// Type for tracking write operations per context+key +type TimeoutId = ReturnType; +type WriteTracking = { + pendingTimeout: TimeoutId | null; + pendingValue: any; // Store the value to be written when timeout fires + writeCount: number; + lastResetTime: number; +}; + +// Map to store write tracking by context and key +const writeTrackers = new Map>(); + +// 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 + export const setupIframe = () => setIframeContextHandler({ read(context: any, key: string): any { @@ -25,10 +41,64 @@ export const setupIframe = () => return serialized; }, write(context: any, key: string, value: any) { - if (isCell(context)) { - context.key(key).setRaw(value); - } else { - context[key] = value; + // Get or create context map for this specific context+key + 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, + pendingValue: undefined, + 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++; + + // Perform write immediately + if (isCell(context)) { + context.key(key).setRaw(value); + } else { + context[key] = value; + } + } // Otherwise, use debouncing + else { + // Update the value to be written when the timeout fires + tracking.pendingValue = value; + + // Only set a new timeout if there isn't one already + if (!tracking.pendingTimeout) { + tracking.pendingTimeout = setTimeout(() => { + // Perform the actual write operation with the latest value + if (isCell(context)) { + context.key(key).setRaw(tracking.pendingValue); + } else { + context[key] = tracking.pendingValue; + } + + // Clear the timeout reference + tracking.pendingTimeout = null; + }, THROTTLED_WRITE_INTERVAL_MS); + } } }, subscribe( From e1c7ae8c251f68b93dc0871cdaea05b1e2af9973 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 7 Mar 2025 11:17:59 -0800 Subject: [PATCH 2/3] factor out throttle --- typescript/packages/jumble/src/iframe-ctx.ts | 112 ++++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/typescript/packages/jumble/src/iframe-ctx.ts b/typescript/packages/jumble/src/iframe-ctx.ts index a9c2a874c..2c2c03623 100644 --- a/typescript/packages/jumble/src/iframe-ctx.ts +++ b/typescript/packages/jumble/src/iframe-ctx.ts @@ -21,7 +21,7 @@ const serializeProxyObjects = (proxy: any) => { type TimeoutId = ReturnType; type WriteTracking = { pendingTimeout: TimeoutId | null; - pendingValue: any; // Store the value to be written when timeout fires + pendingCallback: (() => void) | null; // Store the callback to execute when timeout fires writeCount: number; lastResetTime: number; }; @@ -33,6 +33,60 @@ const writeTrackers = new Map>(); 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 { @@ -41,65 +95,13 @@ export const setupIframe = () => return serialized; }, write(context: any, key: string, value: any) { - // Get or create context map for this specific context+key - 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, - pendingValue: undefined, - 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++; - - // Perform write immediately + throttle(context, key, () => { if (isCell(context)) { context.key(key).setRaw(value); } else { context[key] = value; } - } // Otherwise, use debouncing - else { - // Update the value to be written when the timeout fires - tracking.pendingValue = value; - - // Only set a new timeout if there isn't one already - if (!tracking.pendingTimeout) { - tracking.pendingTimeout = setTimeout(() => { - // Perform the actual write operation with the latest value - if (isCell(context)) { - context.key(key).setRaw(tracking.pendingValue); - } else { - context[key] = tracking.pendingValue; - } - - // Clear the timeout reference - tracking.pendingTimeout = null; - }, THROTTLED_WRITE_INTERVAL_MS); - } - } + }); }, subscribe( context: any, From 2d21182b6d61e2543bcc0f1f31b09b9b4f6a2454 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 7 Mar 2025 11:19:27 -0800 Subject: [PATCH 3/3] only send data to iframe if it changed --- typescript/packages/jumble/src/iframe-ctx.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/typescript/packages/jumble/src/iframe-ctx.ts b/typescript/packages/jumble/src/iframe-ctx.ts index 2c2c03623..830ee17cd 100644 --- a/typescript/packages/jumble/src/iframe-ctx.ts +++ b/typescript/packages/jumble/src/iframe-ctx.ts @@ -108,12 +108,17 @@ export const setupIframe = () => 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);