@dabble/patches 0.1.1 → 0.2.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.
Files changed (51) hide show
  1. package/README.md +90 -169
  2. package/dist/client/Patches.d.ts +64 -0
  3. package/dist/client/Patches.js +166 -0
  4. package/dist/client/{PatchDoc.d.ts → PatchesDoc.d.ts} +33 -19
  5. package/dist/client/{PatchDoc.js → PatchesDoc.js} +57 -67
  6. package/dist/client/PatchesHistoryClient.d.ts +40 -0
  7. package/dist/client/PatchesHistoryClient.js +129 -0
  8. package/dist/client/index.d.ts +2 -2
  9. package/dist/client/index.js +1 -1
  10. package/dist/event-signal.js +13 -5
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +2 -1
  13. package/dist/net/AbstractTransport.js +2 -0
  14. package/dist/net/PatchesSync.d.ts +47 -0
  15. package/dist/net/PatchesSync.js +289 -0
  16. package/dist/net/index.d.ts +9 -7
  17. package/dist/net/index.js +8 -6
  18. package/dist/net/protocol/types.d.ts +3 -3
  19. package/dist/net/types.d.ts +6 -0
  20. package/dist/net/types.js +1 -0
  21. package/dist/net/websocket/PatchesWebSocket.d.ts +3 -3
  22. package/dist/net/websocket/WebSocketTransport.d.ts +13 -2
  23. package/dist/net/websocket/WebSocketTransport.js +105 -53
  24. package/dist/net/websocket/onlineState.d.ts +9 -0
  25. package/dist/net/websocket/onlineState.js +18 -0
  26. package/dist/persist/InMemoryStore.d.ts +23 -0
  27. package/dist/persist/InMemoryStore.js +103 -0
  28. package/dist/persist/IndexedDBStore.d.ts +13 -9
  29. package/dist/persist/IndexedDBStore.js +96 -14
  30. package/dist/persist/PatchesStore.d.ts +38 -0
  31. package/dist/persist/PatchesStore.js +1 -0
  32. package/dist/persist/index.d.ts +4 -2
  33. package/dist/persist/index.js +3 -1
  34. package/dist/server/{BranchManager.d.ts → PatchesBranchManager.d.ts} +4 -4
  35. package/dist/server/{BranchManager.js → PatchesBranchManager.js} +5 -5
  36. package/dist/server/{HistoryManager.d.ts → PatchesHistoryManager.d.ts} +4 -24
  37. package/dist/server/{HistoryManager.js → PatchesHistoryManager.js} +1 -34
  38. package/dist/server/{PatchServer.d.ts → PatchesServer.d.ts} +9 -9
  39. package/dist/server/{PatchServer.js → PatchesServer.js} +1 -1
  40. package/dist/server/index.d.ts +4 -4
  41. package/dist/server/index.js +3 -3
  42. package/dist/types.d.ts +11 -6
  43. package/dist/utils/batching.d.ts +5 -0
  44. package/dist/utils/batching.js +38 -0
  45. package/dist/utils.d.ts +2 -2
  46. package/dist/utils.js +8 -2
  47. package/package.json +1 -1
  48. package/dist/net/PatchesOfflineFirst.d.ts +0 -3
  49. package/dist/net/PatchesOfflineFirst.js +0 -3
  50. package/dist/net/PatchesRealtime.d.ts +0 -90
  51. package/dist/net/PatchesRealtime.js +0 -257
@@ -7,7 +7,7 @@ import { applyChanges } from '../utils.js';
7
7
  * coordinating batches of changes, managing versioning based on sessions (including offline),
8
8
  * and persisting data using a backend store.
9
9
  */
10
- export class PatchServer {
10
+ export class PatchesServer {
11
11
  constructor(store, options = {}) {
12
12
  this.store = store;
13
13
  this.sessionTimeoutMillis = (options.sessionTimeoutMinutes ?? 30) * 60 * 1000;
@@ -1,4 +1,4 @@
1
- export { BranchManager } from './BranchManager';
2
- export { HistoryManager } from './HistoryManager';
3
- export { PatchServer } from './PatchServer';
4
- export type { Change, PatchSnapshot, PatchState, VersionMetadata } from '../types';
1
+ export { PatchesBranchManager } from './PatchesBranchManager';
2
+ export { PatchesHistoryManager } from './PatchesHistoryManager';
3
+ export { PatchesServer } from './PatchesServer';
4
+ export type { Change, PatchesSnapshot as PatchSnapshot, PatchesState as PatchState, VersionMetadata } from '../types';
@@ -1,3 +1,3 @@
1
- export { BranchManager } from './BranchManager';
2
- export { HistoryManager } from './HistoryManager';
3
- export { PatchServer } from './PatchServer';
1
+ export { PatchesBranchManager } from './PatchesBranchManager';
2
+ export { PatchesHistoryManager } from './PatchesHistoryManager';
3
+ export { PatchesServer } from './PatchesServer';
package/dist/types.d.ts CHANGED
@@ -20,7 +20,7 @@ export interface Change {
20
20
  * @property state - The state of the document.
21
21
  * @property rev - The revision number of the state.
22
22
  */
23
- export interface PatchState<T = any> {
23
+ export interface PatchesState<T = any> {
24
24
  state: T;
25
25
  rev: number;
26
26
  }
@@ -30,7 +30,7 @@ export interface PatchState<T = any> {
30
30
  * @property rev - The revision number of the state.
31
31
  * @property changes - Any unapplied changes since `rev` that may be applied to the `state` to get the latest state.
32
32
  */
33
- export interface PatchSnapshot<T = any> extends PatchState<T> {
33
+ export interface PatchesSnapshot<T = any> extends PatchesState<T> {
34
34
  changes: Change[];
35
35
  }
36
36
  /** Status options for a branch */
@@ -111,9 +111,9 @@ export interface ListVersionsOptions {
111
111
  }
112
112
  /**
113
113
  * Interface for a backend storage system for patch synchronization.
114
- * Defines methods needed by PatchServer, HistoryManager, etc.
114
+ * Defines methods needed by PatchesServer, PatchesHistoryManager, etc.
115
115
  */
116
- export interface PatchStoreBackend {
116
+ export interface PatchesStoreBackend {
117
117
  /** Adds a subscription for a client to one or more documents. */
118
118
  addSubscription(clientId: string, docIds: string[]): Promise<string[]>;
119
119
  /** Removes a subscription for a client from one or more documents. */
@@ -139,9 +139,9 @@ export interface PatchStoreBackend {
139
139
  deleteDoc(docId: string): Promise<void>;
140
140
  }
141
141
  /**
142
- * Extends PatchStoreBackend with methods specifically for managing branches.
142
+ * Extends PatchesStoreBackend with methods specifically for managing branches.
143
143
  */
144
- export interface BranchingStoreBackend extends PatchStoreBackend {
144
+ export interface BranchingStoreBackend extends PatchesStoreBackend {
145
145
  /** Lists metadata records for branches originating from a document. */
146
146
  listBranches(docId: string): Promise<Branch[]>;
147
147
  /** Loads the metadata record for a specific branch ID. */
@@ -156,3 +156,8 @@ export interface BranchingStoreBackend extends PatchStoreBackend {
156
156
  */
157
157
  closeBranch(branchId: string): Promise<void>;
158
158
  }
159
+ export interface Deferred<T = void> {
160
+ promise: Promise<T>;
161
+ resolve: (value: T) => void;
162
+ reject: (reason?: any) => void;
163
+ }
@@ -0,0 +1,5 @@
1
+ import type { Change } from '../types.js';
2
+ /** Estimate JSON string byte size. */
3
+ export declare function getJSONByteSize(data: any): number;
4
+ /** Break changes into batches based on maxBatchSize. */
5
+ export declare function breakIntoBatches(changes: Change[], maxSize?: number): Change[][];
@@ -0,0 +1,38 @@
1
+ import { createId } from 'crypto-id';
2
+ /** Estimate JSON string byte size. */
3
+ export function getJSONByteSize(data) {
4
+ // Basic estimation, might not be perfectly accurate due to encoding nuances
5
+ return new TextEncoder().encode(JSON.stringify(data)).length;
6
+ }
7
+ /** Break changes into batches based on maxBatchSize. */
8
+ export function breakIntoBatches(changes, maxSize) {
9
+ if (!maxSize || getJSONByteSize(changes) < maxSize) {
10
+ return [changes];
11
+ }
12
+ const batchId = createId(12);
13
+ const batches = [];
14
+ let currentBatch = [];
15
+ let currentSize = 2; // Account for [] wrapper
16
+ for (const change of changes) {
17
+ // Add batchId if breaking up
18
+ const changeWithBatchId = { ...change, batchId };
19
+ const changeSize = getJSONByteSize(changeWithBatchId) + (currentBatch.length > 0 ? 1 : 0); // Add 1 for comma
20
+ // If a single change is too big, we have an issue (should be rare)
21
+ if (changeSize > maxSize && currentBatch.length === 0) {
22
+ console.error(`Single change ${change.id} (size ${changeSize}) exceeds maxBatchSize (${maxSize}). Sending as its own batch.`);
23
+ batches.push([changeWithBatchId]); // Send it anyway
24
+ continue;
25
+ }
26
+ if (currentSize + changeSize > maxSize) {
27
+ batches.push(currentBatch);
28
+ currentBatch = [];
29
+ currentSize = 2;
30
+ }
31
+ currentBatch.push(changeWithBatchId);
32
+ currentSize += changeSize;
33
+ }
34
+ if (currentBatch.length > 0) {
35
+ batches.push(currentBatch);
36
+ }
37
+ return batches;
38
+ }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Change, DeletedChange } from './types.js';
1
+ import type { Change, Deferred } from './types.js';
2
2
  /**
3
3
  * Splits an array of changes into two arrays based on the presence of a baseRev.
4
4
  * The first array contains changes before the first change with a baseRev,
@@ -33,4 +33,4 @@ export declare function applyChanges<T>(state: T, changes: Change[]): T;
33
33
  * @returns Array of rebased local changes with updated revision numbers
34
34
  */
35
35
  export declare function rebaseChanges(serverChanges: Change[], localChanges: Change[]): Change[];
36
- export declare function isChange(change: Change | DeletedChange): change is Change;
36
+ export declare function deferred<T = void>(): Deferred<T>;
package/dist/utils.js CHANGED
@@ -78,6 +78,12 @@ export function rebaseChanges(serverChanges, localChanges) {
78
78
  })
79
79
  .filter(Boolean);
80
80
  }
81
- export function isChange(change) {
82
- return 'rev' in change;
81
+ export function deferred() {
82
+ let resolve;
83
+ let reject;
84
+ const promise = new Promise((_resolve, _reject) => {
85
+ resolve = _resolve;
86
+ reject = _reject;
87
+ });
88
+ return { promise, resolve, reject };
83
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins",
5
5
  "author": "Jacob Wright <jacwright@gmail.com>",
6
6
  "bugs": {
@@ -1,3 +0,0 @@
1
- import { PatchesRealtime } from '../client/PatchesRealtime';
2
- export declare class PatchesOfflineFirst extends PatchesRealtime {
3
- }
@@ -1,3 +0,0 @@
1
- import { PatchesRealtime } from '../client/PatchesRealtime';
2
- export class PatchesOfflineFirst extends PatchesRealtime {
3
- }
@@ -1,90 +0,0 @@
1
- import { PatchDoc } from '../client/PatchDoc.js';
2
- import { type Signal } from '../event-signal.js';
3
- import type { ConnectionState } from './protocol/types.js';
4
- import type { WebSocketOptions } from './websocket/WebSocketTransport.js';
5
- export interface PatchesRealtimeOptions {
6
- /** Initial metadata to attach to changes from this client */
7
- metadata?: Record<string, any>;
8
- /** Custom WebSocket configuration (e.g. headers, protocols) */
9
- wsOptions?: WebSocketOptions;
10
- }
11
- /**
12
- * High-level client for real-time collaborative editing.
13
- * Manages WebSocket connection and document synchronization.
14
- */
15
- export declare class PatchesRealtime {
16
- private ws;
17
- private docs;
18
- private options;
19
- private wsChangesUnsubscriber;
20
- private connectionState;
21
- /** Emitted when the WebSocket connection state changes. */
22
- readonly onStateChange: Signal<(state: ConnectionState) => void>;
23
- /** Emitted when an error occurs during synchronization or connection. */
24
- readonly onError: Signal<(details: {
25
- type: "sendFailed" | "applyFailed" | "syncError" | "connectionError";
26
- docId?: string;
27
- error: Error;
28
- recoveryAttempted?: boolean;
29
- recoveryError?: Error;
30
- }) => void>;
31
- /**
32
- * Creates an instance of PatchesRealtime.
33
- * @param url The WebSocket URL of the Patches server.
34
- * @param opts Configuration options.
35
- */
36
- constructor(url: string, opts?: PatchesRealtimeOptions);
37
- /**
38
- * Establishes the WebSocket connection to the server.
39
- * Automatically called by `openDoc` if not already connected.
40
- */
41
- connect(): Promise<void>;
42
- /**
43
- * Opens a document by its ID, fetches its initial state, and sets up
44
- * real-time synchronization. If the document is already open, returns
45
- * the existing instance.
46
- * @param docId The unique identifier for the document.
47
- * @param opts Options, including optional metadata specific to this client's interaction with the document.
48
- * @returns A Promise resolving to the synchronized PatchDoc instance.
49
- * @throws If the connection fails, subscription fails, or fetching the initial document state fails.
50
- */
51
- openDoc<T extends object>(docId: string, opts?: {
52
- metadata?: Record<string, any>;
53
- }): Promise<PatchDoc<T>>;
54
- /**
55
- * Closes a specific document locally, stops listening for its changes,
56
- * and unsubscribes from server updates for that document.
57
- * @param docId The ID of the document to close.
58
- */
59
- closeDoc(docId: string): Promise<void>;
60
- /**
61
- * Closes all open documents, unsubscribes from all server updates,
62
- * cleans up all local listeners, and disconnects the WebSocket.
63
- */
64
- close(): void;
65
- /**
66
- * Sets up the synchronization logic for a single document. Listens for local
67
- * changes on the `PatchDoc` and triggers sending them to the server.
68
- * @param docId The document ID.
69
- * @param doc The `PatchDoc` instance.
70
- * @returns An unsubscriber function to remove the listener.
71
- * @internal
72
- */
73
- private _setupDocSync;
74
- /**
75
- * Checks if a document has pending local changes that haven't been sent
76
- * to the server and attempts to send them if the document is not already
77
- * in the process of sending. Handles server confirmation and potential
78
- * new changes that occurred during the send operation.
79
- * @param docId The document ID.
80
- * @param doc The `PatchDoc` instance.
81
- * @internal
82
- */
83
- private _sendPendingIfNecessary;
84
- /**
85
- * Attempts to resynchronize a document by fetching its latest state from the server.
86
- * @param docId The ID of the document to resynchronize.
87
- * @internal
88
- */
89
- private _resyncDoc;
90
- }
@@ -1,257 +0,0 @@
1
- import { PatchDoc } from '../client/PatchDoc.js';
2
- import { signal } from '../event-signal.js';
3
- import { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
4
- /**
5
- * High-level client for real-time collaborative editing.
6
- * Manages WebSocket connection and document synchronization.
7
- */
8
- export class PatchesRealtime {
9
- /**
10
- * Creates an instance of PatchesRealtime.
11
- * @param url The WebSocket URL of the Patches server.
12
- * @param opts Configuration options.
13
- */
14
- constructor(url, opts = {}) {
15
- this.docs = new Map();
16
- this.wsChangesUnsubscriber = null;
17
- this.connectionState = 'disconnected'; // Track internal state
18
- /** Emitted when an error occurs during synchronization or connection. */
19
- this.onError = signal();
20
- this.ws = new PatchesWebSocket(url, opts.wsOptions);
21
- this.options = opts;
22
- // Re-emit the state change signal and track internally
23
- this.onStateChange = signal(); // Explicit type
24
- this.ws.onStateChange((state) => {
25
- // Call signal directly to subscribe
26
- this.connectionState = state;
27
- this.onStateChange.emit(state); // Pass through the signal
28
- });
29
- // Register a single listener for all incoming changes from the WebSocket.
30
- this.wsChangesUnsubscriber = this.ws.onChangesCommitted(({ docId, changes }) => {
31
- const managedDoc = this.docs.get(docId);
32
- if (managedDoc) {
33
- try {
34
- // Apply changes received from the server to the local document copy.
35
- managedDoc.doc.applyExternalServerUpdate(changes);
36
- // After applying, immediately check if there are new local changes to send.
37
- this._sendPendingIfNecessary(docId, managedDoc.doc);
38
- }
39
- catch (error) {
40
- console.error(`Error applying external server update for doc ${docId}:`, error);
41
- const err = error instanceof Error ? error : new Error(String(error));
42
- // Emit error and attempt recovery by resyncing
43
- this.onError.emit({ type: 'applyFailed', docId, error: err, recoveryAttempted: true });
44
- // Use void to explicitly ignore the promise returned by _resyncDoc
45
- void this._resyncDoc(docId);
46
- }
47
- }
48
- // If the document isn't open locally (e.g., already closed), ignore the incoming change.
49
- });
50
- }
51
- /**
52
- * Establishes the WebSocket connection to the server.
53
- * Automatically called by `openDoc` if not already connected.
54
- */
55
- async connect() {
56
- // The underlying PatchesWebSocket handles connection state and idempotency.
57
- await this.ws.connect();
58
- }
59
- /**
60
- * Opens a document by its ID, fetches its initial state, and sets up
61
- * real-time synchronization. If the document is already open, returns
62
- * the existing instance.
63
- * @param docId The unique identifier for the document.
64
- * @param opts Options, including optional metadata specific to this client's interaction with the document.
65
- * @returns A Promise resolving to the synchronized PatchDoc instance.
66
- * @throws If the connection fails, subscription fails, or fetching the initial document state fails.
67
- */
68
- async openDoc(docId, opts = {}) {
69
- // Ensure connection is established before proceeding.
70
- await this.connect();
71
- // Return existing instance if already managed.
72
- const existingManagedDoc = this.docs.get(docId);
73
- if (existingManagedDoc) {
74
- return existingManagedDoc.doc;
75
- }
76
- // Subscribe to server updates for this document ID *before* fetching state
77
- // to avoid missing updates that might occur between getDoc and subscription completion.
78
- await this.ws.subscribe(docId);
79
- let snapshot;
80
- try {
81
- // Fetch the initial state (snapshot) of the document from the server.
82
- snapshot = await this.ws.getDoc(docId);
83
- }
84
- catch (err) {
85
- // If fetching the document fails, attempt to clean up by unsubscribing.
86
- console.error(`Failed to get initial state for doc ${docId}, attempting to unsubscribe:`, err);
87
- this.ws.unsubscribe(docId).catch(unsubErr => {
88
- // Log secondary error if unsubscribe also fails, but prioritize original error.
89
- console.warn(`Failed to unsubscribe ${docId} after getDoc error:`, unsubErr);
90
- });
91
- // Re-throw the original error to signal failure to the caller.
92
- throw err;
93
- }
94
- // Create the local PatchDoc instance with the fetched state and merged metadata.
95
- const doc = new PatchDoc(snapshot.state, { ...this.options.metadata, ...opts.metadata });
96
- // Import the snapshot details (like revision number) into the PatchDoc.
97
- doc.import(snapshot);
98
- // Set up the listener to send local changes to the server.
99
- const onChangeUnsubscriber = this._setupDocSync(docId, doc);
100
- // Store the document instance and its change listener unsubscriber.
101
- this.docs.set(docId, { doc, onChangeUnsubscriber });
102
- return doc;
103
- }
104
- /**
105
- * Closes a specific document locally, stops listening for its changes,
106
- * and unsubscribes from server updates for that document.
107
- * @param docId The ID of the document to close.
108
- */
109
- async closeDoc(docId) {
110
- const managedDoc = this.docs.get(docId);
111
- if (managedDoc) {
112
- // Stop listening to local changes for this document.
113
- managedDoc.onChangeUnsubscriber();
114
- // Remove the document from local management.
115
- this.docs.delete(docId);
116
- // Unsubscribe from server updates for this document.
117
- try {
118
- await this.ws.unsubscribe(docId);
119
- }
120
- catch (err) {
121
- // Log error but continue, as the primary goal (local cleanup) is done.
122
- console.warn(`Error unsubscribing from doc ${docId} during closeDoc:`, err);
123
- }
124
- }
125
- else {
126
- // Log if closeDoc is called for a document not currently managed.
127
- // This might indicate a logic error elsewhere or harmless redundant calls.
128
- console.warn(`closeDoc called for non-existent or already closed doc: ${docId}`);
129
- // No need to attempt unsubscribe if we don't think we were subscribed.
130
- }
131
- }
132
- /**
133
- * Closes all open documents, unsubscribes from all server updates,
134
- * cleans up all local listeners, and disconnects the WebSocket.
135
- */
136
- close() {
137
- // Attempt to unsubscribe from all currently managed documents on the server.
138
- const docIds = Array.from(this.docs.keys());
139
- if (docIds.length > 0) {
140
- this.ws.unsubscribe(docIds).catch(err => {
141
- console.warn('Error unsubscribing from documents during close:', err);
142
- });
143
- }
144
- // Clean up local change listeners for all managed documents.
145
- this.docs.forEach(managedDoc => managedDoc.onChangeUnsubscriber());
146
- this.docs.clear(); // Remove all documents from local management.
147
- // Clean up the global listener for incoming changes from the WebSocket.
148
- if (this.wsChangesUnsubscriber) {
149
- this.wsChangesUnsubscriber();
150
- this.wsChangesUnsubscriber = null;
151
- }
152
- // Disconnect the WebSocket connection.
153
- this.ws.disconnect();
154
- }
155
- /**
156
- * Sets up the synchronization logic for a single document. Listens for local
157
- * changes on the `PatchDoc` and triggers sending them to the server.
158
- * @param docId The document ID.
159
- * @param doc The `PatchDoc` instance.
160
- * @returns An unsubscriber function to remove the listener.
161
- * @internal
162
- */
163
- _setupDocSync(docId, doc) {
164
- // Listen for local changes and attempt to send them.
165
- return doc.onChange(async () => {
166
- await this._sendPendingIfNecessary(docId, doc);
167
- });
168
- }
169
- /**
170
- * Checks if a document has pending local changes that haven't been sent
171
- * to the server and attempts to send them if the document is not already
172
- * in the process of sending. Handles server confirmation and potential
173
- * new changes that occurred during the send operation.
174
- * @param docId The document ID.
175
- * @param doc The `PatchDoc` instance.
176
- * @internal
177
- */
178
- async _sendPendingIfNecessary(docId, doc) {
179
- // Only proceed if there are pending changes and we are not already sending.
180
- if (!doc.isSending && doc.hasPending) {
181
- let changes;
182
- try {
183
- // Get the changes formatted for the server.
184
- changes = doc.getUpdatesForServer();
185
- // Basic sanity check - should not happen if hasPending is true.
186
- if (changes.length === 0) {
187
- console.warn(`_sendPendingIfNecessary called for ${docId} with hasPending=true but getUpdatesForServer returned empty.`);
188
- return;
189
- }
190
- // Send the changes to the server via WebSocket.
191
- const serverCommit = await this.ws.commitChanges(docId, changes);
192
- // Apply the server's confirmation (e.g., updated revision number) to the local doc.
193
- doc.applyServerConfirmation(serverCommit);
194
- // IMPORTANT: After successful confirmation, immediately check again.
195
- // New local changes might have occurred while the commit request was in flight.
196
- // This recursive call ensures those are sent promptly.
197
- await this._sendPendingIfNecessary(docId, doc);
198
- }
199
- catch (error) {
200
- console.error(`Error sending changes to server for doc ${docId}:`, error);
201
- const err = error instanceof Error ? error : new Error(String(error));
202
- // Notify PatchDoc to move sending changes back to pending
203
- doc.handleSendFailure();
204
- // Emit error and attempt recovery if online
205
- this.onError.emit({ type: 'sendFailed', docId, error: err, recoveryAttempted: true });
206
- // Use the internally tracked state
207
- if (this.connectionState === 'connected' || this.connectionState === 'connecting') {
208
- console.warn(`Attempting recovery via resync for doc ${docId} after send failure.`);
209
- // Use void to explicitly ignore the promise returned by _resyncDoc
210
- void this._resyncDoc(docId);
211
- }
212
- else {
213
- console.warn(`Send failure for doc ${docId} while offline. Recovery deferred until reconnection.`);
214
- // No immediate recovery action needed, handleSendFailure already reset state.
215
- // The standard reconnection logic should trigger a send attempt later.
216
- }
217
- }
218
- }
219
- }
220
- /**
221
- * Attempts to resynchronize a document by fetching its latest state from the server.
222
- * @param docId The ID of the document to resynchronize.
223
- * @internal
224
- */
225
- async _resyncDoc(docId) {
226
- const managedDoc = this.docs.get(docId);
227
- if (!managedDoc) {
228
- console.warn(`_resyncDoc called for non-managed doc: ${docId}`);
229
- return;
230
- }
231
- console.log(`Attempting resync for doc ${docId}...`);
232
- try {
233
- const snapshot = await this.ws.getDoc(docId);
234
- // Import will trigger recalculateLocalState and onUpdate
235
- managedDoc.doc.import(snapshot);
236
- console.log(`Successfully resynced doc ${docId} to revision ${snapshot.rev}.`);
237
- // After successful resync, try sending any pending changes immediately
238
- await this._sendPendingIfNecessary(docId, managedDoc.doc);
239
- }
240
- catch (recoveryError) {
241
- console.error(`Failed to resync doc ${docId}:`, recoveryError);
242
- const recErr = recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError));
243
- // Emit a specific syncError to indicate recovery failure
244
- this.onError.emit({
245
- type: 'syncError',
246
- docId,
247
- // TODO: Should we include the original error that triggered the resync attempt?
248
- // For now, just include the recovery error itself.
249
- error: recErr, // Error during the recovery attempt
250
- recoveryAttempted: true,
251
- recoveryError: recErr,
252
- });
253
- // If resync fails, the document might be in an inconsistent state.
254
- // Further actions might be needed (e.g., closing the doc, user notification).
255
- }
256
- }
257
- }