cojson 0.19.21 → 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 (124) 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 +19 -1
  28. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  29. package/dist/coValueCore/coValueCore.js +29 -5
  30. package/dist/coValueCore/coValueCore.js.map +1 -1
  31. package/dist/exports.d.ts +1 -0
  32. package/dist/exports.d.ts.map +1 -1
  33. package/dist/exports.js +1 -0
  34. package/dist/exports.js.map +1 -1
  35. package/dist/localNode.d.ts +1 -3
  36. package/dist/localNode.d.ts.map +1 -1
  37. package/dist/localNode.js +3 -2
  38. package/dist/localNode.js.map +1 -1
  39. package/dist/storage/storageAsync.d.ts +8 -3
  40. package/dist/storage/storageAsync.d.ts.map +1 -1
  41. package/dist/storage/storageAsync.js +12 -3
  42. package/dist/storage/storageAsync.js.map +1 -1
  43. package/dist/storage/storageSync.d.ts +8 -3
  44. package/dist/storage/storageSync.d.ts.map +1 -1
  45. package/dist/storage/storageSync.js +12 -3
  46. package/dist/storage/storageSync.js.map +1 -1
  47. package/dist/storage/types.d.ts +5 -0
  48. package/dist/storage/types.d.ts.map +1 -1
  49. package/dist/sync.d.ts +6 -0
  50. package/dist/sync.d.ts.map +1 -1
  51. package/dist/sync.js +25 -4
  52. package/dist/sync.js.map +1 -1
  53. package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
  54. package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
  55. package/dist/tests/CojsonMessageChannel.test.js +236 -0
  56. package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
  57. package/dist/tests/GarbageCollector.test.js +87 -13
  58. package/dist/tests/GarbageCollector.test.js.map +1 -1
  59. package/dist/tests/StorageApiAsync.test.js +33 -1
  60. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  61. package/dist/tests/StorageApiSync.test.js +32 -0
  62. package/dist/tests/StorageApiSync.test.js.map +1 -1
  63. package/dist/tests/SyncManager.processQueues.test.js +1 -1
  64. package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
  65. package/dist/tests/SyncStateManager.test.js +1 -1
  66. package/dist/tests/SyncStateManager.test.js.map +1 -1
  67. package/dist/tests/coPlainText.test.js +1 -1
  68. package/dist/tests/coPlainText.test.js.map +1 -1
  69. package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
  70. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  71. package/dist/tests/knownState.lazyLoading.test.js +1 -0
  72. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  73. package/dist/tests/sync.garbageCollection.test.js +56 -32
  74. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  75. package/dist/tests/sync.load.test.js +3 -5
  76. package/dist/tests/sync.load.test.js.map +1 -1
  77. package/dist/tests/sync.mesh.test.js +1 -1
  78. package/dist/tests/sync.mesh.test.js.map +1 -1
  79. package/dist/tests/sync.peerReconciliation.test.js +3 -3
  80. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  81. package/dist/tests/sync.storage.test.js +9 -9
  82. package/dist/tests/sync.storage.test.js.map +1 -1
  83. package/dist/tests/sync.storageAsync.test.js +7 -7
  84. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  85. package/dist/tests/sync.tracking.test.js +35 -4
  86. package/dist/tests/sync.tracking.test.js.map +1 -1
  87. package/dist/tests/testStorage.js +2 -2
  88. package/dist/tests/testStorage.js.map +1 -1
  89. package/dist/tests/testUtils.d.ts +24 -2
  90. package/dist/tests/testUtils.d.ts.map +1 -1
  91. package/dist/tests/testUtils.js +68 -7
  92. package/dist/tests/testUtils.js.map +1 -1
  93. package/package.json +4 -4
  94. package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
  95. package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
  96. package/src/CojsonMessageChannel/index.ts +9 -0
  97. package/src/CojsonMessageChannel/types.ts +200 -0
  98. package/src/GarbageCollector.ts +5 -5
  99. package/src/SyncStateManager.ts +6 -6
  100. package/src/coValueCore/coValueCore.ts +30 -7
  101. package/src/exports.ts +1 -0
  102. package/src/localNode.ts +3 -5
  103. package/src/storage/storageAsync.ts +15 -4
  104. package/src/storage/storageSync.ts +15 -4
  105. package/src/storage/types.ts +6 -0
  106. package/src/sync.ts +33 -4
  107. package/src/tests/CojsonMessageChannel.test.ts +306 -0
  108. package/src/tests/GarbageCollector.test.ts +114 -13
  109. package/src/tests/StorageApiAsync.test.ts +50 -1
  110. package/src/tests/StorageApiSync.test.ts +49 -0
  111. package/src/tests/SyncManager.processQueues.test.ts +1 -1
  112. package/src/tests/SyncStateManager.test.ts +1 -1
  113. package/src/tests/coPlainText.test.ts +1 -1
  114. package/src/tests/coValueCore.loadFromStorage.test.ts +2 -0
  115. package/src/tests/knownState.lazyLoading.test.ts +2 -0
  116. package/src/tests/sync.garbageCollection.test.ts +69 -36
  117. package/src/tests/sync.load.test.ts +3 -5
  118. package/src/tests/sync.mesh.test.ts +1 -1
  119. package/src/tests/sync.peerReconciliation.test.ts +3 -3
  120. package/src/tests/sync.storage.test.ts +9 -9
  121. package/src/tests/sync.storageAsync.test.ts +7 -7
  122. package/src/tests/sync.tracking.test.ts +54 -4
  123. package/src/tests/testStorage.ts +2 -2
  124. package/src/tests/testUtils.ts +85 -6
@@ -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
 
@@ -373,16 +373,26 @@ export class CoValueCore {
373
373
  }
374
374
  }
375
375
 
376
- unmount(garbageCollectGroups = false) {
377
- if (
378
- !garbageCollectGroups &&
379
- this.verified?.header.ruleset.type === "group"
380
- ) {
376
+ /**
377
+ * Removes the CoValue from memory.
378
+ *
379
+ * @returns true if the coValue was successfully unmounted, false otherwise
380
+ */
381
+ unmount(): boolean {
382
+ if (this.listeners.size > 0) {
383
+ // The coValue is still in use
381
384
  return false;
382
385
  }
383
386
 
384
- if (this.listeners.size > 0) {
385
- return false; // The coValue is still in use
387
+ for (const dependant of this.dependant) {
388
+ if (this.node.hasCoValue(dependant)) {
389
+ // Another in-memory coValue depends on this coValue
390
+ return false;
391
+ }
392
+ }
393
+
394
+ if (!this.node.syncManager.isSyncedToServerPeers(this.id)) {
395
+ return false;
386
396
  }
387
397
 
388
398
  this.counter.add(-1, { state: this.loadingState });
@@ -1193,6 +1203,15 @@ export class CoValueCore {
1193
1203
  return matchingTransactions;
1194
1204
  }
1195
1205
 
1206
+ /**
1207
+ * The CoValues that this CoValue depends on.
1208
+ * We currently track dependencies for:
1209
+ * - Ownership (a CoValue depends on its account/group owner)
1210
+ * - Group membership (a group depends on its direct account/group members)
1211
+ * - Sessions (a CoValue depends on Accounts that made changes to it)
1212
+ * - Branches (a branched CoValue depends on its branch source)
1213
+ * See {@link dependant} for the CoValues that depend on this CoValue.
1214
+ */
1196
1215
  dependencies: Set<RawCoID> = new Set();
1197
1216
  incompleteDependencies: Set<RawCoID> = new Set();
1198
1217
  private addDependency(dependency: RawCoID) {
@@ -1238,6 +1257,10 @@ export class CoValueCore {
1238
1257
  }
1239
1258
  }
1240
1259
 
1260
+ /**
1261
+ * The CoValues that depend on this CoValue.
1262
+ * This is the inverse relationship of {@link dependencies}.
1263
+ */
1241
1264
  dependant: Set<RawCoID> = new Set();
1242
1265
  private addDependant(dependant: RawCoID) {
1243
1266
  this.dependant.add(dependant);
package/src/exports.ts CHANGED
@@ -206,6 +206,7 @@ export type {
206
206
  };
207
207
 
208
208
  export * from "./storage/index.js";
209
+ export * from "./CojsonMessageChannel/index.js";
209
210
 
210
211
  // biome-ignore format: off
211
212
  // eslint-disable-next-line @typescript-eslint/no-namespace
package/src/localNode.ts CHANGED
@@ -82,15 +82,12 @@ export class LocalNode {
82
82
  this.crypto = crypto;
83
83
  }
84
84
 
85
- enableGarbageCollector(opts?: { garbageCollectGroups?: boolean }) {
85
+ enableGarbageCollector() {
86
86
  if (this.garbageCollector) {
87
87
  return;
88
88
  }
89
89
 
90
- this.garbageCollector = new GarbageCollector(
91
- this.coValues,
92
- opts?.garbageCollectGroups ?? false,
93
- );
90
+ this.garbageCollector = new GarbageCollector(this.coValues);
94
91
  }
95
92
 
96
93
  setStorage(storage: StorageAPI) {
@@ -133,6 +130,7 @@ export class LocalNode {
133
130
 
134
131
  internalDeleteCoValue(id: RawCoID) {
135
132
  this.coValues.delete(id);
133
+ this.storage?.onCoValueUnmounted(id);
136
134
  }
137
135
 
138
136
  getCurrentAccountOrAgentID(): RawAccountID | AgentID {
@@ -34,7 +34,11 @@ import type {
34
34
  export class StorageApiAsync implements StorageAPI {
35
35
  private readonly dbClient: DBClientInterfaceAsync;
36
36
 
37
- private loadedCoValues = new Set<RawCoID>();
37
+ /**
38
+ * Keeps track of CoValues that are in memory, to avoid reloading them from storage
39
+ * when it isn't necessary
40
+ */
41
+ private inMemoryCoValues = new Set<RawCoID>();
38
42
 
39
43
  // Track pending loads to deduplicate concurrent requests
40
44
  private pendingKnownStateLoads = new Map<
@@ -153,7 +157,7 @@ export class StorageApiAsync implements StorageAPI {
153
157
  );
154
158
  }
155
159
 
156
- this.loadedCoValues.add(coValueRow.id);
160
+ this.inMemoryCoValues.add(coValueRow.id);
157
161
 
158
162
  let contentMessage = createContentMessage(coValueRow.id, coValueRow.header);
159
163
 
@@ -224,7 +228,7 @@ export class StorageApiAsync implements StorageAPI {
224
228
  done?.(true);
225
229
  }
226
230
 
227
- async pushContentWithDependencies(
231
+ private async pushContentWithDependencies(
228
232
  coValueRow: StoredCoValueRow,
229
233
  contentMessage: NewContentMessage,
230
234
  pushCallback: (data: NewContentMessage) => void,
@@ -237,7 +241,7 @@ export class StorageApiAsync implements StorageAPI {
237
241
  const promises = [];
238
242
 
239
243
  for (const dependedOnCoValue of dependedOnCoValuesList) {
240
- if (this.loadedCoValues.has(dependedOnCoValue)) {
244
+ if (this.inMemoryCoValues.has(dependedOnCoValue)) {
241
245
  continue;
242
246
  }
243
247
 
@@ -364,6 +368,8 @@ export class StorageApiAsync implements StorageAPI {
364
368
  });
365
369
  }
366
370
 
371
+ this.inMemoryCoValues.add(id);
372
+
367
373
  this.knownStates.handleUpdate(id, knownState);
368
374
 
369
375
  if (invalidAssumptions) {
@@ -460,7 +466,12 @@ export class StorageApiAsync implements StorageAPI {
460
466
  this.dbClient.stopTrackingSyncState(id);
461
467
  }
462
468
 
469
+ onCoValueUnmounted(id: RawCoID): void {
470
+ this.inMemoryCoValues.delete(id);
471
+ }
472
+
463
473
  close() {
474
+ this.inMemoryCoValues.clear();
464
475
  return this.storeQueue.close();
465
476
  }
466
477
  }
@@ -37,7 +37,11 @@ import { getPriorityFromHeader } from "../priority.js";
37
37
 
38
38
  export class StorageApiSync implements StorageAPI {
39
39
  private readonly dbClient: DBClientInterfaceSync;
40
- private loadedCoValues = new Set<RawCoID>();
40
+ /**
41
+ * Keeps track of CoValues that are in memory, to avoid reloading them from storage
42
+ * when it isn't necessary
43
+ */
44
+ private inMemoryCoValues = new Set<RawCoID>();
41
45
 
42
46
  /**
43
47
  * Queue for streaming content that will be pulled by SyncManager.
@@ -138,7 +142,7 @@ export class StorageApiSync implements StorageAPI {
138
142
  );
139
143
  }
140
144
 
141
- this.loadedCoValues.add(coValueRow.id);
145
+ this.inMemoryCoValues.add(coValueRow.id);
142
146
 
143
147
  const priority = getPriorityFromHeader(coValueRow.header);
144
148
  const contentMessage = createContentMessage(
@@ -244,7 +248,7 @@ export class StorageApiSync implements StorageAPI {
244
248
  });
245
249
  }
246
250
 
247
- async pushContentWithDependencies(
251
+ private async pushContentWithDependencies(
248
252
  coValueRow: StoredCoValueRow,
249
253
  contentMessage: NewContentMessage,
250
254
  pushCallback: (data: NewContentMessage) => void,
@@ -255,7 +259,7 @@ export class StorageApiSync implements StorageAPI {
255
259
  );
256
260
 
257
261
  for (const dependedOnCoValue of dependedOnCoValuesList) {
258
- if (this.loadedCoValues.has(dependedOnCoValue)) {
262
+ if (this.inMemoryCoValues.has(dependedOnCoValue)) {
259
263
  continue;
260
264
  }
261
265
 
@@ -353,6 +357,8 @@ export class StorageApiSync implements StorageAPI {
353
357
  });
354
358
  }
355
359
 
360
+ this.inMemoryCoValues.add(id);
361
+
356
362
  this.knownStates.handleUpdate(id, knownState);
357
363
 
358
364
  if (invalidAssumptions) {
@@ -450,7 +456,12 @@ export class StorageApiSync implements StorageAPI {
450
456
  this.dbClient.stopTrackingSyncState(id);
451
457
  }
452
458
 
459
+ onCoValueUnmounted(id: RawCoID): void {
460
+ this.inMemoryCoValues.delete(id);
461
+ }
462
+
453
463
  close() {
464
+ this.inMemoryCoValues.clear();
454
465
  return undefined;
455
466
  }
456
467
  }
@@ -67,6 +67,12 @@ export interface StorageAPI {
67
67
  callback: (knownState: CoValueKnownState | undefined) => void,
68
68
  ): void;
69
69
 
70
+ /**
71
+ * Called when a CoValue is unmounted from memory.
72
+ * Used to clean up the metadata associated with that CoValue.
73
+ */
74
+ onCoValueUnmounted(id: RawCoID): void;
75
+
70
76
  close(): Promise<unknown> | undefined;
71
77
  }
72
78
 
package/src/sync.ts CHANGED
@@ -102,6 +102,10 @@ export interface Peer {
102
102
  persistent?: boolean;
103
103
  }
104
104
 
105
+ function isPersistentServerPeer(peer: Peer | PeerState): boolean {
106
+ return peer.role === "server" && (peer.persistent ?? false);
107
+ }
108
+
105
109
  export type ServerPeerSelector = (
106
110
  id: RawCoID,
107
111
  serverPeers: PeerState[],
@@ -354,7 +358,7 @@ export class SyncManager {
354
358
  }
355
359
 
356
360
  startPeerReconciliation(peer: PeerState) {
357
- if (peer.role === "server" && peer.persistent) {
361
+ if (isPersistentServerPeer(peer)) {
358
362
  // Resume syncing unsynced CoValues asynchronously
359
363
  this.resumeUnsyncedCoValues().catch((error) => {
360
364
  logger.warn("Failed to resume unsynced CoValues:", error);
@@ -525,7 +529,7 @@ export class SyncManager {
525
529
 
526
530
  const unsubscribeFromKnownStatesUpdates =
527
531
  peerState.subscribeToKnownStatesUpdates((id, knownState) => {
528
- this.syncState.triggerUpdate(peer.id, id, knownState.value());
532
+ this.syncState.triggerUpdate(peer, id, knownState.value());
529
533
  });
530
534
 
531
535
  if (!skipReconciliation && peerState.role === "server") {
@@ -711,8 +715,8 @@ export class SyncManager {
711
715
 
712
716
  peer.combineWith(msg.id, knownStateFrom(msg));
713
717
 
714
- // The header is a boolean value that tells us if the other peer do have information about the header.
715
- // If it's false in this point it means that the coValue is unavailable on the other peer.
718
+ // The header is a boolean value that tells us if the other peer has information about the header.
719
+ // If it's false at this point it means that the coValue is unavailable on the other peer.
716
720
  const availableOnPeer = peer.getOptimisticKnownState(msg.id)?.header;
717
721
 
718
722
  if (!availableOnPeer) {
@@ -1076,6 +1080,18 @@ export class SyncManager {
1076
1080
  const isSyncRequired = this.local.syncWhen !== "never";
1077
1081
  if (isSyncRequired && peers.length === 0) {
1078
1082
  this.unsyncedTracker.add(coValueId);
1083
+
1084
+ // Mark CoValue as synced once a persistent server peer is added and
1085
+ // the CoValue is synced
1086
+ const unsubscribe = this.syncState.subscribeToCoValueUpdates(
1087
+ coValueId,
1088
+ (peer, _knownState, syncState) => {
1089
+ if (isPersistentServerPeer(peer) && syncState.uploaded) {
1090
+ this.unsyncedTracker.remove(coValueId);
1091
+ unsubscribe();
1092
+ }
1093
+ },
1094
+ );
1079
1095
  return;
1080
1096
  }
1081
1097
 
@@ -1128,6 +1144,19 @@ export class SyncManager {
1128
1144
  });
1129
1145
  }
1130
1146
 
1147
+ /**
1148
+ * Returns true if the local CoValue changes have been synced to all persistent server peers.
1149
+ *
1150
+ * Used during garbage collection to determine if the coValue is pending sync.
1151
+ */
1152
+ isSyncedToServerPeers(id: RawCoID): boolean {
1153
+ // If there are currently no server peers, go ahead with GC.
1154
+ // The CoValue will be reloaded into memory and synced when a peer is added.
1155
+ return this.getPersistentServerPeers(id).every((peer) =>
1156
+ this.syncState.isSynced(peer, id),
1157
+ );
1158
+ }
1159
+
1131
1160
  waitForSyncWithPeer(peerId: PeerID, id: RawCoID, timeout: number) {
1132
1161
  const peerState = this.peers[peerId];
1133
1162