@dabble/patches 0.2.10 → 0.2.12
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/client/Patches.d.ts +9 -1
- package/dist/client/Patches.js +9 -1
- package/dist/client/PatchesDoc.d.ts +13 -1
- package/dist/client/PatchesDoc.js +17 -3
- package/dist/json-patch/createJSONPatch.d.ts +1 -1
- package/dist/json-patch/createJSONPatch.js +2 -2
- package/dist/json-patch/patchProxy.d.ts +41 -0
- package/dist/json-patch/patchProxy.js +125 -0
- package/dist/json-patch/state.d.ts +2 -0
- package/dist/json-patch/state.js +8 -0
- package/dist/json-patch/transformPatch.d.ts +19 -0
- package/dist/json-patch/transformPatch.js +37 -0
- package/dist/json-patch/types.d.ts +52 -0
- package/dist/json-patch/types.js +1 -0
- package/dist/json-patch/utils/deepEqual.d.ts +1 -0
- package/dist/json-patch/utils/deepEqual.js +33 -0
- package/dist/json-patch/utils/exit.d.ts +2 -0
- package/dist/json-patch/utils/exit.js +4 -0
- package/dist/json-patch/utils/get.d.ts +2 -0
- package/dist/json-patch/utils/get.js +6 -0
- package/dist/json-patch/utils/getOpData.d.ts +2 -0
- package/dist/json-patch/utils/getOpData.js +10 -0
- package/dist/json-patch/utils/getType.d.ts +3 -0
- package/dist/json-patch/utils/getType.js +6 -0
- package/dist/json-patch/utils/index.d.ts +14 -0
- package/dist/json-patch/utils/index.js +14 -0
- package/dist/json-patch/utils/log.d.ts +2 -0
- package/dist/json-patch/utils/log.js +7 -0
- package/dist/json-patch/utils/ops.d.ts +14 -0
- package/dist/json-patch/utils/ops.js +103 -0
- package/dist/json-patch/utils/paths.d.ts +9 -0
- package/dist/json-patch/utils/paths.js +53 -0
- package/dist/json-patch/utils/pluck.d.ts +5 -0
- package/dist/json-patch/utils/pluck.js +30 -0
- package/dist/json-patch/utils/shallowCopy.d.ts +1 -0
- package/dist/json-patch/utils/shallowCopy.js +20 -0
- package/dist/json-patch/utils/softWrites.d.ts +7 -0
- package/dist/json-patch/utils/softWrites.js +18 -0
- package/dist/json-patch/utils/toArrayIndex.d.ts +1 -0
- package/dist/json-patch/utils/toArrayIndex.js +12 -0
- package/dist/json-patch/utils/toKeys.d.ts +1 -0
- package/dist/json-patch/utils/toKeys.js +15 -0
- package/dist/json-patch/utils/updateArrayIndexes.d.ts +5 -0
- package/dist/json-patch/utils/updateArrayIndexes.js +38 -0
- package/dist/json-patch/utils/updateArrayPath.d.ts +5 -0
- package/dist/json-patch/utils/updateArrayPath.js +45 -0
- package/dist/net/AbstractTransport.d.ts +47 -0
- package/dist/net/AbstractTransport.js +39 -0
- package/dist/net/PatchesSync.d.ts +52 -0
- package/dist/net/PatchesSync.js +293 -0
- package/dist/net/index.d.ts +9 -0
- package/dist/net/index.js +7 -0
- package/dist/net/protocol/JSONRPCClient.d.ts +55 -0
- package/dist/net/protocol/JSONRPCClient.js +106 -0
- package/dist/net/protocol/types.d.ts +142 -0
- package/dist/net/protocol/types.js +1 -0
- package/dist/net/types.d.ts +6 -0
- package/dist/net/types.js +1 -0
- package/dist/net/webrtc/WebRTCAwareness.d.ts +81 -0
- package/dist/net/webrtc/WebRTCAwareness.js +119 -0
- package/dist/net/webrtc/WebRTCTransport.d.ts +80 -0
- package/dist/net/webrtc/WebRTCTransport.js +157 -0
- package/dist/net/websocket/PatchesWebSocket.d.ts +107 -0
- package/dist/net/websocket/PatchesWebSocket.js +144 -0
- package/dist/net/websocket/SignalingService.d.ts +91 -0
- package/dist/net/websocket/SignalingService.js +140 -0
- package/dist/net/websocket/WebSocketTransport.d.ts +58 -0
- package/dist/net/websocket/WebSocketTransport.js +190 -0
- package/dist/net/websocket/onlineState.d.ts +9 -0
- package/dist/net/websocket/onlineState.js +18 -0
- package/dist/persist/InMemoryStore.d.ts +23 -0
- package/dist/persist/InMemoryStore.js +103 -0
- package/dist/persist/IndexedDBStore.d.ts +81 -0
- package/dist/persist/IndexedDBStore.js +377 -0
- package/dist/persist/PatchesStore.d.ts +38 -0
- package/dist/persist/PatchesStore.js +1 -0
- package/dist/persist/index.d.ts +3 -0
- package/dist/persist/index.js +3 -0
- package/dist/server/PatchesBranchManager.d.ts +40 -0
- package/dist/server/PatchesBranchManager.js +138 -0
- package/dist/server/PatchesHistoryManager.d.ts +43 -0
- package/dist/server/PatchesHistoryManager.js +59 -0
- package/dist/server/PatchesServer.d.ts +129 -0
- package/dist/server/PatchesServer.js +358 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +3 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.js +1 -0
- package/dist/utils/batching.d.ts +3 -0
- package/dist/utils/batching.js +41 -0
- package/dist/utils/breakChange.d.ts +10 -0
- package/dist/utils/breakChange.js +303 -0
- package/dist/utils/getJSONByteSize.d.ts +2 -0
- package/dist/utils/getJSONByteSize.js +13 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.js +103 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a JSON-RPC 2.0 request object.
|
|
3
|
+
*/
|
|
4
|
+
export interface JsonRpcRequest {
|
|
5
|
+
/** JSON-RPC protocol version, always "2.0" */
|
|
6
|
+
jsonrpc: '2.0';
|
|
7
|
+
/** Name of the remote procedure to call */
|
|
8
|
+
method: string;
|
|
9
|
+
/** Parameters to pass to the remote procedure */
|
|
10
|
+
params?: any;
|
|
11
|
+
/** Request identifier, used to match responses to requests */
|
|
12
|
+
id?: number | string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Represents a JSON-RPC 2.0 response object.
|
|
16
|
+
*/
|
|
17
|
+
export interface JsonRpcResponse {
|
|
18
|
+
/** JSON-RPC protocol version, always "2.0" */
|
|
19
|
+
jsonrpc: '2.0';
|
|
20
|
+
/** Result of the successful procedure call */
|
|
21
|
+
result?: any;
|
|
22
|
+
/** Error information if the procedure call failed */
|
|
23
|
+
error?: {
|
|
24
|
+
code: number;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
/** Response identifier, matches the id of the corresponding request */
|
|
28
|
+
id: number | string;
|
|
29
|
+
}
|
|
30
|
+
/** Union type for all possible JSON-RPC message types */
|
|
31
|
+
export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse;
|
|
32
|
+
/** Function type for sending JSON-RPC messages */
|
|
33
|
+
export type SendFn = (message: JsonRpcMessage) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Service that facilitates WebRTC connection establishment by relaying signaling messages.
|
|
36
|
+
* Acts as a central hub for WebRTC peers to exchange connection information.
|
|
37
|
+
*/
|
|
38
|
+
export declare class SignalingService {
|
|
39
|
+
private clients;
|
|
40
|
+
/**
|
|
41
|
+
* Registers a new client connection with the signaling service.
|
|
42
|
+
* Assigns a unique ID to the client and informs them of other connected peers.
|
|
43
|
+
*
|
|
44
|
+
* @param send - Function to send messages to this client
|
|
45
|
+
* @param id - Optional client ID (generated if not provided)
|
|
46
|
+
* @returns The client's assigned ID
|
|
47
|
+
*/
|
|
48
|
+
onClientConnected(send: SendFn, id?: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Handles a client disconnection by removing them from the registry
|
|
51
|
+
* and notifying all other connected clients.
|
|
52
|
+
*
|
|
53
|
+
* @param id - ID of the disconnected client
|
|
54
|
+
*/
|
|
55
|
+
onClientDisconnected(id: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Handles a signaling message from a client, relaying WebRTC session data
|
|
58
|
+
* between peers to facilitate connection establishment.
|
|
59
|
+
*
|
|
60
|
+
* @param fromId - ID of the client sending the message
|
|
61
|
+
* @param message - The JSON-RPC message or its string representation
|
|
62
|
+
* @returns True if the message was a valid signaling message and was handled, false otherwise
|
|
63
|
+
*/
|
|
64
|
+
handleClientMessage(fromId: string, message: string | JsonRpcRequest): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Sends a successful JSON-RPC response to a client.
|
|
67
|
+
*
|
|
68
|
+
* @private
|
|
69
|
+
* @param toId - ID of the client to send the response to
|
|
70
|
+
* @param id - Request ID to match in the response
|
|
71
|
+
* @param result - Result data to include in the response
|
|
72
|
+
*/
|
|
73
|
+
private respond;
|
|
74
|
+
/**
|
|
75
|
+
* Sends an error JSON-RPC response to a client.
|
|
76
|
+
*
|
|
77
|
+
* @private
|
|
78
|
+
* @param toId - ID of the client to send the error response to
|
|
79
|
+
* @param id - Request ID to match in the response, or undefined for notifications
|
|
80
|
+
* @param message - Error message to include
|
|
81
|
+
*/
|
|
82
|
+
private respondError;
|
|
83
|
+
/**
|
|
84
|
+
* Broadcasts a message to all connected clients, optionally excluding one.
|
|
85
|
+
*
|
|
86
|
+
* @private
|
|
87
|
+
* @param message - The message to broadcast
|
|
88
|
+
* @param excludeId - Optional ID of a client to exclude from the broadcast
|
|
89
|
+
*/
|
|
90
|
+
private broadcast;
|
|
91
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createId } from 'crypto-id';
|
|
2
|
+
/**
|
|
3
|
+
* Service that facilitates WebRTC connection establishment by relaying signaling messages.
|
|
4
|
+
* Acts as a central hub for WebRTC peers to exchange connection information.
|
|
5
|
+
*/
|
|
6
|
+
export class SignalingService {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.clients = new Map();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Registers a new client connection with the signaling service.
|
|
12
|
+
* Assigns a unique ID to the client and informs them of other connected peers.
|
|
13
|
+
*
|
|
14
|
+
* @param send - Function to send messages to this client
|
|
15
|
+
* @param id - Optional client ID (generated if not provided)
|
|
16
|
+
* @returns The client's assigned ID
|
|
17
|
+
*/
|
|
18
|
+
onClientConnected(send, id = createId(14)) {
|
|
19
|
+
this.clients.set(id, { send });
|
|
20
|
+
const welcome = {
|
|
21
|
+
jsonrpc: '2.0',
|
|
22
|
+
method: 'peer-welcome',
|
|
23
|
+
params: {
|
|
24
|
+
id,
|
|
25
|
+
peers: Array.from(this.clients.keys()).filter(pid => pid !== id),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
send(welcome);
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Handles a client disconnection by removing them from the registry
|
|
33
|
+
* and notifying all other connected clients.
|
|
34
|
+
*
|
|
35
|
+
* @param id - ID of the disconnected client
|
|
36
|
+
*/
|
|
37
|
+
onClientDisconnected(id) {
|
|
38
|
+
this.clients.delete(id);
|
|
39
|
+
// Broadcast to all others
|
|
40
|
+
this.broadcast({
|
|
41
|
+
jsonrpc: '2.0',
|
|
42
|
+
method: 'peer-disconnected',
|
|
43
|
+
params: { id },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Handles a signaling message from a client, relaying WebRTC session data
|
|
48
|
+
* between peers to facilitate connection establishment.
|
|
49
|
+
*
|
|
50
|
+
* @param fromId - ID of the client sending the message
|
|
51
|
+
* @param message - The JSON-RPC message or its string representation
|
|
52
|
+
* @returns True if the message was a valid signaling message and was handled, false otherwise
|
|
53
|
+
*/
|
|
54
|
+
handleClientMessage(fromId, message) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = typeof message === 'string' ? JSON.parse(message) : message;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (parsed.jsonrpc !== '2.0' || parsed.method !== 'peer-signal' || !parsed.params?.to)
|
|
63
|
+
return false;
|
|
64
|
+
const { params, id } = parsed;
|
|
65
|
+
const { to, data } = params;
|
|
66
|
+
const target = this.clients.get(to);
|
|
67
|
+
if (!target) {
|
|
68
|
+
this.respondError(fromId, id, 'Target not connected');
|
|
69
|
+
// Was a signaling message, even if the target is not connected
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const outbound = {
|
|
73
|
+
jsonrpc: '2.0',
|
|
74
|
+
method: 'signal',
|
|
75
|
+
params: {
|
|
76
|
+
from: fromId,
|
|
77
|
+
data,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
target.send(outbound);
|
|
81
|
+
if (id !== undefined) {
|
|
82
|
+
this.respond(fromId, id, 'ok');
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Sends a successful JSON-RPC response to a client.
|
|
88
|
+
*
|
|
89
|
+
* @private
|
|
90
|
+
* @param toId - ID of the client to send the response to
|
|
91
|
+
* @param id - Request ID to match in the response
|
|
92
|
+
* @param result - Result data to include in the response
|
|
93
|
+
*/
|
|
94
|
+
respond(toId, id, result) {
|
|
95
|
+
const client = this.clients.get(toId);
|
|
96
|
+
if (!client)
|
|
97
|
+
return;
|
|
98
|
+
const response = {
|
|
99
|
+
jsonrpc: '2.0',
|
|
100
|
+
result,
|
|
101
|
+
id,
|
|
102
|
+
};
|
|
103
|
+
client.send(response);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Sends an error JSON-RPC response to a client.
|
|
107
|
+
*
|
|
108
|
+
* @private
|
|
109
|
+
* @param toId - ID of the client to send the error response to
|
|
110
|
+
* @param id - Request ID to match in the response, or undefined for notifications
|
|
111
|
+
* @param message - Error message to include
|
|
112
|
+
*/
|
|
113
|
+
respondError(toId, id, message) {
|
|
114
|
+
if (id === undefined)
|
|
115
|
+
return;
|
|
116
|
+
const client = this.clients.get(toId);
|
|
117
|
+
if (!client)
|
|
118
|
+
return;
|
|
119
|
+
const response = {
|
|
120
|
+
jsonrpc: '2.0',
|
|
121
|
+
error: { code: -32000, message },
|
|
122
|
+
id,
|
|
123
|
+
};
|
|
124
|
+
client.send(response);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Broadcasts a message to all connected clients, optionally excluding one.
|
|
128
|
+
*
|
|
129
|
+
* @private
|
|
130
|
+
* @param message - The message to broadcast
|
|
131
|
+
* @param excludeId - Optional ID of a client to exclude from the broadcast
|
|
132
|
+
*/
|
|
133
|
+
broadcast(message, excludeId) {
|
|
134
|
+
for (const [id, client] of this.clients.entries()) {
|
|
135
|
+
if (id !== excludeId) {
|
|
136
|
+
client.send(message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AbstractTransport } from '../AbstractTransport.js';
|
|
2
|
+
/** WebSocket constructor options (subset) */
|
|
3
|
+
export interface WebSocketOptions {
|
|
4
|
+
protocol?: string | string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* WebSocket-based transport implementation that provides communication over the WebSocket protocol.
|
|
8
|
+
* Includes automatic reconnection with exponential backoff.
|
|
9
|
+
*/
|
|
10
|
+
export declare class WebSocketTransport extends AbstractTransport {
|
|
11
|
+
private url;
|
|
12
|
+
private wsOptions?;
|
|
13
|
+
private ws;
|
|
14
|
+
private reconnectTimer;
|
|
15
|
+
private backoff;
|
|
16
|
+
private connecting;
|
|
17
|
+
private connectionDeferred;
|
|
18
|
+
private onlineUnsubscriber;
|
|
19
|
+
/** Flag representing the *intent* to be connected. It is set by `connect()` and cleared by `disconnect()`. */
|
|
20
|
+
private shouldBeConnected;
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new WebSocket transport instance.
|
|
23
|
+
* @param url - The WebSocket server URL to connect to
|
|
24
|
+
* @param wsOptions - Optional configuration for the WebSocket connection
|
|
25
|
+
*/
|
|
26
|
+
constructor(url: string, wsOptions?: WebSocketOptions | undefined);
|
|
27
|
+
/**
|
|
28
|
+
* Establishes a connection to the WebSocket server.
|
|
29
|
+
* If a connection is already open or in progress, this method returns immediately.
|
|
30
|
+
* On connection failure, an automatic reconnection attempt will be scheduled.
|
|
31
|
+
* @returns A promise that resolves when the connection is established or rejects on error
|
|
32
|
+
*/
|
|
33
|
+
connect(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Terminates the WebSocket connection and cancels any pending reconnection attempts.
|
|
36
|
+
*/
|
|
37
|
+
disconnect(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Sends data through the WebSocket connection.
|
|
40
|
+
* @param data - The string data to send
|
|
41
|
+
* @throws {Error} If the WebSocket is not connected
|
|
42
|
+
*/
|
|
43
|
+
send(data: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Schedules a reconnection attempt using exponential backoff.
|
|
46
|
+
* The backoff time increases with each failed attempt, up to a maximum of 30 seconds.
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
private _scheduleReconnect;
|
|
50
|
+
/**
|
|
51
|
+
* Internal helper that adds (once) listeners for the browser's online/offline
|
|
52
|
+
* events so we can automatically attempt to connect when the network comes
|
|
53
|
+
* back and forcibly close when it goes away.
|
|
54
|
+
*/
|
|
55
|
+
private _ensureOnlineOfflineListeners;
|
|
56
|
+
/** Removes previously registered online/offline listeners (if any) */
|
|
57
|
+
private _removeOnlineOfflineListeners;
|
|
58
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { deferred } from '../../utils.js';
|
|
2
|
+
import { AbstractTransport } from '../AbstractTransport.js';
|
|
3
|
+
import { onlineState } from './onlineState.js';
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket-based transport implementation that provides communication over the WebSocket protocol.
|
|
6
|
+
* Includes automatic reconnection with exponential backoff.
|
|
7
|
+
*/
|
|
8
|
+
export class WebSocketTransport extends AbstractTransport {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new WebSocket transport instance.
|
|
11
|
+
* @param url - The WebSocket server URL to connect to
|
|
12
|
+
* @param wsOptions - Optional configuration for the WebSocket connection
|
|
13
|
+
*/
|
|
14
|
+
constructor(url, wsOptions) {
|
|
15
|
+
super();
|
|
16
|
+
this.url = url;
|
|
17
|
+
this.wsOptions = wsOptions;
|
|
18
|
+
this.ws = null;
|
|
19
|
+
this.reconnectTimer = null;
|
|
20
|
+
this.backoff = 1000;
|
|
21
|
+
this.connecting = false;
|
|
22
|
+
this.connectionDeferred = null;
|
|
23
|
+
this.onlineUnsubscriber = null;
|
|
24
|
+
/** Flag representing the *intent* to be connected. It is set by `connect()` and cleared by `disconnect()`. */
|
|
25
|
+
this.shouldBeConnected = false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Establishes a connection to the WebSocket server.
|
|
29
|
+
* If a connection is already open or in progress, this method returns immediately.
|
|
30
|
+
* On connection failure, an automatic reconnection attempt will be scheduled.
|
|
31
|
+
* @returns A promise that resolves when the connection is established or rejects on error
|
|
32
|
+
*/
|
|
33
|
+
async connect() {
|
|
34
|
+
// Record the caller's intent.
|
|
35
|
+
this.shouldBeConnected = true;
|
|
36
|
+
// Make sure we react to browser connectivity changes
|
|
37
|
+
this._ensureOnlineOfflineListeners();
|
|
38
|
+
// If the browser is known to be offline, defer the actual connection until
|
|
39
|
+
// it comes back online. We still return a promise that resolves once the
|
|
40
|
+
// connection is eventually established, so callers can `await` safely.
|
|
41
|
+
if (onlineState.isOffline) {
|
|
42
|
+
if (!this.connectionDeferred) {
|
|
43
|
+
this.connectionDeferred = deferred();
|
|
44
|
+
}
|
|
45
|
+
return this.connectionDeferred.promise;
|
|
46
|
+
}
|
|
47
|
+
// Return existing connection if already connected
|
|
48
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
// Return pending connection promise if already connecting
|
|
52
|
+
if (this.connecting && this.connectionDeferred) {
|
|
53
|
+
return this.connectionDeferred.promise;
|
|
54
|
+
}
|
|
55
|
+
this.connecting = true;
|
|
56
|
+
this.state = 'connecting';
|
|
57
|
+
// Create a new connection promise
|
|
58
|
+
this.connectionDeferred = deferred();
|
|
59
|
+
const { resolve, reject } = this.connectionDeferred;
|
|
60
|
+
try {
|
|
61
|
+
// Pass protocol option if available (standard 2nd arg)
|
|
62
|
+
// Other options like headers are not standard and require specific server/client handling
|
|
63
|
+
// or a different WebSocket client library.
|
|
64
|
+
this.ws = new WebSocket(this.url, this.wsOptions?.protocol);
|
|
65
|
+
this.ws.onopen = () => {
|
|
66
|
+
this.backoff = 1000; // Reset backoff on successful connection
|
|
67
|
+
this.state = 'connected';
|
|
68
|
+
this.connecting = false;
|
|
69
|
+
resolve();
|
|
70
|
+
};
|
|
71
|
+
this.ws.onclose = () => {
|
|
72
|
+
this.state = 'disconnected';
|
|
73
|
+
// If we were in the process of connecting, reject the promise
|
|
74
|
+
if (this.connecting) {
|
|
75
|
+
reject(new Error('Connection closed'));
|
|
76
|
+
this.connecting = false;
|
|
77
|
+
}
|
|
78
|
+
// Schedule reconnect regardless of whether it was a clean close
|
|
79
|
+
// as WebSockets don't always emit error events before closing
|
|
80
|
+
this._scheduleReconnect();
|
|
81
|
+
};
|
|
82
|
+
this.ws.onerror = error => {
|
|
83
|
+
this.state = 'error';
|
|
84
|
+
// If we're in the connection phase, reject the promise
|
|
85
|
+
if (this.connecting) {
|
|
86
|
+
this.connecting = false;
|
|
87
|
+
reject(error);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// If error happens after established connection,
|
|
91
|
+
// schedule a reconnect. The socket will likely close
|
|
92
|
+
// right after this, but we schedule it anyway to be sure.
|
|
93
|
+
this._scheduleReconnect();
|
|
94
|
+
}
|
|
95
|
+
// Log the error for debugging
|
|
96
|
+
console.error('WebSocket error:', error);
|
|
97
|
+
};
|
|
98
|
+
this.ws.onmessage = event => {
|
|
99
|
+
this.onMessage.emit(event.data);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.state = 'error';
|
|
104
|
+
this.connecting = false;
|
|
105
|
+
reject(error);
|
|
106
|
+
this._scheduleReconnect();
|
|
107
|
+
}
|
|
108
|
+
return this.connectionDeferred.promise;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Terminates the WebSocket connection and cancels any pending reconnection attempts.
|
|
112
|
+
*/
|
|
113
|
+
disconnect() {
|
|
114
|
+
// Clearing the intent stops automatic reconnection attempts.
|
|
115
|
+
this.shouldBeConnected = false;
|
|
116
|
+
// Remove listener now that we no longer intend to stay connected.
|
|
117
|
+
this._removeOnlineOfflineListeners();
|
|
118
|
+
if (this.reconnectTimer) {
|
|
119
|
+
clearTimeout(this.reconnectTimer);
|
|
120
|
+
this.reconnectTimer = null;
|
|
121
|
+
}
|
|
122
|
+
this.connecting = false;
|
|
123
|
+
if (this.ws) {
|
|
124
|
+
// Only attempt to close if not already closed
|
|
125
|
+
if (this.ws.readyState !== WebSocket.CLOSED && this.ws.readyState !== WebSocket.CLOSING) {
|
|
126
|
+
this.ws.close();
|
|
127
|
+
}
|
|
128
|
+
this.ws = null;
|
|
129
|
+
}
|
|
130
|
+
this.state = 'disconnected';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Sends data through the WebSocket connection.
|
|
134
|
+
* @param data - The string data to send
|
|
135
|
+
* @throws {Error} If the WebSocket is not connected
|
|
136
|
+
*/
|
|
137
|
+
send(data) {
|
|
138
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
139
|
+
throw new Error('WebSocket is not connected');
|
|
140
|
+
}
|
|
141
|
+
this.ws.send(data);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Schedules a reconnection attempt using exponential backoff.
|
|
145
|
+
* The backoff time increases with each failed attempt, up to a maximum of 30 seconds.
|
|
146
|
+
* @private
|
|
147
|
+
*/
|
|
148
|
+
_scheduleReconnect() {
|
|
149
|
+
// Only schedule a reconnect if the caller still wants to be connected.
|
|
150
|
+
if (!this.shouldBeConnected || onlineState.isOffline) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (this.reconnectTimer) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.reconnectTimer = setTimeout(() => {
|
|
157
|
+
this.reconnectTimer = null;
|
|
158
|
+
this.connect().catch(err => {
|
|
159
|
+
console.error('WebSocket reconnect failed:', err);
|
|
160
|
+
});
|
|
161
|
+
}, this.backoff);
|
|
162
|
+
this.backoff = Math.min(this.backoff * 1.5, 30000);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Internal helper that adds (once) listeners for the browser's online/offline
|
|
166
|
+
* events so we can automatically attempt to connect when the network comes
|
|
167
|
+
* back and forcibly close when it goes away.
|
|
168
|
+
*/
|
|
169
|
+
_ensureOnlineOfflineListeners() {
|
|
170
|
+
if (!this.onlineUnsubscriber) {
|
|
171
|
+
this.onlineUnsubscriber = onlineState.onOnlineChange(isOnline => {
|
|
172
|
+
if (isOnline && this.shouldBeConnected && !this.connecting && this.state !== 'connected') {
|
|
173
|
+
const { resolve, reject } = this.connectionDeferred;
|
|
174
|
+
this.connectionDeferred = null;
|
|
175
|
+
this.connect().then(resolve, reject);
|
|
176
|
+
}
|
|
177
|
+
else if (!isOnline && this.ws) {
|
|
178
|
+
this.ws.close();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Removes previously registered online/offline listeners (if any) */
|
|
184
|
+
_removeOnlineOfflineListeners() {
|
|
185
|
+
if (this.onlineUnsubscriber) {
|
|
186
|
+
this.onlineUnsubscriber();
|
|
187
|
+
this.onlineUnsubscriber = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare class OnlineState {
|
|
2
|
+
onOnlineChange: import("../../event-signal").Signal<(isOnline: boolean) => void>;
|
|
3
|
+
_isOnline: boolean;
|
|
4
|
+
constructor();
|
|
5
|
+
get isOnline(): boolean;
|
|
6
|
+
get isOffline(): boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const onlineState: OnlineState;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { signal } from '../../event-signal';
|
|
2
|
+
class OnlineState {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.onOnlineChange = signal();
|
|
5
|
+
this._isOnline = typeof navigator !== 'undefined' && navigator.onLine;
|
|
6
|
+
if (typeof addEventListener === 'function') {
|
|
7
|
+
addEventListener('online', () => (this._isOnline = true) && this.onOnlineChange.emit(true));
|
|
8
|
+
addEventListener('offline', () => (this._isOnline = false) || this.onOnlineChange.emit(false));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
get isOnline() {
|
|
12
|
+
return this._isOnline;
|
|
13
|
+
}
|
|
14
|
+
get isOffline() {
|
|
15
|
+
return !this._isOnline;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const onlineState = new OnlineState();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Change, PatchesSnapshot } from '../types.js';
|
|
2
|
+
import type { PatchesStore, TrackedDoc } from './PatchesStore.js';
|
|
3
|
+
/**
|
|
4
|
+
* A trivial in‑memory implementation of OfflineStore (soon PatchesStore).
|
|
5
|
+
* All data lives in JS objects – nothing survives a page reload.
|
|
6
|
+
* Useful for unit tests or when you want the old 'stateless realtime' behaviour.
|
|
7
|
+
*/
|
|
8
|
+
export declare class InMemoryStore implements PatchesStore {
|
|
9
|
+
private docs;
|
|
10
|
+
/** Signal emitted when pending changes are added (mirrors IndexedDBStore API) */
|
|
11
|
+
readonly onPendingChanges: import("../event-signal.js").Signal<(docId: string, changes: Change[]) => void>;
|
|
12
|
+
getDoc(docId: string): Promise<PatchesSnapshot | undefined>;
|
|
13
|
+
getPendingChanges(docId: string): Promise<Change[]>;
|
|
14
|
+
getLastRevs(docId: string): Promise<[number, number]>;
|
|
15
|
+
listDocs(includeDeleted?: boolean): Promise<TrackedDoc[]>;
|
|
16
|
+
savePendingChanges(docId: string, changes: Change[]): Promise<void>;
|
|
17
|
+
saveCommittedChanges(docId: string, changes: Change[], sentPendingRange?: [number, number]): Promise<void>;
|
|
18
|
+
trackDocs(docIds: string[]): Promise<void>;
|
|
19
|
+
untrackDocs(docIds: string[]): Promise<void>;
|
|
20
|
+
deleteDoc(docId: string): Promise<void>;
|
|
21
|
+
confirmDeleteDoc(docId: string): Promise<void>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { signal } from '../event-signal.js';
|
|
2
|
+
import { transformPatch } from '../json-patch/transformPatch.js';
|
|
3
|
+
import { applyChanges } from '../utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* A trivial in‑memory implementation of OfflineStore (soon PatchesStore).
|
|
6
|
+
* All data lives in JS objects – nothing survives a page reload.
|
|
7
|
+
* Useful for unit tests or when you want the old 'stateless realtime' behaviour.
|
|
8
|
+
*/
|
|
9
|
+
export class InMemoryStore {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.docs = new Map();
|
|
12
|
+
/** Signal emitted when pending changes are added (mirrors IndexedDBStore API) */
|
|
13
|
+
this.onPendingChanges = signal();
|
|
14
|
+
}
|
|
15
|
+
// ─── Reconstruction ────────────────────────────────────────────────────
|
|
16
|
+
async getDoc(docId) {
|
|
17
|
+
const buf = this.docs.get(docId);
|
|
18
|
+
if (!buf || buf.deleted)
|
|
19
|
+
return undefined;
|
|
20
|
+
const state = applyChanges(buf.snapshot?.state ?? null, buf.committed);
|
|
21
|
+
const committedRev = buf.committed.at(-1)?.rev ?? buf.snapshot?.rev ?? 0;
|
|
22
|
+
// Rebase pending if they are stale w.r.t committed
|
|
23
|
+
if (buf.pending.length && buf.pending[0].baseRev < committedRev) {
|
|
24
|
+
const patch = buf.committed.filter(c => c.rev > buf.pending[0].baseRev).flatMap(c => c.ops);
|
|
25
|
+
const offset = committedRev - buf.pending[0].baseRev;
|
|
26
|
+
buf.pending.forEach(ch => {
|
|
27
|
+
ch.rev += offset;
|
|
28
|
+
ch.ops = transformPatch(state, patch, ch.ops);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
state,
|
|
33
|
+
rev: committedRev,
|
|
34
|
+
changes: [...buf.pending],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async getPendingChanges(docId) {
|
|
38
|
+
return this.docs.get(docId)?.pending.slice() ?? [];
|
|
39
|
+
}
|
|
40
|
+
async getLastRevs(docId) {
|
|
41
|
+
const buf = this.docs.get(docId);
|
|
42
|
+
if (!buf)
|
|
43
|
+
return [0, 0];
|
|
44
|
+
const committedRev = buf.committed.at(-1)?.rev ?? buf.snapshot?.rev ?? 0;
|
|
45
|
+
const pendingRev = buf.pending.at(-1)?.rev ?? committedRev;
|
|
46
|
+
return [committedRev, pendingRev];
|
|
47
|
+
}
|
|
48
|
+
async listDocs(includeDeleted = false) {
|
|
49
|
+
return Array.from(this.docs.entries())
|
|
50
|
+
.filter(([, b]) => includeDeleted || !b.deleted)
|
|
51
|
+
.map(([docId, buf]) => ({
|
|
52
|
+
docId,
|
|
53
|
+
committedRev: buf.snapshot?.rev ?? buf.committed.at(-1)?.rev ?? 0,
|
|
54
|
+
deleted: buf.deleted,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
// ─── Writes ────────────────────────────────────────────────────────────
|
|
58
|
+
async savePendingChanges(docId, changes) {
|
|
59
|
+
const buf = this.docs.get(docId) ?? { committed: [], pending: [] };
|
|
60
|
+
if (!this.docs.has(docId))
|
|
61
|
+
this.docs.set(docId, buf);
|
|
62
|
+
buf.pending.push(...changes);
|
|
63
|
+
this.onPendingChanges.emit(docId, changes);
|
|
64
|
+
}
|
|
65
|
+
async saveCommittedChanges(docId, changes, sentPendingRange) {
|
|
66
|
+
const buf = this.docs.get(docId) ?? { committed: [], pending: [] };
|
|
67
|
+
if (!this.docs.has(docId))
|
|
68
|
+
this.docs.set(docId, buf);
|
|
69
|
+
buf.committed.push(...changes);
|
|
70
|
+
if (sentPendingRange) {
|
|
71
|
+
const [min, max] = sentPendingRange;
|
|
72
|
+
buf.pending = buf.pending.filter(p => p.rev < min || p.rev > max);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ─── Metadata / Tracking ───────────────────────────────────────────
|
|
76
|
+
async trackDocs(docIds) {
|
|
77
|
+
for (const docId of docIds) {
|
|
78
|
+
const buf = this.docs.get(docId) ?? { committed: [], pending: [] };
|
|
79
|
+
buf.deleted = undefined; // Ensure not marked as deleted
|
|
80
|
+
if (!this.docs.has(docId)) {
|
|
81
|
+
this.docs.set(docId, buf);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async untrackDocs(docIds) {
|
|
86
|
+
docIds.forEach(this.docs.delete, this.docs);
|
|
87
|
+
}
|
|
88
|
+
// ─── Misc / Lifecycle ────────────────────────────────────────────────
|
|
89
|
+
async deleteDoc(docId) {
|
|
90
|
+
const buf = this.docs.get(docId) ?? { committed: [], pending: [] };
|
|
91
|
+
buf.deleted = true;
|
|
92
|
+
buf.committed = [];
|
|
93
|
+
buf.pending = [];
|
|
94
|
+
buf.snapshot = undefined;
|
|
95
|
+
this.docs.set(docId, buf);
|
|
96
|
+
}
|
|
97
|
+
async confirmDeleteDoc(docId) {
|
|
98
|
+
this.docs.delete(docId);
|
|
99
|
+
}
|
|
100
|
+
async close() {
|
|
101
|
+
this.docs.clear();
|
|
102
|
+
}
|
|
103
|
+
}
|