@docukit/docsync 0.0.1-alpha.1 → 0.2.0-alpha.1
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/src/bindings/docnode.d.ts +3 -0
- package/dist/src/bindings/docnode.d.ts.map +1 -0
- package/dist/src/{shared/docBinding.js → bindings/docnode.js} +2 -5
- package/dist/src/bindings/docnode.js.map +1 -0
- package/dist/src/bindings/index.d.ts +3 -0
- package/dist/src/bindings/index.d.ts.map +1 -0
- package/dist/src/bindings/index.js +5 -0
- package/dist/src/bindings/index.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/deleteDoc.d.ts +4 -0
- package/dist/src/client/handlers/clientInitiated/deleteDoc.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/deleteDoc.js +6 -0
- package/dist/src/client/handlers/clientInitiated/deleteDoc.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/presence.d.ts +10 -0
- package/dist/src/client/handlers/clientInitiated/presence.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/presence.js +45 -0
- package/dist/src/client/handlers/clientInitiated/presence.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/buildSyncPayload.d.ts +20 -0
- package/dist/src/client/handlers/clientInitiated/sync/buildSyncPayload.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/buildSyncPayload.js +38 -0
- package/dist/src/client/handlers/clientInitiated/sync/buildSyncPayload.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/applyAndBroadcastServerOps.d.ts +12 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/applyAndBroadcastServerOps.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/applyAndBroadcastServerOps.js +39 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/applyAndBroadcastServerOps.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/handleSyncResponse.d.ts +11 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/handleSyncResponse.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/handleSyncResponse.js +37 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/handleSyncResponse.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistDocDeleted.d.ts +8 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistDocDeleted.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistDocDeleted.js +13 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistDocDeleted.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistSyncResult.d.ts +14 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistSyncResult.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistSyncResult.js +38 -0
- package/dist/src/client/handlers/clientInitiated/sync/handleSyncResponse/persistSyncResult.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/sync.d.ts +7 -0
- package/dist/src/client/handlers/clientInitiated/sync/sync.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync/sync.js +41 -0
- package/dist/src/client/handlers/clientInitiated/sync/sync.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync.d.ts +21 -0
- package/dist/src/client/handlers/clientInitiated/sync.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/sync.js +174 -0
- package/dist/src/client/handlers/clientInitiated/sync.js.map +1 -0
- package/dist/src/client/handlers/clientInitiated/unsubscribe.d.ts +4 -0
- package/dist/src/client/handlers/clientInitiated/unsubscribe.d.ts.map +1 -0
- package/dist/src/client/handlers/clientInitiated/unsubscribe.js +12 -0
- package/dist/src/client/handlers/clientInitiated/unsubscribe.js.map +1 -0
- package/dist/src/client/handlers/connection/connect.d.ts +5 -0
- package/dist/src/client/handlers/connection/connect.d.ts.map +1 -0
- package/dist/src/client/handlers/connection/connect.js +10 -0
- package/dist/src/client/handlers/connection/connect.js.map +1 -0
- package/dist/src/client/handlers/connection/disconnect.d.ts +5 -0
- package/dist/src/client/handlers/connection/disconnect.d.ts.map +1 -0
- package/dist/src/client/handlers/connection/disconnect.js +21 -0
- package/dist/src/client/handlers/connection/disconnect.js.map +1 -0
- package/dist/src/client/handlers/serverInitiated/dirty.d.ts +5 -0
- package/dist/src/client/handlers/serverInitiated/dirty.d.ts.map +1 -0
- package/dist/src/client/handlers/serverInitiated/dirty.js +7 -0
- package/dist/src/client/handlers/serverInitiated/dirty.js.map +1 -0
- package/dist/src/client/handlers/serverInitiated/presence.d.ts +6 -0
- package/dist/src/client/handlers/serverInitiated/presence.d.ts.map +1 -0
- package/dist/src/client/handlers/serverInitiated/presence.js +11 -0
- package/dist/src/client/handlers/serverInitiated/presence.js.map +1 -0
- package/dist/src/client/index.d.ts +20 -73
- package/dist/src/client/index.d.ts.map +1 -1
- package/dist/src/client/index.js +54 -522
- package/dist/src/client/index.js.map +1 -1
- package/dist/src/client/methods/deleteDoc.d.ts +5 -0
- package/dist/src/client/methods/deleteDoc.d.ts.map +1 -0
- package/dist/src/client/methods/deleteDoc.js +22 -0
- package/dist/src/client/methods/deleteDoc.js.map +1 -0
- package/dist/src/client/methods/getDoc/getDoc.d.ts +6 -0
- package/dist/src/client/methods/getDoc/getDoc.d.ts.map +1 -0
- package/dist/src/client/methods/getDoc/getDoc.js +107 -0
- package/dist/src/client/methods/getDoc/getDoc.js.map +1 -0
- package/dist/src/client/methods/getDoc/loadOrCreateDoc.d.ts +3 -0
- package/dist/src/client/methods/getDoc/loadOrCreateDoc.d.ts.map +1 -0
- package/dist/src/client/methods/getDoc/loadOrCreateDoc.js +42 -0
- package/dist/src/client/methods/getDoc/loadOrCreateDoc.js.map +1 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/onLocalOperations.d.ts +6 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/onLocalOperations.d.ts.map +1 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/onLocalOperations.js +36 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/onLocalOperations.js.map +1 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/setupChangeListener.d.ts +3 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/setupChangeListener.d.ts.map +1 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/setupChangeListener.js +30 -0
- package/dist/src/client/methods/getDoc/setupChangeListener/setupChangeListener.js.map +1 -0
- package/dist/src/client/methods/getDoc/unloadDoc.d.ts +3 -0
- package/dist/src/client/methods/getDoc/unloadDoc.d.ts.map +1 -0
- package/dist/src/client/methods/getDoc/unloadDoc.js +32 -0
- package/dist/src/client/methods/getDoc/unloadDoc.js.map +1 -0
- package/dist/src/client/methods/getPresence.d.ts +6 -0
- package/dist/src/client/methods/getPresence.d.ts.map +1 -0
- package/dist/src/client/methods/getPresence.js +23 -0
- package/dist/src/client/methods/getPresence.js.map +1 -0
- package/dist/src/client/providers/indexeddb.d.ts +3 -3
- package/dist/src/client/providers/indexeddb.d.ts.map +1 -1
- package/dist/src/client/providers/indexeddb.js.map +1 -1
- package/dist/src/client/types.d.ts +81 -0
- package/dist/src/client/types.d.ts.map +1 -0
- package/dist/src/client/types.js +2 -0
- package/dist/src/client/types.js.map +1 -0
- package/dist/src/client/utils/BCHelper.d.ts +21 -0
- package/dist/src/client/utils/BCHelper.d.ts.map +1 -0
- package/dist/src/client/utils/BCHelper.js +57 -0
- package/dist/src/client/utils/BCHelper.js.map +1 -0
- package/dist/src/client/utils/applyPresencePatch.d.ts +11 -0
- package/dist/src/client/utils/applyPresencePatch.d.ts.map +1 -0
- package/dist/src/client/utils/applyPresencePatch.js +21 -0
- package/dist/src/client/utils/applyPresencePatch.js.map +1 -0
- package/dist/src/client/utils/createSocket.d.ts +4 -0
- package/dist/src/client/utils/createSocket.d.ts.map +1 -0
- package/dist/src/client/utils/createSocket.js +29 -0
- package/dist/src/client/utils/createSocket.js.map +1 -0
- package/dist/src/client/utils/events.d.ts +46 -0
- package/dist/src/client/utils/events.d.ts.map +1 -0
- package/dist/src/client/utils/events.js +25 -0
- package/dist/src/client/utils/events.js.map +1 -0
- package/dist/src/client/utils/getDeviceId.d.ts +6 -0
- package/dist/src/client/utils/getDeviceId.d.ts.map +1 -0
- package/dist/src/client/utils/getDeviceId.js +14 -0
- package/dist/src/client/utils/getDeviceId.js.map +1 -0
- package/dist/src/client/utils/getOwnPresencePatch.d.ts +3 -0
- package/dist/src/client/utils/getOwnPresencePatch.d.ts.map +1 -0
- package/dist/src/client/utils/getOwnPresencePatch.js +10 -0
- package/dist/src/client/utils/getOwnPresencePatch.js.map +1 -0
- package/dist/src/client/utils/request.d.ts +10 -0
- package/dist/src/client/utils/request.d.ts.map +1 -0
- package/dist/src/client/utils/request.js +15 -0
- package/dist/src/client/utils/request.js.map +1 -0
- package/dist/src/exports/client.d.ts +4 -1
- package/dist/src/exports/client.d.ts.map +1 -1
- package/dist/src/exports/client.js +1 -0
- package/dist/src/exports/client.js.map +1 -1
- package/dist/src/exports/docnode.d.ts +1 -1
- package/dist/src/exports/docnode.d.ts.map +1 -1
- package/dist/src/exports/docnode.js +1 -1
- package/dist/src/exports/docnode.js.map +1 -1
- package/dist/src/exports/server.d.ts +1 -1
- package/dist/src/exports/server.d.ts.map +1 -1
- package/dist/src/exports/server.js.map +1 -1
- package/dist/src/exports/shared.d.ts +2 -0
- package/dist/src/exports/shared.d.ts.map +1 -0
- package/dist/src/exports/shared.js +2 -0
- package/dist/src/exports/shared.js.map +1 -0
- package/dist/src/server/cli.js +1 -1
- package/dist/src/server/cli.js.map +1 -1
- package/dist/src/server/handlers/connection/authenticationAndConnection.d.ts +7 -0
- package/dist/src/server/handlers/connection/authenticationAndConnection.d.ts.map +1 -0
- package/dist/src/server/handlers/connection/authenticationAndConnection.js +57 -0
- package/dist/src/server/handlers/connection/authenticationAndConnection.js.map +1 -0
- package/dist/src/server/handlers/connection/disconnect.d.ts +5 -0
- package/dist/src/server/handlers/connection/disconnect.d.ts.map +1 -0
- package/dist/src/server/handlers/connection/disconnect.js +25 -0
- package/dist/src/server/handlers/connection/disconnect.js.map +1 -0
- package/dist/src/server/handlers/deleteDoc.d.ts +11 -0
- package/dist/src/server/handlers/deleteDoc.d.ts.map +1 -0
- package/dist/src/server/handlers/deleteDoc.js +13 -0
- package/dist/src/server/handlers/deleteDoc.js.map +1 -0
- package/dist/src/server/handlers/disconnect.d.ts +10 -0
- package/dist/src/server/handlers/disconnect.d.ts.map +1 -0
- package/dist/src/server/handlers/disconnect.js +24 -0
- package/dist/src/server/handlers/disconnect.js.map +1 -0
- package/dist/src/server/handlers/presence.d.ts +12 -0
- package/dist/src/server/handlers/presence.d.ts.map +1 -0
- package/dist/src/server/handlers/presence.js +19 -0
- package/dist/src/server/handlers/presence.js.map +1 -0
- package/dist/src/server/handlers/sync/handleError.d.ts +13 -0
- package/dist/src/server/handlers/sync/handleError.d.ts.map +1 -0
- package/dist/src/server/handlers/sync/handleError.js +23 -0
- package/dist/src/server/handlers/sync/handleError.js.map +1 -0
- package/dist/src/server/handlers/sync/handleSync.d.ts +10 -0
- package/dist/src/server/handlers/sync/handleSync.d.ts.map +1 -0
- package/dist/src/server/handlers/sync/handleSync.js +68 -0
- package/dist/src/server/handlers/sync/handleSync.js.map +1 -0
- package/dist/src/server/handlers/sync/notifyClients.d.ts +12 -0
- package/dist/src/server/handlers/sync/notifyClients.d.ts.map +1 -0
- package/dist/src/server/handlers/sync/notifyClients.js +27 -0
- package/dist/src/server/handlers/sync/notifyClients.js.map +1 -0
- package/dist/src/server/handlers/sync/runSyncTransaction.d.ts +16 -0
- package/dist/src/server/handlers/sync/runSyncTransaction.d.ts.map +1 -0
- package/dist/src/server/handlers/sync/runSyncTransaction.js +43 -0
- package/dist/src/server/handlers/sync/runSyncTransaction.js.map +1 -0
- package/dist/src/server/handlers/sync/squashIfNeeded.d.ts +9 -0
- package/dist/src/server/handlers/sync/squashIfNeeded.d.ts.map +1 -0
- package/dist/src/server/handlers/sync/squashIfNeeded.js +32 -0
- package/dist/src/server/handlers/sync/squashIfNeeded.js.map +1 -0
- package/dist/src/server/handlers/sync/subscribeToRoom.d.ts +8 -0
- package/dist/src/server/handlers/sync/subscribeToRoom.d.ts.map +1 -0
- package/dist/src/server/handlers/sync/subscribeToRoom.js +21 -0
- package/dist/src/server/handlers/sync/subscribeToRoom.js.map +1 -0
- package/dist/src/server/handlers/sync.d.ts +13 -0
- package/dist/src/server/handlers/sync.d.ts.map +1 -0
- package/dist/src/server/handlers/sync.js +161 -0
- package/dist/src/server/handlers/sync.js.map +1 -0
- package/dist/src/server/handlers/unsubscribe.d.ts +10 -0
- package/dist/src/server/handlers/unsubscribe.d.ts.map +1 -0
- package/dist/src/server/handlers/unsubscribe.js +21 -0
- package/dist/src/server/handlers/unsubscribe.js.map +1 -0
- package/dist/src/server/index.d.ts +11 -11
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +37 -337
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/server/providers/memory.d.ts +3 -3
- package/dist/src/server/providers/memory.d.ts.map +1 -1
- package/dist/src/server/providers/memory.js +5 -0
- package/dist/src/server/providers/memory.js.map +1 -1
- package/dist/src/server/providers/postgres/drizzle.config.d.ts.map +1 -1
- package/dist/src/server/providers/postgres/drizzle.config.js +2 -1
- package/dist/src/server/providers/postgres/drizzle.config.js.map +1 -1
- package/dist/src/server/providers/postgres/index.d.ts +4 -3
- package/dist/src/server/providers/postgres/index.d.ts.map +1 -1
- package/dist/src/server/providers/postgres/index.js +7 -9
- package/dist/src/server/providers/postgres/index.js.map +1 -1
- package/dist/src/server/providers/postgres/schema.d.ts +1 -0
- package/dist/src/server/providers/postgres/schema.d.ts.map +1 -1
- package/dist/src/server/providers/postgres/schema.js +18 -1
- package/dist/src/server/providers/postgres/schema.js.map +1 -1
- package/dist/src/server/types.d.ts +107 -0
- package/dist/src/server/types.d.ts.map +1 -0
- package/dist/src/server/types.js +2 -0
- package/dist/src/server/types.js.map +1 -0
- package/dist/src/server/utils/applyPresenceUpdate.d.ts +11 -0
- package/dist/src/server/utils/applyPresenceUpdate.d.ts.map +1 -0
- package/dist/src/server/utils/applyPresenceUpdate.js +25 -0
- package/dist/src/server/utils/applyPresenceUpdate.js.map +1 -0
- package/dist/src/server/utils/authorizeMiddleware.d.ts +4 -0
- package/dist/src/server/utils/authorizeMiddleware.d.ts.map +1 -0
- package/dist/src/server/utils/authorizeMiddleware.js +73 -0
- package/dist/src/server/utils/authorizeMiddleware.js.map +1 -0
- package/dist/src/server/utils/events.d.ts +16 -0
- package/dist/src/server/utils/events.d.ts.map +1 -0
- package/dist/src/server/utils/events.js +22 -0
- package/dist/src/server/utils/events.js.map +1 -0
- package/dist/src/server/utils/rateLimitMiddleware.d.ts +6 -0
- package/dist/src/server/utils/rateLimitMiddleware.d.ts.map +1 -0
- package/dist/src/server/utils/rateLimitMiddleware.js +65 -0
- package/dist/src/server/utils/rateLimitMiddleware.js.map +1 -0
- package/dist/src/shared/types.d.ts +52 -338
- package/dist/src/shared/types.d.ts.map +1 -1
- package/dist/src/shared/types.js +0 -4
- package/dist/src/shared/types.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -5
- package/dist/src/exports/index.d.ts +0 -3
- package/dist/src/exports/index.d.ts.map +0 -1
- package/dist/src/exports/index.js +0 -3
- package/dist/src/exports/index.js.map +0 -1
- package/dist/src/exports/testing.d.ts +0 -7
- package/dist/src/exports/testing.d.ts.map +0 -1
- package/dist/src/exports/testing.js +0 -6
- package/dist/src/exports/testing.js.map +0 -1
- package/dist/src/shared/debounce.d.ts +0 -2
- package/dist/src/shared/debounce.d.ts.map +0 -1
- package/dist/src/shared/debounce.js +0 -10
- package/dist/src/shared/debounce.js.map +0 -1
- package/dist/src/shared/docBinding.d.ts +0 -17
- package/dist/src/shared/docBinding.d.ts.map +0 -1
- package/dist/src/shared/docBinding.js.map +0 -1
- package/dist/src/shared/throttle.d.ts +0 -30
- package/dist/src/shared/throttle.d.ts.map +0 -1
- package/dist/src/shared/throttle.js +0 -51
- package/dist/src/shared/throttle.js.map +0 -1
- package/dist/src/shared/utils.d.ts +0 -2
- package/dist/src/shared/utils.d.ts.map +0 -1
- package/dist/src/shared/utils.js +0 -11
- package/dist/src/shared/utils.js.map +0 -1
package/dist/src/client/index.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
|
2
2
|
import { io } from "socket.io-client";
|
|
3
|
+
import { createClientEventEmitter } from "./utils/events.js";
|
|
4
|
+
import { handleConnect } from "./handlers/connection/connect.js";
|
|
5
|
+
import { handleDeleteDoc } from "./handlers/clientInitiated/deleteDoc.js";
|
|
6
|
+
import { handleDisconnect } from "./handlers/connection/disconnect.js";
|
|
7
|
+
import { handleDirty } from "./handlers/serverInitiated/dirty.js";
|
|
8
|
+
import { handlePresence } from "./handlers/clientInitiated/presence.js";
|
|
9
|
+
import { handlePresence as handleServerPresence } from "./handlers/serverInitiated/presence.js";
|
|
10
|
+
import { handleSync } from "./handlers/clientInitiated/sync.js";
|
|
11
|
+
import { handleUnsubscribe } from "./handlers/clientInitiated/unsubscribe.js";
|
|
12
|
+
import { BCHelper } from "./utils/BCHelper.js";
|
|
13
|
+
import { getDeviceId } from "./utils/getDeviceId.js";
|
|
14
|
+
import { getOwnPresencePatch } from "./utils/getOwnPresencePatch.js";
|
|
3
15
|
export class DocSyncClient {
|
|
4
16
|
_docBinding;
|
|
5
17
|
_docsCache = new Map();
|
|
@@ -8,7 +20,7 @@ export class DocSyncClient {
|
|
|
8
20
|
/** Client-generated id for presence (works offline; sent in auth so server uses same key) */
|
|
9
21
|
_clientId;
|
|
10
22
|
_shouldBroadcast = true;
|
|
11
|
-
|
|
23
|
+
_bcHelper;
|
|
12
24
|
_socket;
|
|
13
25
|
// Flow control state (batching, debouncing, push queueing)
|
|
14
26
|
_localOpsBatchState = new Map();
|
|
@@ -16,13 +28,8 @@ export class DocSyncClient {
|
|
|
16
28
|
_presenceDebounceState = new Map();
|
|
17
29
|
_presenceDebounce = 200;
|
|
18
30
|
_pushStatusByDocId = new Map();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
_disconnectHandlers = new Set();
|
|
22
|
-
_changeHandlers = new Set();
|
|
23
|
-
_syncHandlers = new Set();
|
|
24
|
-
_docLoadHandlers = new Set();
|
|
25
|
-
_docUnloadHandlers = new Set();
|
|
31
|
+
/** Typed as unknown so DocSyncClient remains covariant in O, S (assignable to DocSyncClient base). */
|
|
32
|
+
_events = createClientEventEmitter();
|
|
26
33
|
constructor(config) {
|
|
27
34
|
if (typeof window === "undefined")
|
|
28
35
|
throw new Error("DocSyncClient can only be used in the browser");
|
|
@@ -33,88 +40,23 @@ export class DocSyncClient {
|
|
|
33
40
|
this._localPromise = (async () => {
|
|
34
41
|
const identity = await local.getIdentity();
|
|
35
42
|
const provider = new local.provider(identity);
|
|
36
|
-
|
|
37
|
-
// This ensures only tabs of the same user share operations
|
|
38
|
-
this._broadcastChannel = new BroadcastChannel(`docsync:${identity.userId}`);
|
|
39
|
-
this._broadcastChannel.onmessage = async (ev) => {
|
|
40
|
-
// RECEIVED MESSAGES
|
|
41
|
-
if (ev.data.type === "OPERATIONS") {
|
|
42
|
-
// Another tab is pushing operations - they are responsible for pushing to server
|
|
43
|
-
// We just need to coordinate push status to avoid conflicts
|
|
44
|
-
const currentStatus = this._pushStatusByDocId.get(ev.data.docId) ?? "idle";
|
|
45
|
-
if (currentStatus === "pushing") {
|
|
46
|
-
// Mark as busy to avoid concurrent pushes
|
|
47
|
-
this._pushStatusByDocId.set(ev.data.docId, "pushing-with-pending");
|
|
48
|
-
}
|
|
49
|
-
// Note: We don't call saveRemote here - the sender is responsible for pushing
|
|
50
|
-
// If the sender is offline, the push will happen when they reconnect
|
|
51
|
-
void this._applyOperations(ev.data.operations, ev.data.docId);
|
|
52
|
-
// Apply presence after ops so the doc is updated first (avoids cursor lag)
|
|
53
|
-
if (ev.data.presence) {
|
|
54
|
-
const cacheEntry = this._docsCache.get(ev.data.docId);
|
|
55
|
-
if (cacheEntry)
|
|
56
|
-
this._applyPresencePatch(cacheEntry, ev.data.presence);
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (ev.data.type === "PRESENCE") {
|
|
61
|
-
const { docId, presence } = ev.data;
|
|
62
|
-
const cacheEntry = this._docsCache.get(docId);
|
|
63
|
-
if (!cacheEntry)
|
|
64
|
-
return;
|
|
65
|
-
this._applyPresencePatch(cacheEntry, presence);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
43
|
+
this._bcHelper = new BCHelper(this, identity.userId);
|
|
68
44
|
return { provider, identity };
|
|
69
45
|
})();
|
|
70
46
|
this._deviceId = getDeviceId();
|
|
71
47
|
this._socket = io(config.server.url, {
|
|
72
48
|
auth: (cb) => {
|
|
73
|
-
void config.server.auth.getToken().then((token) => {
|
|
49
|
+
void Promise.resolve(config.server.auth.getToken()).then((token) => {
|
|
74
50
|
cb({ token, deviceId: this._deviceId, clientId: this._clientId });
|
|
75
51
|
});
|
|
76
52
|
},
|
|
77
53
|
// Performance optimizations for testing
|
|
78
54
|
transports: ["websocket"], // Skip polling, go straight to WebSocket
|
|
79
55
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const docId of this._docsCache.keys()) {
|
|
85
|
-
this.saveRemote({ docId });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
this._socket.on("disconnect", (reason) => {
|
|
89
|
-
this._pushStatusByDocId.clear();
|
|
90
|
-
// Clear pending presence debounce timers so their callbacks never run after disconnect
|
|
91
|
-
for (const state of this._presenceDebounceState.values()) {
|
|
92
|
-
clearTimeout(state.timeout);
|
|
93
|
-
}
|
|
94
|
-
this._presenceDebounceState.clear();
|
|
95
|
-
// Tell other tabs to remove this client's presence (clientId works offline)
|
|
96
|
-
for (const docId of this._docsCache.keys()) {
|
|
97
|
-
this._sendMessage({
|
|
98
|
-
type: "PRESENCE",
|
|
99
|
-
docId,
|
|
100
|
-
presence: { [this._clientId]: null },
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
this._emit(this._disconnectHandlers, { reason });
|
|
104
|
-
});
|
|
105
|
-
this._socket.on("connect_error", (err) => {
|
|
106
|
-
this._emit(this._disconnectHandlers, { reason: err.message });
|
|
107
|
-
});
|
|
108
|
-
// Listen for dirty notifications from server
|
|
109
|
-
this._socket.on("dirty", (payload) => {
|
|
110
|
-
this.saveRemote({ docId: payload.docId });
|
|
111
|
-
});
|
|
112
|
-
this._socket.on("presence", (payload) => {
|
|
113
|
-
const cacheEntry = this._docsCache.get(payload.docId);
|
|
114
|
-
if (!cacheEntry)
|
|
115
|
-
return;
|
|
116
|
-
this._applyPresencePatch(cacheEntry, payload.presence);
|
|
117
|
-
});
|
|
56
|
+
handleConnect({ client: this });
|
|
57
|
+
handleDisconnect({ client: this });
|
|
58
|
+
handleDirty({ client: this });
|
|
59
|
+
handleServerPresence({ client: this });
|
|
118
60
|
}
|
|
119
61
|
connect() {
|
|
120
62
|
this._socket.connect();
|
|
@@ -122,88 +64,6 @@ export class DocSyncClient {
|
|
|
122
64
|
disconnect() {
|
|
123
65
|
this._socket.disconnect();
|
|
124
66
|
}
|
|
125
|
-
async _applyOperations(operations, docId) {
|
|
126
|
-
const docFromCache = this._docsCache.get(docId);
|
|
127
|
-
if (!docFromCache)
|
|
128
|
-
return;
|
|
129
|
-
const doc = await docFromCache.promisedDoc;
|
|
130
|
-
if (!doc)
|
|
131
|
-
return;
|
|
132
|
-
this._shouldBroadcast = false;
|
|
133
|
-
this._docBinding.applyOperations(doc, operations);
|
|
134
|
-
this._shouldBroadcast = true;
|
|
135
|
-
// Emit change event for broadcast operations
|
|
136
|
-
this._emit(this._changeHandlers, {
|
|
137
|
-
docId,
|
|
138
|
-
origin: "broadcast",
|
|
139
|
-
operations: [operations],
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
_applyPresencePatch(cacheEntry, patch) {
|
|
143
|
-
const newPresence = { ...cacheEntry.presence };
|
|
144
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
145
|
-
if (key === this._clientId)
|
|
146
|
-
continue; // never store own presence in cache; local tab must not render self as remote
|
|
147
|
-
if (value === undefined || value === null) {
|
|
148
|
-
delete newPresence[key];
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
newPresence[key] = value;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
cacheEntry.presence = newPresence;
|
|
155
|
-
cacheEntry.presenceHandlers.forEach((handler) => handler(cacheEntry.presence));
|
|
156
|
-
}
|
|
157
|
-
/** Current presence for this client (debounce state or cache); does not clear the timer */
|
|
158
|
-
_getOwnPresencePatch(docId) {
|
|
159
|
-
const debounced = this._presenceDebounceState.get(docId);
|
|
160
|
-
if (debounced)
|
|
161
|
-
return { [this._clientId]: debounced.data };
|
|
162
|
-
const cacheEntry = this._docsCache.get(docId);
|
|
163
|
-
if (cacheEntry?.presence[this._clientId] !== undefined)
|
|
164
|
-
return { [this._clientId]: cacheEntry.presence[this._clientId] };
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
// TODO: used when server responds with a new doc (squashing)
|
|
168
|
-
async _replaceDocInCache({ docId, doc, serializedDoc, }) {
|
|
169
|
-
const cacheEntry = this._docsCache.get(docId);
|
|
170
|
-
if (!cacheEntry)
|
|
171
|
-
return;
|
|
172
|
-
// Deserialize if needed
|
|
173
|
-
const newDoc = doc ?? this._docBinding.deserialize(serializedDoc);
|
|
174
|
-
// Replace the cached document with the new one
|
|
175
|
-
// Keep the same refCount
|
|
176
|
-
// Note: We don't setup a new change listener here because:
|
|
177
|
-
// 1. The doc already has all operations applied from the sync
|
|
178
|
-
// 2. A listener will be setup when the doc is loaded via getDoc
|
|
179
|
-
// 3. Multiple listeners would cause operations to be applied multiple times
|
|
180
|
-
this._docsCache.set(docId, {
|
|
181
|
-
promisedDoc: Promise.resolve(newDoc),
|
|
182
|
-
refCount: cacheEntry.refCount,
|
|
183
|
-
presence: cacheEntry.presence,
|
|
184
|
-
presenceHandlers: cacheEntry.presenceHandlers,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
async _applyServerOperations({ docId, operations, }) {
|
|
188
|
-
const cacheEntry = this._docsCache.get(docId);
|
|
189
|
-
if (!cacheEntry)
|
|
190
|
-
return;
|
|
191
|
-
// Get the cached document and apply server operations to it
|
|
192
|
-
const doc = await cacheEntry.promisedDoc;
|
|
193
|
-
if (!doc)
|
|
194
|
-
return;
|
|
195
|
-
this._shouldBroadcast = false;
|
|
196
|
-
for (const op of operations) {
|
|
197
|
-
this._docBinding.applyOperations(doc, op);
|
|
198
|
-
}
|
|
199
|
-
this._shouldBroadcast = true;
|
|
200
|
-
// Emit change event for remote operations
|
|
201
|
-
this._emit(this._changeHandlers, {
|
|
202
|
-
docId,
|
|
203
|
-
origin: "remote",
|
|
204
|
-
operations,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
67
|
/**
|
|
208
68
|
* Subscribe to a document with reactive state updates.
|
|
209
69
|
*
|
|
@@ -248,13 +108,13 @@ export class DocSyncClient {
|
|
|
248
108
|
this._docsCache.set(createdDocId, {
|
|
249
109
|
promisedDoc: Promise.resolve(doc),
|
|
250
110
|
refCount: 1,
|
|
111
|
+
type,
|
|
251
112
|
presence: {},
|
|
252
|
-
|
|
113
|
+
presenceListeners: new Set(),
|
|
253
114
|
});
|
|
254
115
|
this._setupChangeListener(doc, createdDocId);
|
|
255
116
|
emit({ status: "success", data: { doc, docId: createdDocId } });
|
|
256
|
-
|
|
257
|
-
this._emit(this._docLoadHandlers, {
|
|
117
|
+
this._events.emit("docLoad", {
|
|
258
118
|
docId: createdDocId,
|
|
259
119
|
source: "created",
|
|
260
120
|
refCount: 1,
|
|
@@ -269,7 +129,7 @@ export class DocSyncClient {
|
|
|
269
129
|
clock: 0,
|
|
270
130
|
}));
|
|
271
131
|
})();
|
|
272
|
-
// We don't trigger
|
|
132
|
+
// We don't trigger an initial sync here because argId is undefined;
|
|
273
133
|
// so this is truly a new doc. Initial operations will be pushed to server
|
|
274
134
|
return () => void this._unloadDoc(createdDocId);
|
|
275
135
|
}
|
|
@@ -289,8 +149,9 @@ export class DocSyncClient {
|
|
|
289
149
|
this._docsCache.set(docId, {
|
|
290
150
|
promisedDoc,
|
|
291
151
|
refCount: 1,
|
|
152
|
+
type,
|
|
292
153
|
presence: {},
|
|
293
|
-
|
|
154
|
+
presenceListeners: new Set(),
|
|
294
155
|
});
|
|
295
156
|
}
|
|
296
157
|
void (async () => {
|
|
@@ -310,22 +171,14 @@ export class DocSyncClient {
|
|
|
310
171
|
source = createIfMissing ? "created" : "local";
|
|
311
172
|
}
|
|
312
173
|
}
|
|
313
|
-
// Emit doc load event
|
|
314
174
|
if (doc) {
|
|
315
175
|
const refCount = this._docsCache.get(docId)?.refCount ?? 1;
|
|
316
|
-
this.
|
|
317
|
-
docId,
|
|
318
|
-
source,
|
|
319
|
-
refCount,
|
|
320
|
-
});
|
|
176
|
+
this._events.emit("docLoad", { docId, source, refCount });
|
|
321
177
|
}
|
|
322
|
-
emit({
|
|
323
|
-
status: "success",
|
|
324
|
-
data: doc ? { doc, docId } : undefined,
|
|
325
|
-
});
|
|
178
|
+
emit({ status: "success", data: doc ? { doc, docId } : undefined });
|
|
326
179
|
// Fetch from server to check if document exists there
|
|
327
180
|
if (doc) {
|
|
328
|
-
void this
|
|
181
|
+
void handleSync(this, docId);
|
|
329
182
|
}
|
|
330
183
|
}
|
|
331
184
|
catch (e) {
|
|
@@ -341,7 +194,7 @@ export class DocSyncClient {
|
|
|
341
194
|
}
|
|
342
195
|
/**
|
|
343
196
|
* Subscribe to presence updates for a document.
|
|
344
|
-
* Multiple
|
|
197
|
+
* Multiple listeners can be registered for the same document.
|
|
345
198
|
* @param args - The arguments for the getPresence request.
|
|
346
199
|
* @param onChange - The callback to invoke when the presence changes.
|
|
347
200
|
* @returns A function to unsubscribe from presence updates.
|
|
@@ -354,64 +207,28 @@ export class DocSyncClient {
|
|
|
354
207
|
if (!cacheEntry) {
|
|
355
208
|
throw new Error(`Cannot subscribe to presence for document "${docId}" - document not loaded.`);
|
|
356
209
|
}
|
|
357
|
-
// Add
|
|
358
|
-
cacheEntry.
|
|
210
|
+
// Add listener to the set
|
|
211
|
+
cacheEntry.presenceListeners.add(onChange);
|
|
359
212
|
// Immediately call with current presence if available
|
|
360
213
|
if (Object.keys(cacheEntry.presence).length > 0) {
|
|
361
214
|
onChange(cacheEntry.presence);
|
|
362
215
|
}
|
|
363
|
-
// Return unsubscribe function that removes only this
|
|
216
|
+
// Return unsubscribe function that removes only this listener
|
|
364
217
|
return () => {
|
|
365
218
|
const entry = this._docsCache.get(docId);
|
|
366
219
|
if (entry) {
|
|
367
|
-
entry.
|
|
220
|
+
entry.presenceListeners.delete(onChange);
|
|
368
221
|
}
|
|
369
222
|
};
|
|
370
223
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (!cacheEntry)
|
|
374
|
-
throw new Error(`Doc ${docId} is not loaded, cannot set presence`);
|
|
375
|
-
// Clear existing timeout if any
|
|
376
|
-
const existingState = this._presenceDebounceState.get(docId);
|
|
377
|
-
clearTimeout(existingState?.timeout);
|
|
378
|
-
// Debounce the presence update
|
|
379
|
-
const timeout = setTimeout(() => {
|
|
380
|
-
const state = this._presenceDebounceState.get(docId);
|
|
381
|
-
if (!state)
|
|
382
|
-
return;
|
|
383
|
-
this._presenceDebounceState.delete(docId);
|
|
384
|
-
const patch = { [this._clientId]: state.data };
|
|
385
|
-
// Update local cache and notify handlers (so own cursor shows and UI stays in sync)
|
|
386
|
-
this._applyPresencePatch(cacheEntry, patch);
|
|
387
|
-
// Same device: broadcast to other tabs (works offline)
|
|
388
|
-
this._sendMessage({
|
|
389
|
-
type: "PRESENCE",
|
|
390
|
-
docId,
|
|
391
|
-
presence: patch,
|
|
392
|
-
});
|
|
393
|
-
// Other devices: send via WebSocket only when connected
|
|
394
|
-
if (this._socket.connected) {
|
|
395
|
-
void (async () => {
|
|
396
|
-
if (!this._socket.connected)
|
|
397
|
-
return;
|
|
398
|
-
const { error } = await this._request("presence", {
|
|
399
|
-
docId,
|
|
400
|
-
presence: state.data,
|
|
401
|
-
});
|
|
402
|
-
if (error) {
|
|
403
|
-
console.error(`Error setting presence for doc ${docId}:`, error);
|
|
404
|
-
}
|
|
405
|
-
})();
|
|
406
|
-
}
|
|
407
|
-
}, this._presenceDebounce);
|
|
408
|
-
this._presenceDebounceState.set(docId, { timeout, data: presence });
|
|
224
|
+
setPresence({ docId, presence }) {
|
|
225
|
+
void handlePresence(this, { docId, presence });
|
|
409
226
|
}
|
|
410
227
|
_setupChangeListener(doc, docId) {
|
|
411
228
|
this._docBinding.onChange(doc, ({ operations }) => {
|
|
412
229
|
if (this._shouldBroadcast) {
|
|
413
230
|
void this.onLocalOperations({ docId, operations: [operations] });
|
|
414
|
-
this.
|
|
231
|
+
this._events.emit("change", {
|
|
415
232
|
docId,
|
|
416
233
|
origin: "local",
|
|
417
234
|
operations: [operations],
|
|
@@ -420,8 +237,8 @@ export class DocSyncClient {
|
|
|
420
237
|
// include is the new cursor. Two frames so setPresence (from selection change) has run.
|
|
421
238
|
requestAnimationFrame(() => {
|
|
422
239
|
requestAnimationFrame(() => {
|
|
423
|
-
const presencePatch = this
|
|
424
|
-
this.
|
|
240
|
+
const presencePatch = getOwnPresencePatch(this, docId);
|
|
241
|
+
this._bcHelper?.broadcast({
|
|
425
242
|
type: "OPERATIONS",
|
|
426
243
|
operations,
|
|
427
244
|
docId,
|
|
@@ -480,34 +297,23 @@ export class DocSyncClient {
|
|
|
480
297
|
return;
|
|
481
298
|
if (cacheEntry.refCount > 1) {
|
|
482
299
|
cacheEntry.refCount -= 1;
|
|
483
|
-
this.
|
|
484
|
-
docId,
|
|
485
|
-
refCount: cacheEntry.refCount,
|
|
486
|
-
});
|
|
300
|
+
this._events.emit("docUnload", { docId, refCount: cacheEntry.refCount });
|
|
487
301
|
}
|
|
488
302
|
else {
|
|
489
|
-
// Mark refCount as 0 but keep in cache until promise resolves
|
|
490
303
|
cacheEntry.refCount = 0;
|
|
491
|
-
|
|
492
|
-
this._emit(this._docUnloadHandlers, {
|
|
493
|
-
docId,
|
|
494
|
-
refCount: 0,
|
|
495
|
-
});
|
|
304
|
+
this._events.emit("docUnload", { docId, refCount: 0 });
|
|
496
305
|
// Dispose when promise resolves
|
|
497
306
|
const doc = await cacheEntry.promisedDoc;
|
|
498
307
|
const currentEntry = this._docsCache.get(docId);
|
|
499
308
|
if (currentEntry?.refCount === 0) {
|
|
500
309
|
this._docsCache.delete(docId);
|
|
501
310
|
if (doc) {
|
|
502
|
-
await this.
|
|
311
|
+
await handleUnsubscribe(this._socket, { docId });
|
|
503
312
|
this._docBinding.dispose(doc);
|
|
504
313
|
}
|
|
505
314
|
}
|
|
506
315
|
}
|
|
507
316
|
}
|
|
508
|
-
_sendMessage(message) {
|
|
509
|
-
this._broadcastChannel?.postMessage(message);
|
|
510
|
-
}
|
|
511
317
|
onLocalOperations({ docId, operations }) {
|
|
512
318
|
// Get or create the batch state for this document
|
|
513
319
|
let state = this._localOpsBatchState.get(docId);
|
|
@@ -535,298 +341,24 @@ export class DocSyncClient {
|
|
|
535
341
|
if (opsToSave && opsToSave.length > 0) {
|
|
536
342
|
const local = await this._localPromise;
|
|
537
343
|
await local?.provider.transaction("readwrite", (ctx) => ctx.saveOperations({ docId, operations: opsToSave }));
|
|
538
|
-
this
|
|
344
|
+
void handleSync(this, docId);
|
|
539
345
|
}
|
|
540
346
|
})();
|
|
541
347
|
}, this._batchDelay);
|
|
542
348
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
* Uses a per-docId queue to prevent concurrent pushes for the same doc.
|
|
546
|
-
*/
|
|
547
|
-
saveRemote({ docId }) {
|
|
548
|
-
const status = this._pushStatusByDocId.get(docId) ?? "idle";
|
|
549
|
-
if (status !== "idle") {
|
|
550
|
-
this._pushStatusByDocId.set(docId, "pushing-with-pending");
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
void this._doPush({ docId });
|
|
349
|
+
async _deleteDoc(docId) {
|
|
350
|
+
return handleDeleteDoc(this._socket, { docId });
|
|
554
351
|
}
|
|
555
352
|
/**
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return;
|
|
563
|
-
try {
|
|
564
|
-
await this._request("unsubscribe-doc", { docId });
|
|
565
|
-
}
|
|
566
|
-
catch {
|
|
567
|
-
// Silently ignore errors during cleanup (e.g., socket
|
|
568
|
-
// disconnected during request, timeout, or server error)
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
async _doPush({ docId }) {
|
|
572
|
-
this._pushStatusByDocId.set(docId, "pushing");
|
|
573
|
-
const provider = (await this._localPromise).provider;
|
|
574
|
-
// Get the current clock value and operations from provider
|
|
575
|
-
const [operationsBatches, stored] = await provider.transaction("readonly", async (ctx) => {
|
|
576
|
-
return Promise.all([
|
|
577
|
-
ctx.getOperations({ docId }),
|
|
578
|
-
ctx.getSerializedDoc(docId),
|
|
579
|
-
]);
|
|
580
|
-
});
|
|
581
|
-
const operations = operationsBatches.flat();
|
|
582
|
-
const clientClock = stored?.clock ?? 0;
|
|
583
|
-
let response;
|
|
584
|
-
try {
|
|
585
|
-
const presenceState = this._presenceDebounceState.get(docId);
|
|
586
|
-
if (presenceState) {
|
|
587
|
-
clearTimeout(presenceState.timeout);
|
|
588
|
-
this._presenceDebounceState.delete(docId);
|
|
589
|
-
this._sendMessage({
|
|
590
|
-
type: "PRESENCE",
|
|
591
|
-
docId,
|
|
592
|
-
presence: { [this._clientId]: presenceState.data },
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
response = await this._request("sync-operations", {
|
|
596
|
-
clock: clientClock,
|
|
597
|
-
docId,
|
|
598
|
-
operations,
|
|
599
|
-
...(presenceState ? { presence: presenceState.data } : {}),
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
// Emit sync event (network error)
|
|
604
|
-
this._emit(this._syncHandlers, {
|
|
605
|
-
req: {
|
|
606
|
-
docId,
|
|
607
|
-
operations,
|
|
608
|
-
clock: clientClock,
|
|
609
|
-
},
|
|
610
|
-
error: {
|
|
611
|
-
type: "NetworkError",
|
|
612
|
-
message: error instanceof Error ? error.message : String(error),
|
|
613
|
-
},
|
|
614
|
-
});
|
|
615
|
-
// Retry on failure
|
|
616
|
-
this._pushStatusByDocId.set(docId, "idle");
|
|
617
|
-
void this._doPush({ docId });
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
// Check if server returned an error
|
|
621
|
-
if ("error" in response && response.error) {
|
|
622
|
-
// Emit sync event with server error
|
|
623
|
-
this._emit(this._syncHandlers, {
|
|
624
|
-
req: {
|
|
625
|
-
docId,
|
|
626
|
-
operations,
|
|
627
|
-
clock: clientClock,
|
|
628
|
-
},
|
|
629
|
-
error: response.error,
|
|
630
|
-
});
|
|
631
|
-
// Retry on error
|
|
632
|
-
this._pushStatusByDocId.set(docId, "idle");
|
|
633
|
-
void this._doPush({ docId });
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
// At this point, response must have data
|
|
637
|
-
const { data } = response;
|
|
638
|
-
// Emit sync event (success)
|
|
639
|
-
this._emit(this._syncHandlers, {
|
|
640
|
-
req: {
|
|
641
|
-
docId,
|
|
642
|
-
operations,
|
|
643
|
-
clock: clientClock,
|
|
644
|
-
},
|
|
645
|
-
data: {
|
|
646
|
-
...(data.operations ? { operations: data.operations } : {}),
|
|
647
|
-
...(data.serializedDoc ? { serializedDoc: data.serializedDoc } : {}),
|
|
648
|
-
clock: data.clock,
|
|
649
|
-
},
|
|
650
|
-
});
|
|
651
|
-
// Atomically: delete synced operations + consolidate into serialized doc
|
|
652
|
-
let didConsolidate = false; // Track if we actually saved new operations to IDB
|
|
653
|
-
await provider.transaction("readwrite", async (ctx) => {
|
|
654
|
-
// Delete client operations that were synced (delete batches, not individual ops)
|
|
655
|
-
if (operationsBatches.length > 0) {
|
|
656
|
-
await ctx.deleteOperations({
|
|
657
|
-
docId,
|
|
658
|
-
count: operationsBatches.length,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
// Consolidate operations into serialized doc
|
|
662
|
-
const stored = await ctx.getSerializedDoc(docId);
|
|
663
|
-
if (!stored)
|
|
664
|
-
return;
|
|
665
|
-
// Skip consolidation if another client (same IDB) already updated to this clock
|
|
666
|
-
// This handles the case where another tab/client already wrote this update
|
|
667
|
-
if (stored.clock >= data.clock) {
|
|
668
|
-
didConsolidate = false;
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
// Collect all operations to apply: server ops first, then client ops
|
|
672
|
-
const serverOps = data.operations ?? [];
|
|
673
|
-
const allOps = [...serverOps, ...operations];
|
|
674
|
-
// Only proceed if there are operations to apply
|
|
675
|
-
if (allOps.length > 0) {
|
|
676
|
-
const doc = this._docBinding.deserialize(stored.serializedDoc);
|
|
677
|
-
// Apply all operations in order (server ops first, then client ops)
|
|
678
|
-
for (const op of allOps) {
|
|
679
|
-
this._docBinding.applyOperations(doc, op);
|
|
680
|
-
}
|
|
681
|
-
const serializedDoc = this._docBinding.serialize(doc);
|
|
682
|
-
// Before saving, verify clock hasn't changed (another concurrent write)
|
|
683
|
-
// This prevents race conditions when multiple tabs/clients share the same IDB
|
|
684
|
-
const recheckStored = await ctx.getSerializedDoc(docId);
|
|
685
|
-
if (!recheckStored || recheckStored?.clock !== stored.clock) {
|
|
686
|
-
// Clock changed during our transaction - another client beat us
|
|
687
|
-
// Silently skip to avoid duplicate operations
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
await ctx.saveSerializedDoc({
|
|
691
|
-
serializedDoc,
|
|
692
|
-
docId,
|
|
693
|
-
clock: data.clock, // Use clock from server
|
|
694
|
-
});
|
|
695
|
-
didConsolidate = true; // Mark that we successfully saved
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
// CRITICAL: Only apply serverOps to memory if we actually saved to IDB
|
|
699
|
-
// If we skipped (clock already up-to-date), operations are already in memory via BC
|
|
700
|
-
if (didConsolidate && data.operations && data.operations.length > 0) {
|
|
701
|
-
// Apply to our own memory
|
|
702
|
-
void this._applyServerOperations({
|
|
703
|
-
docId,
|
|
704
|
-
operations: data.operations,
|
|
705
|
-
});
|
|
706
|
-
// Broadcast server operations to other tabs so they can apply them too
|
|
707
|
-
const presencePatch = this._getOwnPresencePatch(docId);
|
|
708
|
-
for (const op of data.operations) {
|
|
709
|
-
this._sendMessage({
|
|
710
|
-
type: "OPERATIONS",
|
|
711
|
-
operations: op,
|
|
712
|
-
docId,
|
|
713
|
-
...(presencePatch && { presence: presencePatch }),
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
// Status may have changed to "pushing-with-pending" during async ops
|
|
718
|
-
const currentStatus = this._pushStatusByDocId.get(docId);
|
|
719
|
-
const shouldRetry = currentStatus === "pushing-with-pending";
|
|
720
|
-
if (shouldRetry) {
|
|
721
|
-
// Keep status as "pushing" and retry immediately to avoid race window
|
|
722
|
-
// where a dirty event could trigger another concurrent _doPush
|
|
723
|
-
void this._doPush({ docId });
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
this._pushStatusByDocId.set(docId, "idle");
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
async _request(event, payload) {
|
|
730
|
-
// TO-DO: should I reject on disconnect?
|
|
731
|
-
return new Promise((resolve, reject) => {
|
|
732
|
-
// Add a timeout to prevent hanging forever if socket disconnects during request
|
|
733
|
-
const timeout = setTimeout(() => {
|
|
734
|
-
reject(new Error(`Request timeout: ${event}`));
|
|
735
|
-
}, 5000); // 5 second timeout
|
|
736
|
-
this._socket.emit(event, payload, (response) => {
|
|
737
|
-
clearTimeout(timeout);
|
|
738
|
-
resolve(response);
|
|
739
|
-
});
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
// ============================================================================
|
|
743
|
-
// Event Registration Methods
|
|
744
|
-
// ============================================================================
|
|
745
|
-
/**
|
|
746
|
-
* Register a handler for connection events.
|
|
747
|
-
* @returns Unsubscribe function
|
|
748
|
-
*/
|
|
749
|
-
onConnect(handler) {
|
|
750
|
-
this._connectHandlers.add(handler);
|
|
751
|
-
return () => {
|
|
752
|
-
this._connectHandlers.delete(handler);
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* Register a handler for disconnection events.
|
|
757
|
-
* @returns Unsubscribe function
|
|
758
|
-
*/
|
|
759
|
-
onDisconnect(handler) {
|
|
760
|
-
this._disconnectHandlers.add(handler);
|
|
761
|
-
return () => {
|
|
762
|
-
this._disconnectHandlers.delete(handler);
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Register a handler for document change events.
|
|
767
|
-
* @returns Unsubscribe function
|
|
768
|
-
*/
|
|
769
|
-
onChange(handler) {
|
|
770
|
-
const h = handler;
|
|
771
|
-
this._changeHandlers.add(h);
|
|
772
|
-
return () => {
|
|
773
|
-
this._changeHandlers.delete(h);
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Register a handler for sync events.
|
|
778
|
-
* @returns Unsubscribe function
|
|
779
|
-
*/
|
|
780
|
-
onSync(handler) {
|
|
781
|
-
const h = handler;
|
|
782
|
-
this._syncHandlers.add(h);
|
|
783
|
-
return () => {
|
|
784
|
-
this._syncHandlers.delete(h);
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Register a handler for document load events.
|
|
789
|
-
* @returns Unsubscribe function
|
|
790
|
-
*/
|
|
791
|
-
onDocLoad(handler) {
|
|
792
|
-
this._docLoadHandlers.add(handler);
|
|
793
|
-
return () => {
|
|
794
|
-
this._docLoadHandlers.delete(handler);
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* Register a handler for document unload events.
|
|
799
|
-
* @returns Unsubscribe function
|
|
353
|
+
* Register a listener for an event. Returns an unsubscribe function.
|
|
354
|
+
* Event payload type is inferred from the event name (first argument).
|
|
355
|
+
* @example
|
|
356
|
+
* const off = client.on("connect", () => { ... });
|
|
357
|
+
* client.on("docUnload", (ev) => { ... }); // ev is DocUnloadEvent
|
|
358
|
+
* off(); // unsubscribe
|
|
800
359
|
*/
|
|
801
|
-
|
|
802
|
-
this.
|
|
803
|
-
return () => {
|
|
804
|
-
this._docUnloadHandlers.delete(handler);
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
_emit(handlers, event) {
|
|
808
|
-
for (const handler of handlers) {
|
|
809
|
-
if (event !== undefined) {
|
|
810
|
-
handler(event);
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
handler();
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Get or create a unique device ID stored in localStorage.
|
|
820
|
-
* This ID is shared across all tabs/windows on the same device.
|
|
821
|
-
*/
|
|
822
|
-
function getDeviceId() {
|
|
823
|
-
const key = "docsync:deviceId";
|
|
824
|
-
let deviceId = localStorage.getItem(key);
|
|
825
|
-
if (!deviceId) {
|
|
826
|
-
// Generate a new device ID using crypto.randomUUID()
|
|
827
|
-
deviceId = crypto.randomUUID();
|
|
828
|
-
localStorage.setItem(key, deviceId);
|
|
360
|
+
on(event, listener) {
|
|
361
|
+
return this._events.on(event, listener);
|
|
829
362
|
}
|
|
830
|
-
return deviceId;
|
|
831
363
|
}
|
|
832
364
|
//# sourceMappingURL=index.js.map
|