From 20d48c09fd705cd7ea4edc5bad3249bf51c62d5f Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 11:22:03 +0000 Subject: [PATCH 01/11] add storage queues --- .editorconfig | 2 +- .vscode/settings.json | 7 + packages/server/src/Hocuspocus.ts | 64 +++-- packages/server/src/types.ts | 375 ++++++++++++++------------- packages/server/src/util/debounce.ts | 17 +- tests/server/afterStoreDocument.ts | 69 ++++- tests/server/onStoreDocument.ts | 67 +++++ tests/utils/storeDocument.ts | 50 ++++ 8 files changed, 444 insertions(+), 207 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 tests/utils/storeDocument.ts diff --git a/.editorconfig b/.editorconfig index efb4918cf..05cad8d4c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,4 +9,4 @@ insert_final_newline = true max_line_length = 100 trim_trailing_whitespace = true indent_style = space -indent_size = 2 +indent_size = 2 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..382f91ad7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", + "editor.formatOnPaste": false, // required + "editor.formatOnType": false, // required + "editor.formatOnSaveMode": "file", // required to format on save + "vs-code-prettier-eslint.prettierLast": false // set as "true" to run 'prettier' last not first +} diff --git a/packages/server/src/Hocuspocus.ts b/packages/server/src/Hocuspocus.ts index 77df371a9..ae2e0e3a9 100644 --- a/packages/server/src/Hocuspocus.ts +++ b/packages/server/src/Hocuspocus.ts @@ -19,6 +19,7 @@ import { AwarenessUpdate, Configuration, ConnectionConfiguration, + Extension, HookName, HookPayloadByName, beforeBroadcastStatelessPayload, @@ -43,6 +44,8 @@ export const defaultConfiguration = { gcFilter: () => true, }, unloadImmediately: true, + storageQueue: 'default', + storageQueues: { default: {} }, } /** @@ -91,6 +94,11 @@ export class Hocuspocus { this.configuration = { ...this.configuration, ...configuration, + storageQueues: { + default: {}, + ...this.configuration.storageQueues, + ...configuration.storageQueues, + }, } this.configuration.extensions.sort((a, b) => { @@ -126,6 +134,14 @@ export class Hocuspocus { onRequest: this.configuration.onRequest, onDisconnect: this.configuration.onDisconnect, onDestroy: this.configuration.onDestroy, + storageQueue: this.configuration.storageQueue, + }) + + // create storage queues that are referenced by extensions but not pre-defined at the top level + this.configuration.extensions.forEach(extension => { + if (extension.storageQueue && !this.configuration.storageQueues[extension.storageQueue]) { + this.configuration.storageQueues[extension.storageQueue] = {} + } }) this.hooks('onConfigure', { @@ -331,14 +347,7 @@ export class Hocuspocus { // Only run this if the document has finished loading earlier (i.e. not to persist the empty // ydoc if the onLoadDocument hook returned an error) if (!document.isLoading) { - this.debounce( - `onStoreDocument-${document.name}`, - () => { - this.storeDocumentHooks(document, hookPayload) - }, - this.configuration.unloadImmediately ? 0 : this.configuration.debounce, - this.configuration.maxDebounce, - ) + this.onStoreDocument(document, hookPayload, this.configuration.unloadImmediately) } else { // Remove document from memory immediately this.unloadDocument(document) @@ -378,14 +387,7 @@ export class Hocuspocus { return } - this.debounce( - `onStoreDocument-${document.name}`, - () => { - this.storeDocumentHooks(document, hookPayload) - }, - this.configuration.debounce, - this.configuration.maxDebounce, - ) + this.onStoreDocument(document, hookPayload) } /** @@ -461,10 +463,30 @@ export class Hocuspocus { return document } - storeDocumentHooks(document: Document, hookPayload: onStoreDocumentPayload) { - this.hooks('onStoreDocument', hookPayload) + onStoreDocument(document: Document, hookPayload: onStoreDocumentPayload, unloadImmediately = false) { + const promises = Object.entries(this.configuration.storageQueues).map(([queue, { debounce = this.configuration.debounce, maxDebounce = this.configuration.maxDebounce }]) => { + return this.debounce( + `onStoreDocument-${queue}-${document.name}`, + () => { + this.storeDocumentHooks(document, hookPayload, queue) + }, + unloadImmediately ? 0 : debounce, + maxDebounce, + ) + }) + + return Promise.all(promises) + } + + storeDocumentHooks(document: Document, hookPayload: onStoreDocumentPayload, queue = 'default') { + const filter = (extension: Extension) => { + return ( + extension.storageQueue === queue || (extension.storageQueue === undefined && queue === 'default') + ) + } + return this.hooks('onStoreDocument', hookPayload, null, filter) .then(() => { - this.hooks('afterStoreDocument', hookPayload).then(() => { + this.hooks('afterStoreDocument', hookPayload, null, filter).then(() => { // Remove document from memory. if (document.getConnectionsCount() > 0) { @@ -487,7 +509,7 @@ export class Hocuspocus { * Run the given hook on all configured extensions. * Runs the given callback after each hook. */ - hooks(name: T, payload: HookPayloadByName[T], callback: Function | null = null): Promise { + hooks(name: T, payload: HookPayloadByName[T], callback: Function | null = null, filter?: (extension: Extension) => boolean): Promise { const { extensions } = this.configuration // create a new `thenable` chain @@ -496,7 +518,7 @@ export class Hocuspocus { extensions // get me all extensions which have the given hook - .filter(extension => typeof extension[name] === 'function') + .filter(extension => typeof extension[name] === 'function' && (filter?.(extension) ?? true)) // run through all the configured hooks .forEach(extension => { chain = chain diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 12f6602f3..c5608d437 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,6 +1,4 @@ -import { - IncomingHttpHeaders, IncomingMessage, ServerResponse, -} from 'http' +import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http' import { URLSearchParams } from 'url' import { Awareness } from 'y-protocols/awareness' import Connection from './Connection.js' @@ -21,20 +19,21 @@ export enum MessageType { } export interface AwarenessUpdate { - added: Array, - updated: Array, - removed: Array, + added: Array; + updated: Array; + removed: Array; } export interface ConnectionConfiguration { - readOnly: boolean - requiresAuthentication: boolean - isAuthenticated: boolean + readOnly: boolean; + requiresAuthentication: boolean; + isAuthenticated: boolean; } export interface Extension { priority?: number; extensionName?: string; + storageQueue?: string; onConfigure?(data: onConfigurePayload): Promise; onListen?(data: onListenPayload): Promise; onUpgrade?(data: onUpgradePayload): Promise; @@ -57,81 +56,88 @@ export interface Extension { } export type HookName = - 'onConfigure' | - 'onListen' | - 'onUpgrade' | - 'onConnect' | - 'connected' | - 'onAuthenticate' | - 'onLoadDocument' | - 'afterLoadDocument' | - 'beforeHandleMessage' | - 'beforeBroadcastStateless' | - 'onStateless' | - 'onChange' | - 'onStoreDocument' | - 'afterStoreDocument' | - 'onAwarenessUpdate' | - 'onRequest' | - 'onDisconnect' | - 'afterUnloadDocument' | - 'onDestroy' + | 'onConfigure' + | 'onListen' + | 'onUpgrade' + | 'onConnect' + | 'connected' + | 'onAuthenticate' + | 'onLoadDocument' + | 'afterLoadDocument' + | 'beforeHandleMessage' + | 'beforeBroadcastStateless' + | 'onStateless' + | 'onChange' + | 'onStoreDocument' + | 'afterStoreDocument' + | 'onAwarenessUpdate' + | 'onRequest' + | 'onDisconnect' + | 'afterUnloadDocument' + | 'onDestroy'; export type HookPayloadByName = { - onConfigure: onConfigurePayload, - onListen: onListenPayload, - onUpgrade: onUpgradePayload, - onConnect: onConnectPayload, - connected: connectedPayload, - onAuthenticate: onAuthenticatePayload, - onLoadDocument: onLoadDocumentPayload, - afterLoadDocument: afterLoadDocumentPayload, - beforeHandleMessage: beforeHandleMessagePayload, - beforeBroadcastStateless: beforeBroadcastStatelessPayload, - onStateless: onStatelessPayload, - onChange: onChangePayload, - onStoreDocument: onStoreDocumentPayload, - afterStoreDocument: afterStoreDocumentPayload, - onAwarenessUpdate: onAwarenessUpdatePayload, - onRequest: onRequestPayload, - onDisconnect: onDisconnectPayload, - afterUnloadDocument: afterUnloadDocumentPayload, - onDestroy: onDestroyPayload, -} + onConfigure: onConfigurePayload; + onListen: onListenPayload; + onUpgrade: onUpgradePayload; + onConnect: onConnectPayload; + connected: connectedPayload; + onAuthenticate: onAuthenticatePayload; + onLoadDocument: onLoadDocumentPayload; + afterLoadDocument: afterLoadDocumentPayload; + beforeHandleMessage: beforeHandleMessagePayload; + beforeBroadcastStateless: beforeBroadcastStatelessPayload; + onStateless: onStatelessPayload; + onChange: onChangePayload; + onStoreDocument: onStoreDocumentPayload; + afterStoreDocument: afterStoreDocumentPayload; + onAwarenessUpdate: onAwarenessUpdatePayload; + onRequest: onRequestPayload; + onDisconnect: onDisconnectPayload; + afterUnloadDocument: afterUnloadDocumentPayload; + onDestroy: onDestroyPayload; +}; + +export type StorageQueueConfigs = { + [key: string]: { + debounce?: number; + maxDebounce?: number; + }; +}; export interface Configuration extends Extension { /** * A name for the instance, used for logging. */ - name: string | null, + name: string | null; /** * A list of hocuspocus extenions. */ - extensions: Array, + extensions: Array; /** * The port which the server listens on. */ - port?: number, + port?: number; /** * The address which the server listens on. */ - address?: string, + address?: string; /** * Defines in which interval the server sends a ping, and closes the connection when no pong is sent back. */ - timeout: number, + timeout: number; /** * Debounces the call of the `onStoreDocument` hook for the given amount of time in ms. * Otherwise every single update would be persisted. */ - debounce: number, + debounce: number; /** * Makes sure to call `onStoreDocument` at least in the given amount of time (ms). */ - maxDebounce: number + maxDebounce: number; /** * By default, the servers show a start screen. If passed false, the server will start quietly. */ - quiet: boolean, + quiet: boolean; /** * If set to false, respects the debounce time of `onStoreDocument` before unloading a document. * Otherwise, the document will be unloaded immediately. @@ -139,208 +145,213 @@ export interface Configuration extends Extension { * This prevents a client from DOSing the server by repeatedly connecting and disconnecting when * your onStoreDocument is rate-limited. */ - unloadImmediately: boolean, + unloadImmediately: boolean; /** * options to pass to the ydoc document */ yDocOptions: { - gc: boolean, // enable or disable garbage collection (see https://github.com/yjs/yjs/blob/main/INTERNALS.md#deletions) - gcFilter: () => boolean, // will be called before garbage collecting ; return false to keep it - }, + gc: boolean; // enable or disable garbage collection (see https://github.com/yjs/yjs/blob/main/INTERNALS.md#deletions) + gcFilter: () => boolean; // will be called before garbage collecting ; return false to keep it + }; + /** + * Define specific debounce settings for each storage queue, allowing multiple extensions to store + * documents in different locations in parallel at different rates. + */ + storageQueues: StorageQueueConfigs; } export interface onStatelessPayload { - connection: Connection, - documentName: string, - document: Document, - payload: string, + connection: Connection; + documentName: string; + document: Document; + payload: string; } // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface onAuthenticatePayload { - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - token: string, - connection: ConnectionConfiguration + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + token: string; + connection: ConnectionConfiguration; } // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface onConnectPayload { - context: any, - documentName: string, - instance: Hocuspocus, - request: IncomingMessage, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - connection: ConnectionConfiguration + context: any; + documentName: string; + instance: Hocuspocus; + request: IncomingMessage; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + connection: ConnectionConfiguration; } // @todo Change 'connection' to 'connectionConfig', and 'connectionInstance' to 'connection' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface connectedPayload { - context: any, - documentName: string, - instance: Hocuspocus, - request: IncomingMessage, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - connection: ConnectionConfiguration, - connectionInstance: Connection + context: any; + documentName: string; + instance: Hocuspocus; + request: IncomingMessage; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + connection: ConnectionConfiguration; + connectionInstance: Connection; } // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface onLoadDocumentPayload { - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - connection: ConnectionConfiguration + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + connection: ConnectionConfiguration; } // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface afterLoadDocumentPayload { - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - connection: ConnectionConfiguration + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + connection: ConnectionConfiguration; } export interface onChangePayload { - clientsCount: number, - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - update: Uint8Array, - socketId: string, - transactionOrigin: any, + clientsCount: number; + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + update: Uint8Array; + socketId: string; + transactionOrigin: any; } export interface beforeHandleMessagePayload { - clientsCount: number, - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - update: Uint8Array, - socketId: string, - connection: Connection + clientsCount: number; + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + update: Uint8Array; + socketId: string; + connection: Connection; } export interface beforeBroadcastStatelessPayload { - document: Document, - documentName: string, - payload: string, + document: Document; + documentName: string; + payload: string; } export interface onStoreDocumentPayload { - clientsCount: number, - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - transactionOrigin?: any, + clientsCount: number; + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + transactionOrigin?: any; } export interface afterStoreDocumentPayload extends onStoreDocumentPayload {} export interface onAwarenessUpdatePayload { - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - added: number[], - updated: number[], - removed: number[], - awareness: Awareness, - states: StatesArray, + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + added: number[]; + updated: number[]; + removed: number[]; + awareness: Awareness; + states: StatesArray; } -export type StatesArray = { clientId: number, [key: string | number]: any }[] +export type StatesArray = { clientId: number; [key: string | number]: any }[]; // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface fetchPayload { - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, - connection: ConnectionConfiguration + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; + connection: ConnectionConfiguration; } export interface storePayload extends onStoreDocumentPayload { - state: Buffer, + state: Buffer; } export interface onDisconnectPayload { - clientsCount: number, - context: any, - document: Document, - documentName: string, - instance: Hocuspocus, - requestHeaders: IncomingHttpHeaders, - requestParameters: URLSearchParams, - socketId: string, + clientsCount: number; + context: any; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: IncomingHttpHeaders; + requestParameters: URLSearchParams; + socketId: string; } export interface onRequestPayload { - request: IncomingMessage, - response: ServerResponse, - instance: Hocuspocus, + request: IncomingMessage; + response: ServerResponse; + instance: Hocuspocus; } export interface onUpgradePayload { - request: IncomingMessage, - socket: any, - head: any, - instance: Hocuspocus, + request: IncomingMessage; + socket: any; + head: any; + instance: Hocuspocus; } export interface onListenPayload { - instance: Hocuspocus, - configuration: Configuration, - port: number, + instance: Hocuspocus; + configuration: Configuration; + port: number; } export interface onDestroyPayload { - instance: Hocuspocus, + instance: Hocuspocus; } export interface onConfigurePayload { - instance: Hocuspocus, - configuration: Configuration, - version: string, + instance: Hocuspocus; + configuration: Configuration; + version: string; } export interface afterUnloadDocumentPayload { @@ -349,6 +360,6 @@ export interface afterUnloadDocumentPayload { } export interface DirectConnection { - transact(transaction: (document: Document) => void): Promise, - disconnect(): void + transact(transaction: (document: Document) => void): Promise; + disconnect(): void; } diff --git a/packages/server/src/util/debounce.ts b/packages/server/src/util/debounce.ts index c0eea4f47..1103dcccb 100644 --- a/packages/server/src/util/debounce.ts +++ b/packages/server/src/util/debounce.ts @@ -1,6 +1,8 @@ export const useDebounce = () => { const timers: Map, + resolve: () => void, start: number }> = new Map() @@ -10,12 +12,20 @@ export const useDebounce = () => { debounce: number, maxDebounce: number, ) => { + // default function to satisfy typescript + let newResolve: () => void = () => {} + const newPromise = new Promise(resolve => { + newResolve = resolve + }) const old = timers.get(id) const start = old?.start || Date.now() + const promise = old?.promise || newPromise + const resolve = old?.resolve || newResolve - const run = () => { + const run = async () => { timers.delete(id) - func() + await func() + resolve() } if (old?.timeout) { @@ -33,7 +43,10 @@ export const useDebounce = () => { timers.set(id, { start, timeout: setTimeout(run, debounce), + promise, + resolve, }) + return newPromise } return debounce diff --git a/tests/server/afterStoreDocument.ts b/tests/server/afterStoreDocument.ts index 57068f48e..49f505ae7 100644 --- a/tests/server/afterStoreDocument.ts +++ b/tests/server/afterStoreDocument.ts @@ -1,5 +1,6 @@ import test from 'ava' -import { newHocuspocus, newHocuspocusProvider } from '../utils/index.js' +import { assertThrottledCallback, createPromiseWithResolve, createStorageQueueExtension } from 'tests/utils/storeDocument.js' +import { newHocuspocus, newHocuspocusProvider, newHocuspocusProviderWebsocket } from '../utils/index.js' test('calls the afterStoreDocument hook', async t => { await new Promise(async resolve => { @@ -43,3 +44,69 @@ test('executes afterStoreDocument callback from a custom extension', async t => }) }) }) + +test('executes afterStoreDocument individually for each storageQueue', async t => { + let startTime = 0 + const [a1Promise, a1Resolve] = createPromiseWithResolve() + const [a2Promise, a2Resolve] = createPromiseWithResolve() + const [b1Promise, b1Resolve] = createPromiseWithResolve() + const [b2Promise, b2Resolve] = createPromiseWithResolve() + const [default1Promise, default1Resolve] = createPromiseWithResolve() + const [default2Promise, default2Resolve] = createPromiseWithResolve() + + function assertAfterStoreDocumentThrottled(minTime: number, maxTime: number, resolve: () => void, extensionName = 'default') { + assertThrottledCallback(t, startTime, minTime, maxTime, resolve, 'afterStoreDocument', extensionName) + } + + function createAfterStoreDocumentExtension(extensionName: string, storageQueue: string, debounceMin: number, debounceMax: number, resolve: () => void) { + return createStorageQueueExtension( + extensionName, + storageQueue, + { + async afterStoreDocument() { + assertAfterStoreDocumentThrottled(debounceMin, debounceMax, resolve, extensionName) + }, + }, + ) + } + + const extensions = [ + createAfterStoreDocumentExtension('a1', 'a', 0, 500, a1Resolve), + createAfterStoreDocumentExtension('a2', 'a', 0, 500, a2Resolve), + createAfterStoreDocumentExtension('b1', 'b', 500, 1000, b1Resolve), + createAfterStoreDocumentExtension('b2', 'b', 500, 1000, b2Resolve), + createAfterStoreDocumentExtension('default1', 'default1', 1000, 1500, default1Resolve), + ] + + const server = await newHocuspocus({ + unloadImmediately: false, + debounce: 1000, + maxDebounce: 1500, + async afterStoreDocument() { + assertAfterStoreDocumentThrottled(1000, 1500, default2Resolve, 'default2') + }, + extensions, + storageQueues: { + a: { + debounce: 0, + maxDebounce: 500, + }, + b: { + debounce: 500, + maxDebounce: 100, + }, + }, + }) + + const socket = newHocuspocusProviderWebsocket(server) + + newHocuspocusProvider(server, { + websocketProvider: socket, + onSynced() { + startTime = Date.now() + socket.destroy() + }, + }) + + await Promise.all([a1Promise, a2Promise, b1Promise, b2Promise, default1Promise, default2Promise]) +}) diff --git a/tests/server/onStoreDocument.ts b/tests/server/onStoreDocument.ts index 32634e448..b634eb4a5 100644 --- a/tests/server/onStoreDocument.ts +++ b/tests/server/onStoreDocument.ts @@ -1,5 +1,6 @@ import test from 'ava' import { onStoreDocumentPayload } from '@hocuspocus/server' +import { assertThrottledCallback, createPromiseWithResolve, createStorageQueueExtension } from 'tests/utils/storeDocument.js' import { newHocuspocus, newHocuspocusProvider, newHocuspocusProviderWebsocket, sleep, } from '../utils/index.js' @@ -414,3 +415,69 @@ test('waits before calling onStoreDocument after the last user disconnects when }) }) }) + +test('storageQueues throttle individually of each other', async t => { + let startTime = 0 + const [a1Promise, a1Resolve] = createPromiseWithResolve() + const [a2Promise, a2Resolve] = createPromiseWithResolve() + const [b1Promise, b1Resolve] = createPromiseWithResolve() + const [b2Promise, b2Resolve] = createPromiseWithResolve() + const [default1Promise, default1Resolve] = createPromiseWithResolve() + const [default2Promise, default2Resolve] = createPromiseWithResolve() + + function assertOnStoreDocumentThrottled(minTime: number, maxTime: number, resolve: () => void, extensionName = 'default') { + assertThrottledCallback(t, startTime, minTime, maxTime, resolve, 'onStoreDocument', extensionName) + } + + function createOnStoreDocumentExtension(extensionName: string, storageQueue: string, debounceMin: number, debounceMax: number, resolve: () => void) { + return createStorageQueueExtension( + extensionName, + storageQueue, + { + async onStoreDocument() { + assertOnStoreDocumentThrottled(debounceMin, debounceMax, resolve, extensionName) + }, + }, + ) + } + + const extensions = [ + createOnStoreDocumentExtension('a1', 'a', 0, 500, a1Resolve), + createOnStoreDocumentExtension('a2', 'a', 0, 500, a2Resolve), + createOnStoreDocumentExtension('b1', 'b', 500, 1000, b1Resolve), + createOnStoreDocumentExtension('b2', 'b', 500, 1000, b2Resolve), + createOnStoreDocumentExtension('default1', 'default1', 1000, 1500, default1Resolve), + ] + + const server = await newHocuspocus({ + unloadImmediately: false, + debounce: 1000, + maxDebounce: 1500, + async onStoreDocument() { + assertOnStoreDocumentThrottled(1000, 1500, default2Resolve, 'default2') + }, + extensions, + storageQueues: { + a: { + debounce: 0, + maxDebounce: 500, + }, + b: { + debounce: 500, + maxDebounce: 100, + }, + }, + }) + + const socket = newHocuspocusProviderWebsocket(server) + + newHocuspocusProvider(server, { + websocketProvider: socket, + onSynced() { + startTime = Date.now() + socket.destroy() + }, + }) + + await Promise.all([a1Promise, a2Promise, b1Promise, b2Promise, default1Promise, default2Promise]) +}) diff --git a/tests/utils/storeDocument.ts b/tests/utils/storeDocument.ts new file mode 100644 index 000000000..74b9a367a --- /dev/null +++ b/tests/utils/storeDocument.ts @@ -0,0 +1,50 @@ +import { Extension } from '@hocuspocus/server' +import { ExecutionContext } from 'ava' + +export function createPromiseWithResolve(): [Promise, () => void] { + let resolve: () => void = () => {} + const promise = new Promise(r => { + resolve = r + }) + return [promise, resolve] +} + +export function assertThrottledCallback( + t: ExecutionContext, + startTime: number, + minTime: number, + maxTime: number, + resolve: () => void, + callbackName: string, + extensionName = 'default', +) { + const endTime = Date.now() + const totalTime = endTime - startTime + if (startTime === 0) { + t.fail('startTime not set') + } else if (totalTime < minTime) { + t.fail( + `did not wait ${minTime}ms to call ${callbackName} (${totalTime}ms) (extension ${extensionName})`, + ) + } else if (totalTime > maxTime) { + t.fail( + `waited longer than ${maxTime}ms to call ${callbackName} (${totalTime}ms) (extension ${extensionName})`, + ) + } else { + t.pass(extensionName) + } + resolve() +} + +export function createStorageQueueExtension( + extensionName: string, + storageQueue: string, + extension: Partial = {}, +) { + return { + extensionName, + storageQueue, + async onStoreDocument() {}, + ...extension, + } +} From f0511ccb3961d4539946fbfabc60c8c53e03f49c Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 11:34:50 +0000 Subject: [PATCH 02/11] lint --- packages/server/src/util/debounce.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/util/debounce.ts b/packages/server/src/util/debounce.ts index e00f93550..1f861813f 100644 --- a/packages/server/src/util/debounce.ts +++ b/packages/server/src/util/debounce.ts @@ -1,8 +1,8 @@ export const useDebounce = () => { const timers: Map, - resolve: () => void, + promise: Promise, + resolve: (value: unknown) => void, start: number }> = new Map() @@ -13,8 +13,8 @@ export const useDebounce = () => { maxDebounce: number, ) => { // default function to satisfy typescript - let newResolve: () => void = () => {} - const newPromise = new Promise(resolve => { + let newResolve: (value: unknown) => void = () => {} + const newPromise = new Promise(resolve => { newResolve = resolve }) const old = timers.get(id) From bfff71dece908d178ae14f661e623cf89851af73 Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 11:38:53 +0000 Subject: [PATCH 03/11] remove config changes --- .editorconfig | 2 +- .vscode/settings.json | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.editorconfig b/.editorconfig index 05cad8d4c..efb4918cf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,4 +9,4 @@ insert_final_newline = true max_line_length = 100 trim_trailing_whitespace = true indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 382f91ad7..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", - "editor.formatOnPaste": false, // required - "editor.formatOnType": false, // required - "editor.formatOnSaveMode": "file", // required to format on save - "vs-code-prettier-eslint.prettierLast": false // set as "true" to run 'prettier' last not first -} From f89a2717794529a59f34fe2c717b72197c727ef3 Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 11:42:58 +0000 Subject: [PATCH 04/11] fix formatting change --- packages/server/src/types.ts | 366 +++++++++++++++++------------------ 1 file changed, 183 insertions(+), 183 deletions(-) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index c1638609b..8e90f885b 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -19,40 +19,40 @@ export enum MessageType { } export interface AwarenessUpdate { - added: Array; - updated: Array; - removed: Array; + added: Array, + updated: Array, + removed: Array, } export interface ConnectionConfiguration { - readOnly: boolean; - requiresAuthentication: boolean; - isAuthenticated: boolean; + readOnly: boolean, + requiresAuthentication: boolean, + isAuthenticated: boolean, } export interface Extension { - priority?: number; - extensionName?: string; - storageQueue?: string; - onConfigure?(data: onConfigurePayload): Promise; - onListen?(data: onListenPayload): Promise; - onUpgrade?(data: onUpgradePayload): Promise; - onConnect?(data: onConnectPayload): Promise; - connected?(data: connectedPayload): Promise; - onAuthenticate?(data: onAuthenticatePayload): Promise; - onLoadDocument?(data: onLoadDocumentPayload): Promise; - afterLoadDocument?(data: afterLoadDocumentPayload): Promise; - beforeHandleMessage?(data: beforeHandleMessagePayload): Promise; - beforeBroadcastStateless?(data: beforeBroadcastStatelessPayload): Promise; - onStateless?(payload: onStatelessPayload): Promise; - onChange?(data: onChangePayload): Promise; - onStoreDocument?(data: onStoreDocumentPayload): Promise; - afterStoreDocument?(data: afterStoreDocumentPayload): Promise; - onAwarenessUpdate?(data: onAwarenessUpdatePayload): Promise; - onRequest?(data: onRequestPayload): Promise; - onDisconnect?(data: onDisconnectPayload): Promise; - afterUnloadDocument?(data: afterUnloadDocumentPayload): Promise; - onDestroy?(data: onDestroyPayload): Promise; + priority?: number, + extensionName?: string, + storageQueue?: string, + onConfigure?(data: onConfigurePayload): Promise, + onListen?(data: onListenPayload): Promise, + onUpgrade?(data: onUpgradePayload): Promise, + onConnect?(data: onConnectPayload): Promise, + connected?(data: connectedPayload): Promise, + onAuthenticate?(data: onAuthenticatePayload): Promise, + onLoadDocument?(data: onLoadDocumentPayload): Promise, + afterLoadDocument?(data: afterLoadDocumentPayload): Promise, + beforeHandleMessage?(data: beforeHandleMessagePayload): Promise, + beforeBroadcastStateless?(data: beforeBroadcastStatelessPayload): Promise, + onStateless?(payload: onStatelessPayload): Promise, + onChange?(data: onChangePayload): Promise, + onStoreDocument?(data: onStoreDocumentPayload): Promise, + afterStoreDocument?(data: afterStoreDocumentPayload): Promise, + onAwarenessUpdate?(data: onAwarenessUpdatePayload): Promise, + onRequest?(data: onRequestPayload): Promise, + onDisconnect?(data: onDisconnectPayload): Promise, + afterUnloadDocument?(data: afterUnloadDocumentPayload): Promise, + onDestroy?(data: onDestroyPayload): Promise, } export type HookName = @@ -74,70 +74,70 @@ export type HookName = | 'onRequest' | 'onDisconnect' | 'afterUnloadDocument' - | 'onDestroy'; + | 'onDestroy' export type HookPayloadByName = { - onConfigure: onConfigurePayload; - onListen: onListenPayload; - onUpgrade: onUpgradePayload; - onConnect: onConnectPayload; - connected: connectedPayload; - onAuthenticate: onAuthenticatePayload; - onLoadDocument: onLoadDocumentPayload; - afterLoadDocument: afterLoadDocumentPayload; - beforeHandleMessage: beforeHandleMessagePayload; - beforeBroadcastStateless: beforeBroadcastStatelessPayload; - onStateless: onStatelessPayload; - onChange: onChangePayload; - onStoreDocument: onStoreDocumentPayload; - afterStoreDocument: afterStoreDocumentPayload; - onAwarenessUpdate: onAwarenessUpdatePayload; - onRequest: onRequestPayload; - onDisconnect: onDisconnectPayload; - afterUnloadDocument: afterUnloadDocumentPayload; - onDestroy: onDestroyPayload; -}; + onConfigure: onConfigurePayload, + onListen: onListenPayload, + onUpgrade: onUpgradePayload, + onConnect: onConnectPayload, + connected: connectedPayload, + onAuthenticate: onAuthenticatePayload, + onLoadDocument: onLoadDocumentPayload, + afterLoadDocument: afterLoadDocumentPayload, + beforeHandleMessage: beforeHandleMessagePayload, + beforeBroadcastStateless: beforeBroadcastStatelessPayload, + onStateless: onStatelessPayload, + onChange: onChangePayload, + onStoreDocument: onStoreDocumentPayload, + afterStoreDocument: afterStoreDocumentPayload, + onAwarenessUpdate: onAwarenessUpdatePayload, + onRequest: onRequestPayload, + onDisconnect: onDisconnectPayload, + afterUnloadDocument: afterUnloadDocumentPayload, + onDestroy: onDestroyPayload, +}, export type StorageQueueConfigs = { [key: string]: { - debounce?: number; - maxDebounce?: number; - }; -}; + debounce?: number, + maxDebounce?: number, + }, +}, export interface Configuration extends Extension { /** * A name for the instance, used for logging. */ - name: string | null; + name: string | null, /** * A list of hocuspocus extenions. */ - extensions: Array; + extensions: Array, /** * The port which the server listens on. */ - port?: number; + port?: number, /** * The address which the server listens on. */ - address?: string; + address?: string, /** * Defines in which interval the server sends a ping, and closes the connection when no pong is sent back. */ - timeout: number; + timeout: number, /** * Debounces the call of the `onStoreDocument` hook for the given amount of time in ms. * Otherwise every single update would be persisted. */ - debounce: number; + debounce: number, /** * Makes sure to call `onStoreDocument` at least in the given amount of time (ms). */ - maxDebounce: number; + maxDebounce: number, /** * By default, the servers show a start screen. If passed false, the server will start quietly. */ - quiet: boolean; + quiet: boolean, /** * If set to false, respects the debounce time of `onStoreDocument` before unloading a document. * Otherwise, the document will be unloaded immediately. @@ -145,7 +145,7 @@ export interface Configuration extends Extension { * This prevents a client from DOSing the server by repeatedly connecting and disconnecting when * your onStoreDocument is rate-limited. */ - unloadImmediately: boolean; + unloadImmediately: boolean, /** * the server will gracefully stop if SIGINT, SIGQUIT or SIGTERM is received. @@ -158,22 +158,22 @@ export interface Configuration extends Extension { * options to pass to the ydoc document */ yDocOptions: { - gc: boolean; // enable or disable garbage collection (see https://github.com/yjs/yjs/blob/main/INTERNALS.md#deletions) - gcFilter: () => boolean; // will be called before garbage collecting ; return false to keep it - }; + gc: boolean, // enable or disable garbage collection (see https://github.com/yjs/yjs/blob/main/INTERNALS.md#deletions) + gcFilter: () => boolean, // will be called before garbage collecting , return false to keep it + }, /** * Define specific debounce settings for each storage queue, allowing multiple extensions to store * documents in different locations in parallel at different rates. */ - storageQueues: StorageQueueConfigs; + storageQueues: StorageQueueConfigs, } export interface onStatelessPayload { - connection: Connection; - documentName: string; - document: Document; - payload: string; + connection: Connection, + documentName: string, + document: Document, + payload: string, } // @todo Change 'connection' to 'connectionConfig' in next major release @@ -192,182 +192,182 @@ export interface onAuthenticatePayload { // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface onConnectPayload { - context: any; - documentName: string; - instance: Hocuspocus; - request: IncomingMessage; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; - connection: ConnectionConfiguration; + context: any, + documentName: string, + instance: Hocuspocus, + request: IncomingMessage, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, + connection: ConnectionConfiguration, } // @todo Change 'connection' to 'connectionConfig', and 'connectionInstance' to 'connection' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface connectedPayload { - context: any; - documentName: string; - instance: Hocuspocus; - request: IncomingMessage; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; - connection: ConnectionConfiguration; - connectionInstance: Connection; + context: any, + documentName: string, + instance: Hocuspocus, + request: IncomingMessage, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, + connection: ConnectionConfiguration, + connectionInstance: Connection, } // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface onLoadDocumentPayload { - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; - connection: ConnectionConfiguration; + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, + connection: ConnectionConfiguration, } // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface afterLoadDocumentPayload { - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; - connection: ConnectionConfiguration; + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, + connection: ConnectionConfiguration, } export interface onChangePayload { - clientsCount: number; - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - update: Uint8Array; - socketId: string; - transactionOrigin: any; + clientsCount: number, + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + update: Uint8Array, + socketId: string, + transactionOrigin: any, } export interface beforeHandleMessagePayload { - clientsCount: number; - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - update: Uint8Array; - socketId: string; - connection: Connection; + clientsCount: number, + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + update: Uint8Array, + socketId: string, + connection: Connection, } export interface beforeBroadcastStatelessPayload { - document: Document; - documentName: string; - payload: string; + document: Document, + documentName: string, + payload: string, } export interface onStoreDocumentPayload { - clientsCount: number; - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; - transactionOrigin?: any; + clientsCount: number, + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, + transactionOrigin?: any, } export interface afterStoreDocumentPayload extends onStoreDocumentPayload {} export interface onAwarenessUpdatePayload { - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; - added: number[]; - updated: number[]; - removed: number[]; - awareness: Awareness; - states: StatesArray; + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, + added: number[], + updated: number[], + removed: number[], + awareness: Awareness, + states: StatesArray, } -export type StatesArray = { clientId: number; [key: string | number]: any }[]; +export type StatesArray = { clientId: number, [key: string | number]: any }[], // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 export interface fetchPayload { - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; - connection: ConnectionConfiguration; + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, + connection: ConnectionConfiguration, } export interface storePayload extends onStoreDocumentPayload { - state: Buffer; + state: Buffer, } export interface onDisconnectPayload { - clientsCount: number; - context: any; - document: Document; - documentName: string; - instance: Hocuspocus; - requestHeaders: IncomingHttpHeaders; - requestParameters: URLSearchParams; - socketId: string; + clientsCount: number, + context: any, + document: Document, + documentName: string, + instance: Hocuspocus, + requestHeaders: IncomingHttpHeaders, + requestParameters: URLSearchParams, + socketId: string, } export interface onRequestPayload { - request: IncomingMessage; - response: ServerResponse; - instance: Hocuspocus; + request: IncomingMessage, + response: ServerResponse, + instance: Hocuspocus, } export interface onUpgradePayload { - request: IncomingMessage; - socket: any; - head: any; - instance: Hocuspocus; + request: IncomingMessage, + socket: any, + head: any, + instance: Hocuspocus, } export interface onListenPayload { - instance: Hocuspocus; - configuration: Configuration; - port: number; + instance: Hocuspocus, + configuration: Configuration, + port: number, } export interface onDestroyPayload { - instance: Hocuspocus; + instance: Hocuspocus, } export interface onConfigurePayload { - instance: Hocuspocus; - configuration: Configuration; - version: string; + instance: Hocuspocus, + configuration: Configuration, + version: string, } export interface afterUnloadDocumentPayload { - instance: Hocuspocus; - documentName: string; + instance: Hocuspocus, + documentName: string, } export interface DirectConnection { - transact(transaction: (document: Document) => void): Promise; - disconnect(): void; + transact(transaction: (document: Document) => void): Promise, + disconnect(): void, } From 57bfd77934a25e0f474e304c013ff7191f1ac97c Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 11:44:29 +0000 Subject: [PATCH 05/11] more formatting --- packages/server/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 8e90f885b..c7c117133 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -96,14 +96,14 @@ export type HookPayloadByName = { onDisconnect: onDisconnectPayload, afterUnloadDocument: afterUnloadDocumentPayload, onDestroy: onDestroyPayload, -}, +} export type StorageQueueConfigs = { [key: string]: { debounce?: number, maxDebounce?: number, }, -}, +} export interface Configuration extends Extension { /** * A name for the instance, used for logging. From 7f47ed66b1c17dbeb6df25250ffe5600617d5979 Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 11:44:42 +0000 Subject: [PATCH 06/11] more formatting --- packages/server/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index c7c117133..5e26ac2e5 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -303,7 +303,7 @@ export interface onAwarenessUpdatePayload { states: StatesArray, } -export type StatesArray = { clientId: number, [key: string | number]: any }[], +export type StatesArray = { clientId: number, [key: string | number]: any }[] // @todo Change 'connection' to 'connectionConfig' in next major release // see https://github.com/ueberdosis/hocuspocus/pull/607#issuecomment-1553559805 From c520ef24188406eb2f1b289414e5e427aaa2e795 Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 11:57:40 +0000 Subject: [PATCH 07/11] add to direct connection --- packages/server/src/DirectConnection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/DirectConnection.ts b/packages/server/src/DirectConnection.ts index a4cebef24..7edce6351 100644 --- a/packages/server/src/DirectConnection.ts +++ b/packages/server/src/DirectConnection.ts @@ -32,7 +32,7 @@ export class DirectConnection implements DirectConnectionInterface { transaction(this.document) - await this.instance.storeDocumentHooks(this.document, { + await this.instance.onStoreDocument(this.document, { clientsCount: this.document.getConnectionsCount(), context: this.context, document: this.document, @@ -50,7 +50,7 @@ export class DirectConnection implements DirectConnectionInterface { this.document?.removeDirectConnection() - await this.instance.storeDocumentHooks(this.document, { + await this.instance.onStoreDocument(this.document, { clientsCount: this.document.getConnectionsCount(), context: this.context, document: this.document, From 5d06c4ab65786f83092f4712a9e6949856977790 Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 12:06:50 +0000 Subject: [PATCH 08/11] fix awaiting onStoreDocument --- packages/server/src/Hocuspocus.ts | 5 +---- packages/server/src/util/debounce.ts | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/server/src/Hocuspocus.ts b/packages/server/src/Hocuspocus.ts index cae6a4e6c..f4e931f59 100644 --- a/packages/server/src/Hocuspocus.ts +++ b/packages/server/src/Hocuspocus.ts @@ -491,14 +491,11 @@ export class Hocuspocus { const promises = Object.entries(this.configuration.storageQueues).map(([queue, { debounce = this.configuration.debounce, maxDebounce = this.configuration.maxDebounce }]) => { return this.debounce( `onStoreDocument-${queue}-${document.name}`, - () => { - this.storeDocumentHooks(document, hookPayload, queue) - }, + () => this.storeDocumentHooks(document, hookPayload, queue), unloadImmediately ? 0 : debounce, maxDebounce, ) }) - return Promise.all(promises) } diff --git a/packages/server/src/util/debounce.ts b/packages/server/src/util/debounce.ts index 1f861813f..929f51d7b 100644 --- a/packages/server/src/util/debounce.ts +++ b/packages/server/src/util/debounce.ts @@ -26,6 +26,7 @@ export const useDebounce = () => { timers.delete(id) const result = await func() resolve(result) + return result } if (old?.timeout) { From 3bad35220088caca97a97b8ac9d3b9fd8bd0ee0a Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 12:14:55 +0000 Subject: [PATCH 09/11] fix returning wrong promise from debounce --- packages/server/src/util/debounce.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/util/debounce.ts b/packages/server/src/util/debounce.ts index 929f51d7b..9690a92ad 100644 --- a/packages/server/src/util/debounce.ts +++ b/packages/server/src/util/debounce.ts @@ -47,7 +47,7 @@ export const useDebounce = () => { promise, resolve, }) - return newPromise + return promise } return debounce From 59f2a33ea27a541aa64ac045cd297e8f7b09ef4e Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 13:12:19 +0000 Subject: [PATCH 10/11] add comment to debounce --- packages/server/src/util/debounce.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/server/src/util/debounce.ts b/packages/server/src/util/debounce.ts index 9690a92ad..b5b148d0e 100644 --- a/packages/server/src/util/debounce.ts +++ b/packages/server/src/util/debounce.ts @@ -6,6 +6,10 @@ export const useDebounce = () => { start: number }> = new Map() + /** + * Debounce returns a promise that resolves when the function is eventually called. + * All calls to the function within a given debounce window will recieve the same promise. + */ const debounce = ( id: string, func: Function, From 7621e43f57d1e825312fa13c8a6b2709f44ddfe5 Mon Sep 17 00:00:00 2001 From: George Reith Date: Thu, 16 Nov 2023 13:13:31 +0000 Subject: [PATCH 11/11] better comment --- packages/server/src/util/debounce.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/util/debounce.ts b/packages/server/src/util/debounce.ts index b5b148d0e..53bccf667 100644 --- a/packages/server/src/util/debounce.ts +++ b/packages/server/src/util/debounce.ts @@ -7,8 +7,8 @@ export const useDebounce = () => { }> = new Map() /** - * Debounce returns a promise that resolves when the function is eventually called. - * All calls to the function within a given debounce window will recieve the same promise. + * All calls to the function within a given debounce window will recieve the same promise that + * resolves when the debounced function has resolved. */ const debounce = ( id: string,