cojson 0.19.20 → 0.19.22

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 (159) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
  4. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
  5. package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
  6. package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
  7. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
  8. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
  9. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
  10. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
  11. package/dist/CojsonMessageChannel/index.d.ts +3 -0
  12. package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
  13. package/dist/CojsonMessageChannel/index.js +2 -0
  14. package/dist/CojsonMessageChannel/index.js.map +1 -0
  15. package/dist/CojsonMessageChannel/types.d.ts +149 -0
  16. package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
  17. package/dist/CojsonMessageChannel/types.js +36 -0
  18. package/dist/CojsonMessageChannel/types.js.map +1 -0
  19. package/dist/GarbageCollector.d.ts +4 -2
  20. package/dist/GarbageCollector.d.ts.map +1 -1
  21. package/dist/GarbageCollector.js +5 -3
  22. package/dist/GarbageCollector.js.map +1 -1
  23. package/dist/SyncStateManager.d.ts +3 -3
  24. package/dist/SyncStateManager.d.ts.map +1 -1
  25. package/dist/SyncStateManager.js +4 -4
  26. package/dist/SyncStateManager.js.map +1 -1
  27. package/dist/coValueCore/coValueCore.d.ts +28 -1
  28. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  29. package/dist/coValueCore/coValueCore.js +50 -5
  30. package/dist/coValueCore/coValueCore.js.map +1 -1
  31. package/dist/coValues/account.d.ts.map +1 -1
  32. package/dist/coValues/account.js +10 -10
  33. package/dist/coValues/account.js.map +1 -1
  34. package/dist/exports.d.ts +1 -0
  35. package/dist/exports.d.ts.map +1 -1
  36. package/dist/exports.js +1 -0
  37. package/dist/exports.js.map +1 -1
  38. package/dist/ids.d.ts +1 -1
  39. package/dist/ids.d.ts.map +1 -1
  40. package/dist/ids.js.map +1 -1
  41. package/dist/knownState.d.ts +5 -0
  42. package/dist/knownState.d.ts.map +1 -1
  43. package/dist/knownState.js +15 -0
  44. package/dist/knownState.js.map +1 -1
  45. package/dist/localNode.d.ts +1 -3
  46. package/dist/localNode.d.ts.map +1 -1
  47. package/dist/localNode.js +11 -4
  48. package/dist/localNode.js.map +1 -1
  49. package/dist/storage/knownState.d.ts +5 -0
  50. package/dist/storage/knownState.d.ts.map +1 -1
  51. package/dist/storage/knownState.js +11 -0
  52. package/dist/storage/knownState.js.map +1 -1
  53. package/dist/storage/sqlite/client.d.ts +2 -0
  54. package/dist/storage/sqlite/client.d.ts.map +1 -1
  55. package/dist/storage/sqlite/client.js +18 -0
  56. package/dist/storage/sqlite/client.js.map +1 -1
  57. package/dist/storage/sqliteAsync/client.d.ts +2 -0
  58. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  59. package/dist/storage/sqliteAsync/client.js +20 -0
  60. package/dist/storage/sqliteAsync/client.js.map +1 -1
  61. package/dist/storage/storageAsync.d.ts +10 -3
  62. package/dist/storage/storageAsync.d.ts.map +1 -1
  63. package/dist/storage/storageAsync.js +52 -3
  64. package/dist/storage/storageAsync.js.map +1 -1
  65. package/dist/storage/storageSync.d.ts +9 -3
  66. package/dist/storage/storageSync.d.ts.map +1 -1
  67. package/dist/storage/storageSync.js +27 -3
  68. package/dist/storage/storageSync.js.map +1 -1
  69. package/dist/storage/types.d.ts +23 -0
  70. package/dist/storage/types.d.ts.map +1 -1
  71. package/dist/sync.d.ts +23 -0
  72. package/dist/sync.d.ts.map +1 -1
  73. package/dist/sync.js +136 -45
  74. package/dist/sync.js.map +1 -1
  75. package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
  76. package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
  77. package/dist/tests/CojsonMessageChannel.test.js +236 -0
  78. package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
  79. package/dist/tests/GarbageCollector.test.js +87 -13
  80. package/dist/tests/GarbageCollector.test.js.map +1 -1
  81. package/dist/tests/StorageApiAsync.test.js +124 -1
  82. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  83. package/dist/tests/StorageApiSync.test.js +123 -0
  84. package/dist/tests/StorageApiSync.test.js.map +1 -1
  85. package/dist/tests/SyncManager.processQueues.test.js +1 -1
  86. package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
  87. package/dist/tests/SyncStateManager.test.js +1 -1
  88. package/dist/tests/SyncStateManager.test.js.map +1 -1
  89. package/dist/tests/coPlainText.test.js +1 -1
  90. package/dist/tests/coPlainText.test.js.map +1 -1
  91. package/dist/tests/coValueCore.loadFromStorage.test.js +2 -0
  92. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  93. package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
  94. package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
  95. package/dist/tests/knownState.lazyLoading.test.js +167 -0
  96. package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
  97. package/dist/tests/messagesTestUtils.d.ts +5 -2
  98. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  99. package/dist/tests/messagesTestUtils.js +4 -0
  100. package/dist/tests/messagesTestUtils.js.map +1 -1
  101. package/dist/tests/sync.garbageCollection.test.js +56 -32
  102. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  103. package/dist/tests/sync.load.test.js +387 -1
  104. package/dist/tests/sync.load.test.js.map +1 -1
  105. package/dist/tests/sync.mesh.test.js +5 -5
  106. package/dist/tests/sync.mesh.test.js.map +1 -1
  107. package/dist/tests/sync.peerReconciliation.test.js +3 -3
  108. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  109. package/dist/tests/sync.storage.test.js +9 -9
  110. package/dist/tests/sync.storage.test.js.map +1 -1
  111. package/dist/tests/sync.storageAsync.test.js +7 -7
  112. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  113. package/dist/tests/sync.tracking.test.js +35 -4
  114. package/dist/tests/sync.tracking.test.js.map +1 -1
  115. package/dist/tests/testStorage.js +38 -2
  116. package/dist/tests/testStorage.js.map +1 -1
  117. package/dist/tests/testUtils.d.ts +38 -4
  118. package/dist/tests/testUtils.d.ts.map +1 -1
  119. package/dist/tests/testUtils.js +68 -7
  120. package/dist/tests/testUtils.js.map +1 -1
  121. package/package.json +4 -4
  122. package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
  123. package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
  124. package/src/CojsonMessageChannel/index.ts +9 -0
  125. package/src/CojsonMessageChannel/types.ts +200 -0
  126. package/src/GarbageCollector.ts +5 -5
  127. package/src/SyncStateManager.ts +6 -6
  128. package/src/coValueCore/coValueCore.ts +56 -7
  129. package/src/coValues/account.ts +12 -14
  130. package/src/exports.ts +1 -0
  131. package/src/ids.ts +1 -1
  132. package/src/knownState.ts +24 -0
  133. package/src/localNode.ts +12 -7
  134. package/src/storage/knownState.ts +12 -0
  135. package/src/storage/sqlite/client.ts +31 -0
  136. package/src/storage/sqliteAsync/client.ts +35 -0
  137. package/src/storage/storageAsync.ts +66 -4
  138. package/src/storage/storageSync.ts +37 -4
  139. package/src/storage/types.ts +32 -0
  140. package/src/sync.ts +159 -46
  141. package/src/tests/CojsonMessageChannel.test.ts +306 -0
  142. package/src/tests/GarbageCollector.test.ts +114 -13
  143. package/src/tests/StorageApiAsync.test.ts +186 -1
  144. package/src/tests/StorageApiSync.test.ts +181 -0
  145. package/src/tests/SyncManager.processQueues.test.ts +1 -1
  146. package/src/tests/SyncStateManager.test.ts +1 -1
  147. package/src/tests/coPlainText.test.ts +1 -1
  148. package/src/tests/coValueCore.loadFromStorage.test.ts +5 -0
  149. package/src/tests/knownState.lazyLoading.test.ts +219 -0
  150. package/src/tests/messagesTestUtils.ts +10 -3
  151. package/src/tests/sync.garbageCollection.test.ts +69 -36
  152. package/src/tests/sync.load.test.ts +482 -2
  153. package/src/tests/sync.mesh.test.ts +5 -5
  154. package/src/tests/sync.peerReconciliation.test.ts +3 -3
  155. package/src/tests/sync.storage.test.ts +9 -9
  156. package/src/tests/sync.storageAsync.test.ts +7 -7
  157. package/src/tests/sync.tracking.test.ts +54 -4
  158. package/src/tests/testStorage.ts +40 -2
  159. package/src/tests/testUtils.ts +99 -8
@@ -0,0 +1,332 @@
1
+ import type { Peer } from "../sync.js";
2
+ import { ConnectedPeerChannel } from "../streamUtils.js";
3
+ import { MessagePortOutgoingChannel } from "./MessagePortOutgoingChannel.js";
4
+ import type {
5
+ AcceptFromPortOptions,
6
+ WaitForConnectionOptions,
7
+ ExposeOptions,
8
+ MessageChannelLike,
9
+ MessagePortLike,
10
+ PostMessageTarget,
11
+ ReadyMessage,
12
+ } from "./types.js";
13
+ import {
14
+ isControlMessage,
15
+ isPortTransferMessage,
16
+ isReadyAckMessage,
17
+ isReadyMessage,
18
+ } from "./types.js";
19
+ import { logger } from "../logger.js";
20
+
21
+ /**
22
+ * CojsonMessageChannel provides a low-level API for creating cojson peers
23
+ * that communicate via the MessageChannel API (or compatible implementations
24
+ * like Electron's MessageChannelMain).
25
+ *
26
+ * Inspired by Comlink, it handles:
27
+ * - Port management (creating MessageChannels and transferring ports)
28
+ * - Handshake protocol (ensuring both sides are ready)
29
+ * - Peer creation (returning a standard cojson Peer)
30
+ */
31
+ export class CojsonMessageChannel {
32
+ /**
33
+ * Expose a cojson connection to a target with a postMessage API.
34
+ * Creates a MessageChannel, transfers port2 to the target, and waits for handshake.
35
+ *
36
+ * @param target - Any object with a postMessage method (Worker, Window, MessagePort, etc.)
37
+ * @param opts - Configuration options
38
+ * @returns A promise that resolves to a Peer once the handshake completes
39
+ */
40
+ static expose(
41
+ target: PostMessageTarget,
42
+ opts: ExposeOptions = {},
43
+ ): Promise<Peer> {
44
+ const id = opts.id ?? `channel_${Math.random()}`;
45
+ const role = opts.role ?? "client";
46
+
47
+ // Create or use provided MessageChannel
48
+ const channel: MessageChannelLike =
49
+ opts.messageChannel ?? new MessageChannel();
50
+ const { port1, port2 } = channel;
51
+
52
+ return new Promise<Peer>((resolve, reject) => {
53
+ let resolved = false;
54
+
55
+ const cleanup = () => {
56
+ port1.removeEventListener("message", handleMessage);
57
+ port1.removeEventListener("messageerror", handleError);
58
+ };
59
+
60
+ const handleError = (evt: unknown) => {
61
+ if (resolved) return;
62
+ cleanup();
63
+ port1.close();
64
+ reject(
65
+ new Error("MessageChannel error during handshake", {
66
+ cause: evt,
67
+ }),
68
+ );
69
+ };
70
+
71
+ const handleMessage = (event: unknown) => {
72
+ const data = (event as MessageEvent).data;
73
+
74
+ // Wait for ready acknowledgment from guest
75
+ if (isReadyAckMessage(data)) {
76
+ if (resolved) return;
77
+ resolved = true;
78
+ cleanup();
79
+
80
+ // Create the peer
81
+ const peer = createPeerFromPort(port1, {
82
+ id,
83
+ role,
84
+ onClose: opts.onClose,
85
+ });
86
+
87
+ resolve(peer);
88
+ }
89
+ };
90
+
91
+ // Start listening on port1
92
+ port1.addEventListener("message", handleMessage);
93
+ port1.addEventListener("messageerror", handleError);
94
+
95
+ // Start the port if needed (browser MessagePort requires this)
96
+ if (port1.start) {
97
+ port1.start();
98
+ }
99
+
100
+ // Transfer port2 to target
101
+ // Detect if target is a Window (has postMessage with targetOrigin signature)
102
+ // We use duck typing: if targetOrigin is provided, assume it's a Window
103
+ const targetOrigin = opts.targetOrigin ?? "*";
104
+ const portTransferMessage = { type: "jazz:port", id };
105
+
106
+ try {
107
+ // Try Window-style postMessage first if targetOrigin is specified
108
+ if (opts.targetOrigin !== undefined) {
109
+ // Window/iframe: postMessage(data, targetOrigin, [transfer])
110
+ target.postMessage(portTransferMessage, targetOrigin, [port2]);
111
+ } else {
112
+ // Worker/MessagePort style: postMessage(data, [transfer])
113
+ target.postMessage(portTransferMessage, [port2]);
114
+ }
115
+ } catch {
116
+ // Fallback: try the other signature
117
+ try {
118
+ target.postMessage(portTransferMessage, [port2]);
119
+ } catch (e) {
120
+ cleanup();
121
+ port1.close();
122
+ reject(new Error(`Failed to transfer port to target: ${e}`));
123
+ return;
124
+ }
125
+ }
126
+
127
+ // Send ready message with our ID
128
+ const readyMessage: ReadyMessage = { type: "jazz:ready", id };
129
+ port1.postMessage(readyMessage);
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Wait for an incoming Jazz connection.
135
+ * Listens for a port transfer message on the global scope and completes the handshake.
136
+ *
137
+ * @param opts - Configuration options
138
+ * @returns A promise that resolves to a Peer once the handshake completes
139
+ */
140
+ static waitForConnection(opts: WaitForConnectionOptions = {}): Promise<Peer> {
141
+ return new Promise<Peer>((resolve) => {
142
+ let resolved = false;
143
+
144
+ const scope = globalThis as unknown as EventTarget;
145
+
146
+ const cleanup = () => {
147
+ scope.removeEventListener("message", handlePortTransfer);
148
+ };
149
+
150
+ const handlePortTransfer = async (event: unknown) => {
151
+ const messageEvent = event as MessageEvent;
152
+ const data = messageEvent.data;
153
+
154
+ // Check if this is a valid port transfer message
155
+ if (!isPortTransferMessage(data)) {
156
+ return;
157
+ }
158
+
159
+ // If id filter is provided, check it against the port transfer message
160
+ if (opts.id !== undefined && data.id !== opts.id) {
161
+ return; // Ignore, keep waiting for matching id
162
+ }
163
+
164
+ // Validate origin if in Window context and allowedOrigins is specified
165
+ if (opts.allowedOrigins && opts.allowedOrigins.length > 0) {
166
+ const origin = messageEvent.origin;
167
+ const isAllowed = opts.allowedOrigins.some(
168
+ (allowed) => allowed === "*" || allowed === origin,
169
+ );
170
+ if (!isAllowed) {
171
+ logger.warn(`Ignoring message from non-allowed origin: ${origin}`);
172
+ return; // Ignore messages from non-allowed origins
173
+ }
174
+ }
175
+
176
+ // Get the transferred port
177
+ const port = messageEvent.ports?.[0] as MessagePortLike | undefined;
178
+ if (!port) {
179
+ return; // No port transferred, ignore
180
+ }
181
+
182
+ if (resolved) return;
183
+ resolved = true;
184
+ cleanup();
185
+
186
+ // Complete the handshake using acceptFromPort
187
+ // Pass the id from the port transfer message to acceptFromPort
188
+ const peer = await CojsonMessageChannel.acceptFromPort(port, {
189
+ ...opts,
190
+ id: data.id, // Use the id from the port transfer message
191
+ });
192
+ resolve(peer);
193
+ };
194
+
195
+ // Start listening for port transfer
196
+ scope.addEventListener("message", handlePortTransfer);
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Accept an incoming Jazz connection from a specific port.
202
+ * Lower-level API useful for testing or when you already have the port.
203
+ * This method has no timeout - it will wait indefinitely for the handshake.
204
+ *
205
+ * @param port - The MessagePort to accept the connection on
206
+ * @param opts - Configuration options
207
+ * @returns A promise that resolves to a Peer once the handshake completes
208
+ */
209
+ static acceptFromPort(
210
+ port: MessagePortLike,
211
+ opts: AcceptFromPortOptions = {},
212
+ ): Promise<Peer> {
213
+ const role = opts.role ?? "client";
214
+
215
+ return new Promise<Peer>((resolve) => {
216
+ let resolved = false;
217
+ let peerId: string | undefined;
218
+
219
+ const cleanup = () => {
220
+ port.removeEventListener("message", handleMessage);
221
+ port.removeEventListener("messageerror", handleError);
222
+ };
223
+
224
+ const handleError = () => {
225
+ if (resolved) return;
226
+ // On error, just close and let the caller handle it
227
+ cleanup();
228
+ port.close();
229
+ };
230
+
231
+ const handleMessage = (event: unknown) => {
232
+ const data = (event as MessageEvent).data;
233
+
234
+ // Wait for ready message from host
235
+ if (isReadyMessage(data)) {
236
+ // If id filter is provided, validate it
237
+ if (opts.id !== undefined && data.id !== opts.id) {
238
+ return; // Ignore, keep waiting for matching id
239
+ }
240
+
241
+ peerId = data.id;
242
+
243
+ // Send acknowledgment
244
+ port.postMessage({ type: "jazz:ready" });
245
+
246
+ if (resolved) return;
247
+ resolved = true;
248
+ cleanup();
249
+
250
+ // Create the peer
251
+ const peer = createPeerFromPort(port, {
252
+ id: peerId,
253
+ role,
254
+ onClose: opts.onClose,
255
+ });
256
+
257
+ resolve(peer);
258
+ }
259
+ };
260
+
261
+ // Start listening
262
+ port.addEventListener("message", handleMessage);
263
+ port.addEventListener("messageerror", handleError);
264
+
265
+ // Start the port if needed (browser MessagePort requires this)
266
+ if (port.start) {
267
+ port.start();
268
+ }
269
+ });
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Create a Peer from a MessagePort after handshake is complete.
275
+ */
276
+ function createPeerFromPort(
277
+ port: MessagePortLike,
278
+ opts: {
279
+ id: string;
280
+ role: "client" | "server";
281
+ onClose?: () => void;
282
+ },
283
+ ): Peer {
284
+ const incoming = new ConnectedPeerChannel();
285
+ const outgoing = new MessagePortOutgoingChannel(port);
286
+
287
+ // Forward messages from port to incoming channel
288
+ const handleMessage = (event: unknown) => {
289
+ const data = (event as MessageEvent).data;
290
+
291
+ // Skip control messages (they're for handshake only)
292
+ if (isControlMessage(data)) {
293
+ return;
294
+ }
295
+
296
+ incoming.push(data);
297
+ };
298
+
299
+ const handleError = () => {
300
+ incoming.push("Disconnected");
301
+ incoming.close();
302
+ };
303
+
304
+ port.addEventListener("message", handleMessage);
305
+ port.addEventListener("messageerror", handleError);
306
+ port.addEventListener("close", () => {
307
+ incoming.push("Disconnected");
308
+ incoming.close();
309
+ });
310
+
311
+ // Handle outgoing channel close
312
+ outgoing.onClose(() => {
313
+ port.removeEventListener("message", handleMessage);
314
+ port.removeEventListener("messageerror", handleError);
315
+ port.close();
316
+ incoming.push("Disconnected");
317
+ incoming.close();
318
+ opts.onClose?.();
319
+ });
320
+
321
+ // Handle incoming channel close (propagate to outgoing)
322
+ incoming.onClose(() => {
323
+ outgoing.close();
324
+ });
325
+
326
+ return {
327
+ id: opts.id,
328
+ incoming,
329
+ outgoing,
330
+ role: opts.role,
331
+ };
332
+ }
@@ -0,0 +1,52 @@
1
+ import type {
2
+ DisconnectedError,
3
+ OutgoingPeerChannel,
4
+ SyncMessage,
5
+ } from "../sync.js";
6
+ import type { MessagePortLike } from "./types.js";
7
+
8
+ /**
9
+ * An implementation of OutgoingPeerChannel that sends messages via a MessagePortLike.
10
+ *
11
+ * Messages are sent directly using the port's postMessage method,
12
+ * which uses structured cloning (no JSON serialization needed).
13
+ */
14
+ export class MessagePortOutgoingChannel implements OutgoingPeerChannel {
15
+ private port: MessagePortLike;
16
+ private closed = false;
17
+ private closeListeners = new Set<() => void>();
18
+
19
+ constructor(port: MessagePortLike) {
20
+ this.port = port;
21
+ }
22
+
23
+ push(msg: SyncMessage | DisconnectedError): void {
24
+ if (this.closed) {
25
+ return;
26
+ }
27
+
28
+ if (msg === "Disconnected") {
29
+ this.close();
30
+ return;
31
+ }
32
+
33
+ this.port.postMessage(msg);
34
+ }
35
+
36
+ close(): void {
37
+ if (this.closed) {
38
+ return;
39
+ }
40
+
41
+ this.closed = true;
42
+ this.port.close();
43
+
44
+ for (const listener of this.closeListeners) {
45
+ listener();
46
+ }
47
+ }
48
+
49
+ onClose(callback: () => void): void {
50
+ this.closeListeners.add(callback);
51
+ }
52
+ }
@@ -0,0 +1,9 @@
1
+ export { CojsonMessageChannel } from "./CojsonMessageChannel.js";
2
+ export type {
3
+ AcceptFromPortOptions,
4
+ WaitForConnectionOptions,
5
+ ExposeOptions,
6
+ MessageChannelLike,
7
+ PostMessageTarget,
8
+ MessagePortLike,
9
+ } from "./types.js";
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Type definitions for JazzMessageChannel
3
+ *
4
+ * These types support cross-context communication via MessageChannel API
5
+ * and compatible implementations (e.g., Electron's MessageChannelMain).
6
+ */
7
+
8
+ /**
9
+ * Duck-typed interface for any object that can receive messages via postMessage.
10
+ *
11
+ * Covers:
12
+ * - Worker
13
+ * - Window (including iframes via contentWindow)
14
+ * - MessagePort
15
+ * - ServiceWorker
16
+ * - Client (Service Worker clients)
17
+ * - Electron's WebContents (renderer windows)
18
+ */
19
+ export interface PostMessageTarget {
20
+ postMessage(message: unknown, transfer?: MessagePortLike[]): void;
21
+ postMessage(
22
+ message: unknown,
23
+ targetOrigin: string,
24
+ transfer?: MessagePortLike[],
25
+ ): void;
26
+ }
27
+
28
+ /**
29
+ * MessagePort-like interface that covers browser MessagePort and Electron MessagePortMain.
30
+ *
31
+ * Note: Electron's MessagePortMain does not have a start() method,
32
+ * so it's optional here.
33
+ */
34
+ export interface MessagePortLike {
35
+ postMessage(message: unknown): void;
36
+ addEventListener(
37
+ type: "message" | "messageerror",
38
+ listener: (event: unknown) => void,
39
+ ): void;
40
+ removeEventListener(
41
+ type: "message" | "messageerror",
42
+ listener: (event: unknown) => void,
43
+ ): void;
44
+ addEventListener(event: "close", listener: () => void): void;
45
+ removeEventListener(type: "close", listener: () => void): void;
46
+ /** Optional - not present on Electron's MessagePortMain */
47
+ start?(): void;
48
+ close(): void;
49
+ }
50
+
51
+ /**
52
+ * MessageChannel-like interface that covers browser MessageChannel
53
+ * and Electron MessageChannelMain.
54
+ */
55
+ export interface MessageChannelLike {
56
+ port1: MessagePortLike;
57
+ port2: MessagePortLike;
58
+ }
59
+
60
+ /**
61
+ * Options for JazzMessageChannel.expose()
62
+ */
63
+ export interface ExposeOptions {
64
+ /**
65
+ * Unique identifier for the peer connection, sent to the guest during handshake.
66
+ * Both sides will use this same ID for the Peer object.
67
+ * If not provided, a unique ID will be generated using `channel_${Math.random()}`.
68
+ */
69
+ id?: string;
70
+
71
+ /** Role of the peer in the sync topology */
72
+ role?: "client" | "server";
73
+
74
+ /** Target origin for Window targets (default: "*") */
75
+ targetOrigin?: string;
76
+
77
+ /**
78
+ * A pre-created MessageChannel to use instead of creating a new one.
79
+ * Use this for environments where the global MessageChannel is not available,
80
+ * e.g., Electron main process with MessageChannelMain.
81
+ * If not provided, a new MessageChannel will be created.
82
+ */
83
+ messageChannel?: MessageChannelLike;
84
+
85
+ /** Callback when the connection closes */
86
+ onClose?: () => void;
87
+ }
88
+
89
+ /**
90
+ * Options for CojsonMessageChannel.waitForConnection()
91
+ */
92
+ export interface WaitForConnectionOptions {
93
+ /**
94
+ * Expected peer ID to accept.
95
+ * If provided, only handshakes with matching id will be accepted; others are ignored.
96
+ * If not provided, any connection will be accepted.
97
+ */
98
+ id?: string;
99
+
100
+ /** Role of the peer in the sync topology */
101
+ role?: "client" | "server";
102
+
103
+ /** Allowed origins for Window contexts (default: ["*"]) */
104
+ allowedOrigins?: string[];
105
+
106
+ /** Callback when the connection closes */
107
+ onClose?: () => void;
108
+ }
109
+
110
+ /**
111
+ * Options for CojsonMessageChannel.acceptFromPort()
112
+ * Note: No timeout option - acceptFromPort waits indefinitely.
113
+ */
114
+ export interface AcceptFromPortOptions {
115
+ /**
116
+ * Expected peer ID to accept.
117
+ * If provided, only handshakes with matching id will be accepted; others are ignored.
118
+ * If not provided, any connection will be accepted.
119
+ */
120
+ id?: string;
121
+
122
+ /** Role of the peer in the sync topology */
123
+ role?: "client" | "server";
124
+
125
+ /** Callback when the connection closes */
126
+ onClose?: () => void;
127
+ }
128
+
129
+ /**
130
+ * Port transfer message sent via target.postMessage.
131
+ * The actual port is transferred via Transferable.
132
+ */
133
+ export interface PortTransferMessage {
134
+ type: "jazz:port";
135
+ /** The peer ID for this connection */
136
+ id: string;
137
+ }
138
+
139
+ /**
140
+ * Ready signal sent from host to guest via MessagePort.
141
+ * Contains the peer ID that both sides will use.
142
+ */
143
+ export interface ReadyMessage {
144
+ type: "jazz:ready";
145
+ /** The peer ID to use on both sides */
146
+ id: string;
147
+ }
148
+
149
+ /**
150
+ * Acknowledgment sent from guest to host via MessagePort.
151
+ * No id needed - guest uses the id from the host's ReadyMessage.
152
+ */
153
+ export interface ReadyAckMessage {
154
+ type: "jazz:ready";
155
+ }
156
+
157
+ /**
158
+ * Union of all control messages used in the handshake protocol.
159
+ */
160
+ export type ControlMessage =
161
+ | PortTransferMessage
162
+ | ReadyMessage
163
+ | ReadyAckMessage;
164
+
165
+ /**
166
+ * Type guard to check if a message is a Jazz control message.
167
+ * Control messages are identified by having a "type" property starting with "jazz:".
168
+ */
169
+ export function isControlMessage(msg: unknown): msg is ControlMessage {
170
+ return (
171
+ typeof msg === "object" &&
172
+ msg !== null &&
173
+ "type" in msg &&
174
+ typeof msg.type === "string" &&
175
+ msg.type.startsWith("jazz:")
176
+ );
177
+ }
178
+
179
+ /**
180
+ * Type guard to check if a message is a PortTransferMessage.
181
+ */
182
+ export function isPortTransferMessage(
183
+ msg: unknown,
184
+ ): msg is PortTransferMessage {
185
+ return isControlMessage(msg) && msg.type === "jazz:port";
186
+ }
187
+
188
+ /**
189
+ * Type guard to check if a message is a ReadyMessage (with id).
190
+ */
191
+ export function isReadyMessage(msg: unknown): msg is ReadyMessage {
192
+ return isControlMessage(msg) && msg.type === "jazz:ready" && "id" in msg;
193
+ }
194
+
195
+ /**
196
+ * Type guard to check if a message is a ReadyAckMessage (without id).
197
+ */
198
+ export function isReadyAckMessage(msg: unknown): msg is ReadyAckMessage {
199
+ return isControlMessage(msg) && msg.type === "jazz:ready" && !("id" in msg);
200
+ }
@@ -2,13 +2,13 @@ import { CoValueCore } from "./coValueCore/coValueCore.js";
2
2
  import { GARBAGE_COLLECTOR_CONFIG } from "./config.js";
3
3
  import { RawCoID } from "./ids.js";
4
4
 
5
+ /**
6
+ * TTL-based garbage collector for removing unused CoValues from memory.
7
+ */
5
8
  export class GarbageCollector {
6
9
  private readonly interval: ReturnType<typeof setInterval>;
7
10
 
8
- constructor(
9
- private readonly coValues: Map<RawCoID, CoValueCore>,
10
- private readonly garbageCollectGroups: boolean,
11
- ) {
11
+ constructor(private readonly coValues: Map<RawCoID, CoValueCore>) {
12
12
  this.interval = setInterval(() => {
13
13
  this.collect();
14
14
  }, GARBAGE_COLLECTOR_CONFIG.INTERVAL);
@@ -36,7 +36,7 @@ export class GarbageCollector {
36
36
  const timeSinceLastAccessed = currentTime - verified.lastAccessed;
37
37
 
38
38
  if (timeSinceLastAccessed > GARBAGE_COLLECTOR_CONFIG.MAX_AGE) {
39
- coValue.unmount(this.garbageCollectGroups);
39
+ coValue.unmount();
40
40
  }
41
41
  }
42
42
  }
@@ -4,14 +4,14 @@ import {
4
4
  areCurrentSessionsInSyncWith,
5
5
  } from "./knownState.js";
6
6
  import { PeerState } from "./PeerState.js";
7
- import { PeerID, SyncManager } from "./sync.js";
7
+ import { Peer, PeerID, SyncManager } from "./sync.js";
8
8
 
9
9
  export type SyncState = {
10
10
  uploaded: boolean;
11
11
  };
12
12
 
13
13
  export type GlobalSyncStateListenerCallback = (
14
- peerId: PeerID,
14
+ peer: Peer,
15
15
  knownState: CoValueKnownState,
16
16
  sync: SyncState,
17
17
  ) => void;
@@ -93,10 +93,10 @@ export class SyncStateManager {
93
93
  };
94
94
  }
95
95
 
96
- triggerUpdate(peerId: PeerID, id: RawCoID, knownState: CoValueKnownState) {
96
+ triggerUpdate(peer: Peer, id: RawCoID, knownState: CoValueKnownState) {
97
97
  const globalListeners = this.listeners;
98
98
  const coValueListeners = this.listenersByCoValues.get(id);
99
- const peerMap = this.listenersByPeersAndCoValues.get(peerId);
99
+ const peerMap = this.listenersByPeersAndCoValues.get(peer.id);
100
100
  const coValueAndPeerListeners = peerMap?.get(id);
101
101
 
102
102
  if (
@@ -113,12 +113,12 @@ export class SyncStateManager {
113
113
  };
114
114
 
115
115
  for (const listener of this.listeners) {
116
- listener(peerId, knownState, syncState);
116
+ listener(peer, knownState, syncState);
117
117
  }
118
118
 
119
119
  if (coValueListeners) {
120
120
  for (const listener of coValueListeners) {
121
- listener(peerId, knownState, syncState);
121
+ listener(peer, knownState, syncState);
122
122
  }
123
123
  }
124
124