@dabble/patches 0.1.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.
Files changed (120) hide show
  1. package/README.md +632 -0
  2. package/dist/client/PatchDoc.d.ts +85 -0
  3. package/dist/client/PatchDoc.js +299 -0
  4. package/dist/client/index.d.ts +2 -0
  5. package/dist/client/index.js +1 -0
  6. package/dist/event-signal.d.ts +31 -0
  7. package/dist/event-signal.js +40 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +1 -0
  10. package/dist/json-patch/JSONPatch.d.ts +126 -0
  11. package/dist/json-patch/JSONPatch.js +221 -0
  12. package/dist/json-patch/applyPatch.d.ts +11 -0
  13. package/dist/json-patch/applyPatch.js +37 -0
  14. package/dist/json-patch/composePatch.d.ts +2 -0
  15. package/dist/json-patch/composePatch.js +38 -0
  16. package/dist/json-patch/createJSONPatch.d.ts +35 -0
  17. package/dist/json-patch/createJSONPatch.js +41 -0
  18. package/dist/json-patch/index.d.ts +9 -0
  19. package/dist/json-patch/index.js +8 -0
  20. package/dist/json-patch/invertPatch.d.ts +2 -0
  21. package/dist/json-patch/invertPatch.js +31 -0
  22. package/dist/json-patch/ops/add.d.ts +2 -0
  23. package/dist/json-patch/ops/add.js +52 -0
  24. package/dist/json-patch/ops/bitmask.d.ts +14 -0
  25. package/dist/json-patch/ops/bitmask.js +48 -0
  26. package/dist/json-patch/ops/copy.d.ts +2 -0
  27. package/dist/json-patch/ops/copy.js +34 -0
  28. package/dist/json-patch/ops/increment.d.ts +5 -0
  29. package/dist/json-patch/ops/increment.js +21 -0
  30. package/dist/json-patch/ops/index.d.ts +22 -0
  31. package/dist/json-patch/ops/index.js +25 -0
  32. package/dist/json-patch/ops/move.d.ts +2 -0
  33. package/dist/json-patch/ops/move.js +211 -0
  34. package/dist/json-patch/ops/remove.d.ts +2 -0
  35. package/dist/json-patch/ops/remove.js +31 -0
  36. package/dist/json-patch/ops/replace.d.ts +2 -0
  37. package/dist/json-patch/ops/replace.js +44 -0
  38. package/dist/json-patch/ops/test.d.ts +2 -0
  39. package/dist/json-patch/ops/test.js +22 -0
  40. package/dist/json-patch/ops/text.d.ts +2 -0
  41. package/dist/json-patch/ops/text.js +57 -0
  42. package/dist/json-patch/patchProxy.d.ts +41 -0
  43. package/dist/json-patch/patchProxy.js +125 -0
  44. package/dist/json-patch/state.d.ts +2 -0
  45. package/dist/json-patch/state.js +8 -0
  46. package/dist/json-patch/transformPatch.d.ts +19 -0
  47. package/dist/json-patch/transformPatch.js +37 -0
  48. package/dist/json-patch/types.d.ts +52 -0
  49. package/dist/json-patch/types.js +1 -0
  50. package/dist/json-patch/utils/deepEqual.d.ts +1 -0
  51. package/dist/json-patch/utils/deepEqual.js +33 -0
  52. package/dist/json-patch/utils/exit.d.ts +2 -0
  53. package/dist/json-patch/utils/exit.js +4 -0
  54. package/dist/json-patch/utils/get.d.ts +2 -0
  55. package/dist/json-patch/utils/get.js +6 -0
  56. package/dist/json-patch/utils/getOpData.d.ts +2 -0
  57. package/dist/json-patch/utils/getOpData.js +10 -0
  58. package/dist/json-patch/utils/getType.d.ts +3 -0
  59. package/dist/json-patch/utils/getType.js +6 -0
  60. package/dist/json-patch/utils/index.d.ts +14 -0
  61. package/dist/json-patch/utils/index.js +14 -0
  62. package/dist/json-patch/utils/log.d.ts +2 -0
  63. package/dist/json-patch/utils/log.js +7 -0
  64. package/dist/json-patch/utils/ops.d.ts +14 -0
  65. package/dist/json-patch/utils/ops.js +103 -0
  66. package/dist/json-patch/utils/paths.d.ts +9 -0
  67. package/dist/json-patch/utils/paths.js +53 -0
  68. package/dist/json-patch/utils/pluck.d.ts +5 -0
  69. package/dist/json-patch/utils/pluck.js +30 -0
  70. package/dist/json-patch/utils/shallowCopy.d.ts +1 -0
  71. package/dist/json-patch/utils/shallowCopy.js +20 -0
  72. package/dist/json-patch/utils/softWrites.d.ts +7 -0
  73. package/dist/json-patch/utils/softWrites.js +18 -0
  74. package/dist/json-patch/utils/toArrayIndex.d.ts +1 -0
  75. package/dist/json-patch/utils/toArrayIndex.js +12 -0
  76. package/dist/json-patch/utils/toKeys.d.ts +1 -0
  77. package/dist/json-patch/utils/toKeys.js +15 -0
  78. package/dist/json-patch/utils/updateArrayIndexes.d.ts +5 -0
  79. package/dist/json-patch/utils/updateArrayIndexes.js +38 -0
  80. package/dist/json-patch/utils/updateArrayPath.d.ts +5 -0
  81. package/dist/json-patch/utils/updateArrayPath.js +45 -0
  82. package/dist/net/AbstractTransport.d.ts +47 -0
  83. package/dist/net/AbstractTransport.js +37 -0
  84. package/dist/net/PatchesOfflineFirst.d.ts +3 -0
  85. package/dist/net/PatchesOfflineFirst.js +3 -0
  86. package/dist/net/PatchesRealtime.d.ts +90 -0
  87. package/dist/net/PatchesRealtime.js +257 -0
  88. package/dist/net/index.d.ts +9 -0
  89. package/dist/net/index.js +8 -0
  90. package/dist/net/protocol/JSONRPCClient.d.ts +55 -0
  91. package/dist/net/protocol/JSONRPCClient.js +106 -0
  92. package/dist/net/protocol/types.d.ts +142 -0
  93. package/dist/net/protocol/types.js +1 -0
  94. package/dist/net/webrtc/WebRTCAwareness.d.ts +81 -0
  95. package/dist/net/webrtc/WebRTCAwareness.js +119 -0
  96. package/dist/net/webrtc/WebRTCTransport.d.ts +80 -0
  97. package/dist/net/webrtc/WebRTCTransport.js +157 -0
  98. package/dist/net/websocket/PatchesWebSocket.d.ts +107 -0
  99. package/dist/net/websocket/PatchesWebSocket.js +144 -0
  100. package/dist/net/websocket/SignalingService.d.ts +91 -0
  101. package/dist/net/websocket/SignalingService.js +140 -0
  102. package/dist/net/websocket/WebSocketTransport.d.ts +47 -0
  103. package/dist/net/websocket/WebSocketTransport.js +138 -0
  104. package/dist/persist/IndexedDBStore.d.ts +72 -0
  105. package/dist/persist/IndexedDBStore.js +283 -0
  106. package/dist/persist/index.d.ts +2 -0
  107. package/dist/persist/index.js +1 -0
  108. package/dist/server/BranchManager.d.ts +40 -0
  109. package/dist/server/BranchManager.js +138 -0
  110. package/dist/server/HistoryManager.d.ts +63 -0
  111. package/dist/server/HistoryManager.js +92 -0
  112. package/dist/server/PatchServer.d.ts +129 -0
  113. package/dist/server/PatchServer.js +358 -0
  114. package/dist/server/index.d.ts +4 -0
  115. package/dist/server/index.js +3 -0
  116. package/dist/types.d.ts +158 -0
  117. package/dist/types.js +1 -0
  118. package/dist/utils.d.ts +36 -0
  119. package/dist/utils.js +83 -0
  120. package/package.json +78 -0
@@ -0,0 +1,37 @@
1
+ import { signal } from '../event-signal.js';
2
+ /**
3
+ * Abstract base class that implements common functionality for various transport implementations.
4
+ * Provides state management and event signaling for connection state changes and message reception.
5
+ * Concrete transport implementations must extend this class and implement the abstract methods.
6
+ */
7
+ export class AbstractTransport {
8
+ constructor() {
9
+ this._state = 'disconnected';
10
+ /**
11
+ * Signal that emits when the connection state changes.
12
+ * Subscribers will receive the new connection state as an argument.
13
+ */
14
+ this.onStateChange = signal();
15
+ /**
16
+ * Signal that emits when a message is received from the transport.
17
+ * Subscribers will receive the message data as a string.
18
+ */
19
+ this.onMessage = signal();
20
+ }
21
+ /**
22
+ * Gets the current connection state of the transport.
23
+ * @returns The current connection state ('connecting', 'connected', 'disconnected', or 'error')
24
+ */
25
+ get state() {
26
+ return this._state;
27
+ }
28
+ /**
29
+ * Sets the connection state and emits a state change event.
30
+ * This method is protected and should only be called by subclasses.
31
+ * @param state - The new connection state
32
+ */
33
+ set state(state) {
34
+ this._state = state;
35
+ this.onStateChange.emit(state);
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ import { PatchesRealtime } from '../client/PatchesRealtime';
2
+ export declare class PatchesOfflineFirst extends PatchesRealtime {
3
+ }
@@ -0,0 +1,3 @@
1
+ import { PatchesRealtime } from '../client/PatchesRealtime';
2
+ export class PatchesOfflineFirst extends PatchesRealtime {
3
+ }
@@ -0,0 +1,90 @@
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
+ }
@@ -0,0 +1,257 @@
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
+ }
@@ -0,0 +1,9 @@
1
+ export { PatchesOfflineFirst } from './PatchesOfflineFirst';
2
+ export { PatchesRealtime } from './PatchesRealtime';
3
+ export { JSONRPCClient } from './protocol/JSONRPCClient';
4
+ export { PatchesWebSocket } from './websocket/PatchesWebSocket';
5
+ export { WebSocketTransport } from './websocket/WebSocketTransport';
6
+ export type { ConnectionState, ListOptions, PatchesAPI, PatchesNotificationParams, SignalNotificationParams, } from './protocol/types';
7
+ export * from './protocol/types.js';
8
+ export * from './webrtc/WebRTCAwareness.js';
9
+ export * from './webrtc/WebRTCTransport.js';
@@ -0,0 +1,8 @@
1
+ export { PatchesOfflineFirst } from './PatchesOfflineFirst';
2
+ export { PatchesRealtime } from './PatchesRealtime';
3
+ export { JSONRPCClient } from './protocol/JSONRPCClient';
4
+ export { PatchesWebSocket } from './websocket/PatchesWebSocket';
5
+ export { WebSocketTransport } from './websocket/WebSocketTransport';
6
+ export * from './protocol/types.js';
7
+ export * from './webrtc/WebRTCAwareness.js';
8
+ export * from './webrtc/WebRTCTransport.js';
@@ -0,0 +1,55 @@
1
+ import { type SignalSubscriber, type Unsubscriber } from '../../event-signal.js';
2
+ import type { Transport } from './types.js';
3
+ /**
4
+ * Implementation of a JSON-RPC 2.0 client that communicates over a provided transport layer.
5
+ * This client handles sending requests, notifications, and processing responses from a server.
6
+ * It also supports subscription to server-sent notifications.
7
+ */
8
+ export declare class JSONRPCClient {
9
+ private transport;
10
+ private nextId;
11
+ private pending;
12
+ private notificationSignals;
13
+ /**
14
+ * Creates a new JSON-RPC client instance.
15
+ *
16
+ * @param transport - The transport layer implementation that will be used for sending/receiving messages
17
+ */
18
+ constructor(transport: Transport);
19
+ /**
20
+ * Sends a JSON-RPC request to the server and returns a promise for the response.
21
+ *
22
+ * @param method - The name of the remote procedure to call
23
+ * @param params - The parameters to pass to the remote procedure (optional)
24
+ * @returns A promise that resolves with the result of the procedure call or rejects with an error
25
+ * @template T - The expected return type of the remote procedure
26
+ */
27
+ request<T = any>(method: string, params?: any): Promise<T>;
28
+ /**
29
+ * Sends a JSON-RPC notification to the server (no response expected).
30
+ *
31
+ * @param method - The name of the remote procedure to call
32
+ * @param params - The parameters to pass to the remote procedure (optional)
33
+ */
34
+ notify(method: string, params?: any): void;
35
+ /**
36
+ * Subscribes to server-sent notifications for a specific method.
37
+ *
38
+ * @param method - The notification method name to subscribe to
39
+ * @param handler - The callback function that will be invoked when notifications are received
40
+ * @returns A function that can be called to unsubscribe from the notifications
41
+ * @template T - The type of the handler function
42
+ */
43
+ on<T extends SignalSubscriber = SignalSubscriber>(method: string, handler: T): Unsubscriber;
44
+ /**
45
+ * Processes incoming messages from the transport layer.
46
+ * Handles three types of messages:
47
+ * - Notifications: Emitted to registered subscribers
48
+ * - Responses: Resolved/rejected to the corresponding pending promise
49
+ * - Invalid messages: Logged as warnings
50
+ *
51
+ * @private
52
+ * @param data - The raw message data received from the transport
53
+ */
54
+ private handleMessage;
55
+ }
@@ -0,0 +1,106 @@
1
+ import { signal } from '../../event-signal.js';
2
+ /**
3
+ * Implementation of a JSON-RPC 2.0 client that communicates over a provided transport layer.
4
+ * This client handles sending requests, notifications, and processing responses from a server.
5
+ * It also supports subscription to server-sent notifications.
6
+ */
7
+ export class JSONRPCClient {
8
+ /**
9
+ * Creates a new JSON-RPC client instance.
10
+ *
11
+ * @param transport - The transport layer implementation that will be used for sending/receiving messages
12
+ */
13
+ constructor(transport) {
14
+ this.transport = transport;
15
+ this.nextId = 1;
16
+ this.pending = new Map();
17
+ this.notificationSignals = new Map();
18
+ transport.onMessage(this.handleMessage.bind(this));
19
+ }
20
+ /**
21
+ * Sends a JSON-RPC request to the server and returns a promise for the response.
22
+ *
23
+ * @param method - The name of the remote procedure to call
24
+ * @param params - The parameters to pass to the remote procedure (optional)
25
+ * @returns A promise that resolves with the result of the procedure call or rejects with an error
26
+ * @template T - The expected return type of the remote procedure
27
+ */
28
+ async request(method, params) {
29
+ const id = this.nextId++;
30
+ const message = { jsonrpc: '2.0', id, method, params };
31
+ return new Promise((resolve, reject) => {
32
+ this.pending.set(id, { resolve, reject });
33
+ this.transport.send(JSON.stringify(message));
34
+ });
35
+ }
36
+ /**
37
+ * Sends a JSON-RPC notification to the server (no response expected).
38
+ *
39
+ * @param method - The name of the remote procedure to call
40
+ * @param params - The parameters to pass to the remote procedure (optional)
41
+ */
42
+ notify(method, params) {
43
+ const message = { jsonrpc: '2.0', method, params };
44
+ this.transport.send(JSON.stringify(message));
45
+ }
46
+ /**
47
+ * Subscribes to server-sent notifications for a specific method.
48
+ *
49
+ * @param method - The notification method name to subscribe to
50
+ * @param handler - The callback function that will be invoked when notifications are received
51
+ * @returns A function that can be called to unsubscribe from the notifications
52
+ * @template T - The type of the handler function
53
+ */
54
+ on(method, handler) {
55
+ let thisSignal = this.notificationSignals.get(method);
56
+ if (!thisSignal) {
57
+ thisSignal = signal();
58
+ this.notificationSignals.set(method, thisSignal);
59
+ }
60
+ return thisSignal(handler);
61
+ }
62
+ /**
63
+ * Processes incoming messages from the transport layer.
64
+ * Handles three types of messages:
65
+ * - Notifications: Emitted to registered subscribers
66
+ * - Responses: Resolved/rejected to the corresponding pending promise
67
+ * - Invalid messages: Logged as warnings
68
+ *
69
+ * @private
70
+ * @param data - The raw message data received from the transport
71
+ */
72
+ handleMessage(data) {
73
+ try {
74
+ const message = JSON.parse(data);
75
+ // Check if it's a notification (has method but no id)
76
+ if (typeof message === 'object' && message !== null && 'method' in message && !('id' in message)) {
77
+ const thisSignal = this.notificationSignals.get(message.method);
78
+ if (thisSignal)
79
+ thisSignal.emit(message.params);
80
+ return;
81
+ }
82
+ // Must be a response (has id)
83
+ if (typeof message === 'object' && message !== null && 'id' in message) {
84
+ const response = message;
85
+ const pending = this.pending.get(response.id);
86
+ if (pending) {
87
+ this.pending.delete(response.id);
88
+ if ('error' in response) {
89
+ pending.reject(response.error);
90
+ }
91
+ else {
92
+ pending.resolve(response.result);
93
+ }
94
+ }
95
+ else {
96
+ console.warn(`Received response for unknown id: ${response.id}`);
97
+ }
98
+ return;
99
+ }
100
+ console.warn('Received unexpected message format:', message);
101
+ }
102
+ catch (error) {
103
+ console.error('Failed to parse incoming message:', data, error);
104
+ }
105
+ }
106
+ }