@hocuspocus/provider 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hocuspocus-provider.cjs +73 -11
- package/dist/hocuspocus-provider.cjs.map +1 -1
- package/dist/hocuspocus-provider.esm.js +73 -11
- package/dist/hocuspocus-provider.esm.js.map +1 -1
- package/dist/index.d.ts +35 -0
- package/package.json +5 -5
- package/src/HocuspocusProvider.ts +135 -9
- package/src/OutgoingMessage.ts +2 -0
- package/src/types.ts +1 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
awarenessStatesToArray,
|
|
3
|
+
makeRoutingKey,
|
|
4
|
+
parseRoutingKey,
|
|
5
|
+
} from "@hocuspocus/common";
|
|
2
6
|
import { Awareness, removeAwarenessStates } from "y-protocols/awareness";
|
|
3
7
|
import * as Y from "yjs";
|
|
4
8
|
import EventEmitter from "./EventEmitter.ts";
|
|
@@ -82,7 +86,7 @@ export interface CompleteHocuspocusProviderConfiguration {
|
|
|
82
86
|
* sessionId in the documentName field of every message, allowing multiple
|
|
83
87
|
* providers with the same document name on a single WebSocket connection.
|
|
84
88
|
*
|
|
85
|
-
* Only set this to `true` when connecting to a v4 server that does
|
|
89
|
+
* Only set this to `true` when connecting to a v4 server that does
|
|
86
90
|
* support session awareness.
|
|
87
91
|
*
|
|
88
92
|
* Default: false
|
|
@@ -94,6 +98,22 @@ export interface CompleteHocuspocusProviderConfiguration {
|
|
|
94
98
|
*/
|
|
95
99
|
forceSyncInterval: false | number;
|
|
96
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Batch outgoing document and awareness updates over a short window (in
|
|
103
|
+
* milliseconds) instead of sending one message per change. During heavy
|
|
104
|
+
* editing this drastically reduces the number of websocket messages: Yjs
|
|
105
|
+
* updates collected in the window are merged with `Y.mergeUpdates` into a
|
|
106
|
+
* single message, and awareness collapses to the latest state of each
|
|
107
|
+
* changed client.
|
|
108
|
+
*
|
|
109
|
+
* The window is a fixed batch, not a resetting debounce, so the added
|
|
110
|
+
* latency is capped at `flushDelay` even while the user keeps typing. Keep
|
|
111
|
+
* it small (e.g. 500) to avoid delaying what other clients see.
|
|
112
|
+
*
|
|
113
|
+
* Set to `false` (the default) to send every change immediately.
|
|
114
|
+
*/
|
|
115
|
+
flushDelay: false | number;
|
|
116
|
+
|
|
97
117
|
onAuthenticated: (data: onAuthenticatedParameters) => void;
|
|
98
118
|
onAuthenticationFailed: (data: onAuthenticationFailedParameters) => void;
|
|
99
119
|
onOpen: (data: onOpenParameters) => void;
|
|
@@ -125,6 +145,7 @@ export class HocuspocusProvider extends EventEmitter {
|
|
|
125
145
|
token: null,
|
|
126
146
|
sessionAwareness: false,
|
|
127
147
|
forceSyncInterval: false,
|
|
148
|
+
flushDelay: false,
|
|
128
149
|
onAuthenticated: () => null,
|
|
129
150
|
onAuthenticationFailed: () => null,
|
|
130
151
|
onOpen: () => null,
|
|
@@ -155,6 +176,18 @@ export class HocuspocusProvider extends EventEmitter {
|
|
|
155
176
|
|
|
156
177
|
private _isAttached = false;
|
|
157
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Outgoing document updates buffered for the current `flushDelay` window.
|
|
181
|
+
*/
|
|
182
|
+
private pendingUpdates: Uint8Array[] = [];
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Awareness client ids whose latest state should be sent on the next flush.
|
|
186
|
+
*/
|
|
187
|
+
private pendingAwarenessClients = new Set<number>();
|
|
188
|
+
|
|
189
|
+
private flushTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
190
|
+
|
|
158
191
|
/**
|
|
159
192
|
* Unique session identifier for this provider instance.
|
|
160
193
|
* Used for multiplexing multiple providers with the same document name on a single WebSocket.
|
|
@@ -355,23 +388,103 @@ export class HocuspocusProvider extends EventEmitter {
|
|
|
355
388
|
});
|
|
356
389
|
}
|
|
357
390
|
|
|
391
|
+
private get batchingEnabled(): boolean {
|
|
392
|
+
return (
|
|
393
|
+
!!this.configuration.flushDelay &&
|
|
394
|
+
typeof this.configuration.flushDelay === "number"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
358
398
|
documentUpdateHandler(update: Uint8Array, origin: any) {
|
|
359
399
|
if (origin === this) {
|
|
360
400
|
return;
|
|
361
401
|
}
|
|
362
402
|
|
|
363
|
-
this.
|
|
364
|
-
|
|
403
|
+
if (!this.batchingEnabled) {
|
|
404
|
+
this.incrementUnsyncedChanges();
|
|
405
|
+
this.send(UpdateMessage, { update, documentName: this.effectiveName });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Count one outstanding change per batch: the server acks once per
|
|
410
|
+
// merged message, so incrementing per buffered update would never balance.
|
|
411
|
+
if (this.pendingUpdates.length === 0) {
|
|
412
|
+
this.incrementUnsyncedChanges();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.pendingUpdates.push(update);
|
|
416
|
+
this.scheduleFlush();
|
|
365
417
|
}
|
|
366
418
|
|
|
367
419
|
awarenessUpdateHandler({ added, updated, removed }: any, origin: any) {
|
|
420
|
+
if (origin === this) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
368
424
|
const changedClients = added.concat(updated).concat(removed);
|
|
369
425
|
|
|
370
|
-
this.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
426
|
+
if (!this.batchingEnabled) {
|
|
427
|
+
this.send(AwarenessMessage, {
|
|
428
|
+
awareness: this.awareness,
|
|
429
|
+
clients: changedClients,
|
|
430
|
+
documentName: this.effectiveName,
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const client of changedClients) {
|
|
436
|
+
this.pendingAwarenessClients.add(client);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.scheduleFlush();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private scheduleFlush() {
|
|
443
|
+
// Fixed-window batching: the first pending change starts the timer and
|
|
444
|
+
// everything until it fires is flushed together, so latency stays capped
|
|
445
|
+
// at `flushDelay` instead of growing while the user keeps typing.
|
|
446
|
+
if (this.flushTimeout !== null) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
this.flushTimeout = setTimeout(() => {
|
|
451
|
+
this.flushTimeout = null;
|
|
452
|
+
this.flushPendingUpdates();
|
|
453
|
+
}, this.configuration.flushDelay as number);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Send everything buffered for the current `flushDelay` window right away.
|
|
458
|
+
* Buffered document updates are merged into a single message and awareness
|
|
459
|
+
* collapses to the latest state of each changed client. Safe to call when
|
|
460
|
+
* nothing is pending.
|
|
461
|
+
*/
|
|
462
|
+
flushPendingUpdates() {
|
|
463
|
+
if (this.flushTimeout !== null) {
|
|
464
|
+
clearTimeout(this.flushTimeout);
|
|
465
|
+
this.flushTimeout = null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (this.pendingUpdates.length > 0) {
|
|
469
|
+
const update =
|
|
470
|
+
this.pendingUpdates.length === 1
|
|
471
|
+
? this.pendingUpdates[0]
|
|
472
|
+
: Y.mergeUpdates(this.pendingUpdates);
|
|
473
|
+
|
|
474
|
+
this.pendingUpdates = [];
|
|
475
|
+
this.send(UpdateMessage, { update, documentName: this.effectiveName });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (this.pendingAwarenessClients.size > 0) {
|
|
479
|
+
const clients = Array.from(this.pendingAwarenessClients);
|
|
480
|
+
|
|
481
|
+
this.pendingAwarenessClients.clear();
|
|
482
|
+
this.send(AwarenessMessage, {
|
|
483
|
+
awareness: this.awareness,
|
|
484
|
+
clients,
|
|
485
|
+
documentName: this.effectiveName,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
375
488
|
}
|
|
376
489
|
|
|
377
490
|
/**
|
|
@@ -482,6 +595,15 @@ export class HocuspocusProvider extends EventEmitter {
|
|
|
482
595
|
this.isAuthenticated = false;
|
|
483
596
|
this.synced = false;
|
|
484
597
|
|
|
598
|
+
// Drop anything buffered for batching; the reconnect sync handshake will
|
|
599
|
+
// reconcile these changes from the document via state vectors.
|
|
600
|
+
if (this.flushTimeout !== null) {
|
|
601
|
+
clearTimeout(this.flushTimeout);
|
|
602
|
+
this.flushTimeout = null;
|
|
603
|
+
}
|
|
604
|
+
this.pendingUpdates = [];
|
|
605
|
+
this.pendingAwarenessClients.clear();
|
|
606
|
+
|
|
485
607
|
// update awareness (all users except local left)
|
|
486
608
|
if (this.awareness) {
|
|
487
609
|
removeAwarenessStates(
|
|
@@ -511,6 +633,10 @@ export class HocuspocusProvider extends EventEmitter {
|
|
|
511
633
|
this.awareness.destroy();
|
|
512
634
|
}
|
|
513
635
|
|
|
636
|
+
// Send any buffered updates before the socket is torn down. The websocket
|
|
637
|
+
// safely queues the message if it is no longer open.
|
|
638
|
+
this.flushPendingUpdates();
|
|
639
|
+
|
|
514
640
|
this.document.off("update", this.boundDocumentUpdateHandler);
|
|
515
641
|
|
|
516
642
|
this.removeAllListeners();
|
package/src/OutgoingMessage.ts
CHANGED