cojson 0.19.22 → 0.20.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 (223) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +66 -0
  3. package/dist/PeerState.d.ts +6 -1
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +18 -3
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/coValueContentMessage.d.ts +0 -2
  8. package/dist/coValueContentMessage.d.ts.map +1 -1
  9. package/dist/coValueContentMessage.js +0 -8
  10. package/dist/coValueContentMessage.js.map +1 -1
  11. package/dist/coValueCore/SessionMap.d.ts +4 -2
  12. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  13. package/dist/coValueCore/SessionMap.js +30 -0
  14. package/dist/coValueCore/SessionMap.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +70 -5
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +302 -31
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +6 -1
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +9 -0
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/coList.d.ts +4 -2
  24. package/dist/coValues/coList.d.ts.map +1 -1
  25. package/dist/coValues/coList.js +3 -0
  26. package/dist/coValues/coList.js.map +1 -1
  27. package/dist/coValues/group.d.ts.map +1 -1
  28. package/dist/coValues/group.js +3 -6
  29. package/dist/coValues/group.js.map +1 -1
  30. package/dist/config.d.ts +2 -8
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +4 -12
  33. package/dist/config.js.map +1 -1
  34. package/dist/crypto/NapiCrypto.d.ts +1 -2
  35. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  36. package/dist/crypto/NapiCrypto.js +19 -4
  37. package/dist/crypto/NapiCrypto.js.map +1 -1
  38. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  39. package/dist/crypto/RNCrypto.js +19 -4
  40. package/dist/crypto/RNCrypto.js.map +1 -1
  41. package/dist/crypto/WasmCrypto.d.ts +11 -4
  42. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  43. package/dist/crypto/WasmCrypto.js +52 -10
  44. package/dist/crypto/WasmCrypto.js.map +1 -1
  45. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  46. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  47. package/dist/crypto/WasmCryptoEdge.js +4 -1
  48. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  49. package/dist/crypto/crypto.d.ts +3 -3
  50. package/dist/crypto/crypto.d.ts.map +1 -1
  51. package/dist/crypto/crypto.js +6 -1
  52. package/dist/crypto/crypto.js.map +1 -1
  53. package/dist/exports.d.ts +5 -5
  54. package/dist/exports.d.ts.map +1 -1
  55. package/dist/exports.js +4 -3
  56. package/dist/exports.js.map +1 -1
  57. package/dist/ids.d.ts +4 -1
  58. package/dist/ids.d.ts.map +1 -1
  59. package/dist/ids.js +4 -0
  60. package/dist/ids.js.map +1 -1
  61. package/dist/knownState.d.ts +2 -0
  62. package/dist/knownState.d.ts.map +1 -1
  63. package/dist/localNode.d.ts +12 -0
  64. package/dist/localNode.d.ts.map +1 -1
  65. package/dist/localNode.js +14 -0
  66. package/dist/localNode.js.map +1 -1
  67. package/dist/platformUtils.d.ts +3 -0
  68. package/dist/platformUtils.d.ts.map +1 -0
  69. package/dist/platformUtils.js +24 -0
  70. package/dist/platformUtils.js.map +1 -0
  71. package/dist/queue/LinkedList.d.ts +9 -3
  72. package/dist/queue/LinkedList.d.ts.map +1 -1
  73. package/dist/queue/LinkedList.js +30 -1
  74. package/dist/queue/LinkedList.js.map +1 -1
  75. package/dist/queue/OutgoingLoadQueue.d.ts +95 -0
  76. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -0
  77. package/dist/queue/OutgoingLoadQueue.js +240 -0
  78. package/dist/queue/OutgoingLoadQueue.js.map +1 -0
  79. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  80. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  81. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  82. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  83. package/dist/storage/sqlite/client.d.ts +3 -0
  84. package/dist/storage/sqlite/client.d.ts.map +1 -1
  85. package/dist/storage/sqlite/client.js +44 -0
  86. package/dist/storage/sqlite/client.js.map +1 -1
  87. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  88. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  89. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  90. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  91. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  92. package/dist/storage/sqliteAsync/client.js +42 -0
  93. package/dist/storage/sqliteAsync/client.js.map +1 -1
  94. package/dist/storage/storageAsync.d.ts +7 -0
  95. package/dist/storage/storageAsync.d.ts.map +1 -1
  96. package/dist/storage/storageAsync.js +48 -0
  97. package/dist/storage/storageAsync.js.map +1 -1
  98. package/dist/storage/storageSync.d.ts +6 -0
  99. package/dist/storage/storageSync.d.ts.map +1 -1
  100. package/dist/storage/storageSync.js +42 -0
  101. package/dist/storage/storageSync.js.map +1 -1
  102. package/dist/storage/types.d.ts +59 -0
  103. package/dist/storage/types.d.ts.map +1 -1
  104. package/dist/storage/types.js +12 -1
  105. package/dist/storage/types.js.map +1 -1
  106. package/dist/sync.d.ts.map +1 -1
  107. package/dist/sync.js +66 -43
  108. package/dist/sync.js.map +1 -1
  109. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  110. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  111. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  112. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  113. package/dist/tests/GarbageCollector.test.js +5 -6
  114. package/dist/tests/GarbageCollector.test.js.map +1 -1
  115. package/dist/tests/LinkedList.test.js +90 -0
  116. package/dist/tests/LinkedList.test.js.map +1 -1
  117. package/dist/tests/OutgoingLoadQueue.test.d.ts +2 -0
  118. package/dist/tests/OutgoingLoadQueue.test.d.ts.map +1 -0
  119. package/dist/tests/OutgoingLoadQueue.test.js +814 -0
  120. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -0
  121. package/dist/tests/StorageApiAsync.test.js +484 -152
  122. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  123. package/dist/tests/StorageApiSync.test.js +505 -136
  124. package/dist/tests/StorageApiSync.test.js.map +1 -1
  125. package/dist/tests/WasmCrypto.test.js +6 -3
  126. package/dist/tests/WasmCrypto.test.js.map +1 -1
  127. package/dist/tests/coValueCore.loadFromStorage.test.js +3 -0
  128. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  129. package/dist/tests/coValueCore.test.js +34 -13
  130. package/dist/tests/coValueCore.test.js.map +1 -1
  131. package/dist/tests/coreWasm.test.js +127 -4
  132. package/dist/tests/coreWasm.test.js.map +1 -1
  133. package/dist/tests/crypto.test.js +89 -93
  134. package/dist/tests/crypto.test.js.map +1 -1
  135. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  136. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  137. package/dist/tests/deleteCoValue.test.js +313 -0
  138. package/dist/tests/deleteCoValue.test.js.map +1 -0
  139. package/dist/tests/group.removeMember.test.js +18 -30
  140. package/dist/tests/group.removeMember.test.js.map +1 -1
  141. package/dist/tests/knownState.lazyLoading.test.js +3 -0
  142. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  143. package/dist/tests/sync.concurrentLoad.test.d.ts +2 -0
  144. package/dist/tests/sync.concurrentLoad.test.d.ts.map +1 -0
  145. package/dist/tests/sync.concurrentLoad.test.js +481 -0
  146. package/dist/tests/sync.concurrentLoad.test.js.map +1 -0
  147. package/dist/tests/sync.deleted.test.d.ts +2 -0
  148. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  149. package/dist/tests/sync.deleted.test.js +214 -0
  150. package/dist/tests/sync.deleted.test.js.map +1 -0
  151. package/dist/tests/sync.mesh.test.js +3 -2
  152. package/dist/tests/sync.mesh.test.js.map +1 -1
  153. package/dist/tests/sync.storage.test.js +4 -3
  154. package/dist/tests/sync.storage.test.js.map +1 -1
  155. package/dist/tests/sync.test.js +3 -2
  156. package/dist/tests/sync.test.js.map +1 -1
  157. package/dist/tests/testStorage.d.ts +3 -0
  158. package/dist/tests/testStorage.d.ts.map +1 -1
  159. package/dist/tests/testStorage.js +17 -1
  160. package/dist/tests/testStorage.js.map +1 -1
  161. package/dist/tests/testUtils.d.ts +7 -3
  162. package/dist/tests/testUtils.d.ts.map +1 -1
  163. package/dist/tests/testUtils.js +19 -4
  164. package/dist/tests/testUtils.js.map +1 -1
  165. package/package.json +6 -16
  166. package/src/PeerState.ts +26 -3
  167. package/src/coValueContentMessage.ts +0 -14
  168. package/src/coValueCore/SessionMap.ts +43 -1
  169. package/src/coValueCore/coValueCore.ts +415 -27
  170. package/src/coValueCore/verifiedState.ts +26 -3
  171. package/src/coValues/coList.ts +9 -3
  172. package/src/coValues/group.ts +5 -6
  173. package/src/config.ts +4 -13
  174. package/src/crypto/NapiCrypto.ts +29 -13
  175. package/src/crypto/RNCrypto.ts +29 -11
  176. package/src/crypto/WasmCrypto.ts +67 -20
  177. package/src/crypto/WasmCryptoEdge.ts +5 -1
  178. package/src/crypto/crypto.ts +16 -4
  179. package/src/exports.ts +4 -2
  180. package/src/ids.ts +11 -1
  181. package/src/localNode.ts +15 -0
  182. package/src/platformUtils.ts +26 -0
  183. package/src/queue/LinkedList.ts +34 -4
  184. package/src/queue/OutgoingLoadQueue.ts +307 -0
  185. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  186. package/src/storage/sqlite/client.ts +77 -0
  187. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  188. package/src/storage/sqliteAsync/client.ts +75 -0
  189. package/src/storage/storageAsync.ts +62 -0
  190. package/src/storage/storageSync.ts +58 -0
  191. package/src/storage/types.ts +69 -0
  192. package/src/sync.ts +78 -46
  193. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  194. package/src/tests/GarbageCollector.test.ts +6 -10
  195. package/src/tests/LinkedList.test.ts +111 -0
  196. package/src/tests/OutgoingLoadQueue.test.ts +1129 -0
  197. package/src/tests/StorageApiAsync.test.ts +572 -162
  198. package/src/tests/StorageApiSync.test.ts +580 -143
  199. package/src/tests/WasmCrypto.test.ts +8 -3
  200. package/src/tests/coValueCore.loadFromStorage.test.ts +6 -0
  201. package/src/tests/coValueCore.test.ts +49 -14
  202. package/src/tests/coreWasm.test.ts +319 -10
  203. package/src/tests/crypto.test.ts +141 -150
  204. package/src/tests/deleteCoValue.test.ts +528 -0
  205. package/src/tests/group.removeMember.test.ts +35 -35
  206. package/src/tests/knownState.lazyLoading.test.ts +6 -0
  207. package/src/tests/sync.concurrentLoad.test.ts +650 -0
  208. package/src/tests/sync.deleted.test.ts +294 -0
  209. package/src/tests/sync.mesh.test.ts +5 -2
  210. package/src/tests/sync.storage.test.ts +6 -3
  211. package/src/tests/sync.test.ts +5 -2
  212. package/src/tests/testStorage.ts +31 -2
  213. package/src/tests/testUtils.ts +31 -10
  214. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  215. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  216. package/dist/crypto/PureJSCrypto.js +0 -236
  217. package/dist/crypto/PureJSCrypto.js.map +0 -1
  218. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  219. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  220. package/dist/tests/PureJSCrypto.test.js +0 -145
  221. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  222. package/src/crypto/PureJSCrypto.ts +0 -429
  223. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -10,8 +10,9 @@ type Tuple<T, N extends number, A extends unknown[] = []> = A extends {
10
10
  ? A
11
11
  : Tuple<T, N, [...A, T]>;
12
12
  export type QueueTuple = Tuple<LinkedList<SyncMessage>, 3>;
13
- type LinkedListNode<T> = {
13
+ export type LinkedListNode<T> = {
14
14
  value: T;
15
+ prev: LinkedListNode<T> | undefined;
15
16
  next: LinkedListNode<T> | undefined;
16
17
  };
17
18
  /**
@@ -26,13 +27,14 @@ export class LinkedList<T> {
26
27
  tail: LinkedListNode<T> | undefined = undefined;
27
28
  length = 0;
28
29
 
29
- push(value: T) {
30
- const node = { value, next: undefined };
30
+ push(value: T): LinkedListNode<T> {
31
+ const node: LinkedListNode<T> = { value, prev: undefined, next: undefined };
31
32
 
32
33
  if (this.head === undefined) {
33
34
  this.head = node;
34
35
  this.tail = node;
35
36
  } else if (this.tail) {
37
+ node.prev = this.tail;
36
38
  this.tail.next = node;
37
39
  this.tail = node;
38
40
  } else {
@@ -41,6 +43,7 @@ export class LinkedList<T> {
41
43
 
42
44
  this.length++;
43
45
  this.meter?.push();
46
+ return node;
44
47
  }
45
48
 
46
49
  shift() {
@@ -55,6 +58,8 @@ export class LinkedList<T> {
55
58
 
56
59
  if (this.head === undefined) {
57
60
  this.tail = undefined;
61
+ } else {
62
+ this.head.prev = undefined;
58
63
  }
59
64
 
60
65
  this.length--;
@@ -63,6 +68,31 @@ export class LinkedList<T> {
63
68
  return value;
64
69
  }
65
70
 
71
+ /**
72
+ * Remove a specific node from the list in O(1) time.
73
+ * The node must be a valid node that was returned by push().
74
+ */
75
+ remove(node: LinkedListNode<T>): void {
76
+ if (node.prev) {
77
+ node.prev.next = node.next;
78
+ } else {
79
+ // Node is the head
80
+ this.head = node.next;
81
+ }
82
+
83
+ if (node.next) {
84
+ node.next.prev = node.prev;
85
+ } else {
86
+ // Node is the tail
87
+ this.tail = node.prev;
88
+ }
89
+
90
+ node.prev = undefined;
91
+ node.next = undefined;
92
+ this.length--;
93
+ this.meter?.pull();
94
+ }
95
+
66
96
  isEmpty() {
67
97
  return this.head === undefined;
68
98
  }
@@ -108,7 +138,7 @@ class QueueMeter {
108
138
  }
109
139
  }
110
140
  export function meteredList<T>(
111
- type: "incoming" | "outgoing" | "storage-streaming",
141
+ type: "incoming" | "outgoing" | "storage-streaming" | "load-requests-queue",
112
142
  attrs?: Record<string, string | number>,
113
143
  ) {
114
144
  return new LinkedList<T>(new QueueMeter("jazz.messagequeue." + type, attrs));
@@ -0,0 +1,307 @@
1
+ import { CO_VALUE_LOADING_CONFIG } from "../config.js";
2
+ import { CoValueCore } from "../exports.js";
3
+ import type { RawCoID } from "../ids.js";
4
+ import { logger } from "../logger.js";
5
+ import type { PeerID } from "../sync.js";
6
+ import { LinkedList, type LinkedListNode, meteredList } from "./LinkedList.js";
7
+
8
+ interface PendingLoad {
9
+ value: CoValueCore;
10
+ sendCallback: () => void;
11
+ }
12
+
13
+ /**
14
+ * Mode for enqueuing load requests:
15
+ * - "high-priority" (default): high priority, processed in order
16
+ * - "low-priority": processed after all high priority requests
17
+ * - "immediate": bypasses the queue entirely, executes immediately
18
+ */
19
+ export type LoadMode = "low-priority" | "immediate" | "high-priority";
20
+
21
+ /**
22
+ * A queue that manages outgoing load requests with throttling.
23
+ *
24
+ * Features:
25
+ * - Limits concurrent in-flight load requests per peer
26
+ * - FIFO order for pending requests
27
+ * - O(1) enqueue and dequeue operations using LinkedList
28
+ * - Manages timeouts for in-flight loads with a single timer
29
+ */
30
+ export class OutgoingLoadQueue {
31
+ private inFlightLoads: Map<CoValueCore, number> = new Map();
32
+ private highPriorityPending: LinkedList<PendingLoad> = meteredList(
33
+ "load-requests-queue",
34
+ { priority: "high" },
35
+ );
36
+ private lowPriorityPending: LinkedList<PendingLoad> = meteredList(
37
+ "load-requests-queue",
38
+ { priority: "low" },
39
+ );
40
+ /**
41
+ * Tracks nodes in the low-priority queue by CoValue ID for O(1) upgrade lookup.
42
+ */
43
+ private lowPriorityNodes: Map<RawCoID, LinkedListNode<PendingLoad>> =
44
+ new Map();
45
+ /**
46
+ * Tracks nodes in the high-priority queue by CoValue ID for O(1) immediate mode lookup.
47
+ */
48
+ private highPriorityNodes: Map<RawCoID, LinkedListNode<PendingLoad>> =
49
+ new Map();
50
+ private timeoutHandle: ReturnType<typeof setTimeout> | null = null;
51
+
52
+ constructor(private peerId: PeerID) {}
53
+
54
+ /**
55
+ * Check if we can send another load request.
56
+ */
57
+ private canSend(): boolean {
58
+ return (
59
+ this.inFlightLoads.size <
60
+ CO_VALUE_LOADING_CONFIG.MAX_IN_FLIGHT_LOADS_PER_PEER
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Track that a load request has been sent.
66
+ */
67
+ private trackSent(coValue: CoValueCore): void {
68
+ const now = performance.now();
69
+ this.inFlightLoads.set(coValue, now);
70
+ this.scheduleTimeoutCheck(CO_VALUE_LOADING_CONFIG.TIMEOUT);
71
+ }
72
+
73
+ /**
74
+ * Schedule a timeout check if not already scheduled.
75
+ * Uses a single timer to check all in-flight loads.
76
+ */
77
+ private scheduleTimeoutCheck(nextTimeout: number): void {
78
+ if (this.timeoutHandle !== null) {
79
+ return;
80
+ }
81
+
82
+ this.timeoutHandle = setTimeout(() => {
83
+ this.timeoutHandle = null;
84
+ this.checkTimeouts();
85
+ }, nextTimeout);
86
+ }
87
+
88
+ /**
89
+ * Check all in-flight loads for timeouts and handle them.
90
+ */
91
+ private checkTimeouts(): void {
92
+ const now = performance.now();
93
+
94
+ let nextTimeout: number | undefined;
95
+ for (const [coValue, sentAt] of this.inFlightLoads.entries()) {
96
+ const timeout = sentAt + CO_VALUE_LOADING_CONFIG.TIMEOUT;
97
+
98
+ if (now >= timeout) {
99
+ if (!coValue.isAvailable()) {
100
+ logger.warn("Load request timed out", {
101
+ id: coValue.id,
102
+ peerId: this.peerId,
103
+ });
104
+ coValue.markNotFoundInPeer(this.peerId);
105
+ } else if (coValue.isStreaming()) {
106
+ logger.warn(
107
+ "Content streaming is taking more than " +
108
+ CO_VALUE_LOADING_CONFIG.TIMEOUT / 1000 +
109
+ "s",
110
+ {
111
+ id: coValue.id,
112
+ peerId: this.peerId,
113
+ knownState: coValue.knownState().sessions,
114
+ streamingTarget: coValue.knownStateWithStreaming().sessions,
115
+ },
116
+ );
117
+ }
118
+
119
+ this.inFlightLoads.delete(coValue);
120
+ this.processQueue();
121
+ } else {
122
+ nextTimeout = Math.min(nextTimeout ?? Infinity, timeout - now);
123
+ }
124
+ }
125
+
126
+ // Reschedule if there are still in-flight loads
127
+ if (nextTimeout) {
128
+ this.scheduleTimeoutCheck(nextTimeout);
129
+ }
130
+ }
131
+
132
+ trackUpdate(coValue: CoValueCore): void {
133
+ if (!this.inFlightLoads.has(coValue)) {
134
+ return;
135
+ }
136
+
137
+ // Refresh the timeout for the in-flight load
138
+ this.inFlightLoads.set(coValue, performance.now());
139
+ }
140
+
141
+ /**
142
+ * Track that a load request has completed.
143
+ * Triggers processing of pending requests.
144
+ */
145
+ trackComplete(coValue: CoValueCore): void {
146
+ if (!this.inFlightLoads.has(coValue)) {
147
+ return;
148
+ }
149
+
150
+ if (coValue.isStreaming()) {
151
+ // wait for the next chunk
152
+ return;
153
+ }
154
+
155
+ this.inFlightLoads.delete(coValue);
156
+ this.processQueue();
157
+ }
158
+
159
+ /**
160
+ * Enqueue a load request.
161
+ * Immediately processes the queue to send requests if capacity is available.
162
+ * Skips CoValues that are already in-flight or pending.
163
+ *
164
+ * @param coValue - The CoValue to load
165
+ * @param sendCallback - Callback to send the request when ready
166
+ * @param mode - Optional mode: "low-priority" for background loads, "immediate" to bypass queue
167
+ */
168
+ enqueue(
169
+ value: CoValueCore,
170
+ sendCallback: () => void,
171
+ mode: LoadMode = "high-priority",
172
+ ): void {
173
+ if (this.inFlightLoads.has(value)) {
174
+ return;
175
+ }
176
+
177
+ const lowPriorityNode = this.lowPriorityNodes.get(value.id);
178
+ const highPriorityNode = this.highPriorityNodes.get(value.id);
179
+
180
+ switch (mode) {
181
+ case "immediate":
182
+ // Upgrade any low-priority or high-priority requests to immediate priority
183
+ if (lowPriorityNode) {
184
+ this.lowPriorityPending.remove(lowPriorityNode);
185
+ this.lowPriorityNodes.delete(value.id);
186
+ }
187
+ if (highPriorityNode) {
188
+ this.highPriorityPending.remove(highPriorityNode);
189
+ this.highPriorityNodes.delete(value.id);
190
+ }
191
+
192
+ this.trackSent(value);
193
+ sendCallback();
194
+ break;
195
+ case "high-priority":
196
+ if (highPriorityNode) {
197
+ return;
198
+ }
199
+
200
+ // Upgrade any low-priority requests to high-priority
201
+ if (lowPriorityNode) {
202
+ this.lowPriorityPending.remove(lowPriorityNode);
203
+ this.lowPriorityNodes.delete(value.id);
204
+ }
205
+
206
+ this.highPriorityNodes.set(
207
+ value.id,
208
+ this.highPriorityPending.push({ value, sendCallback }),
209
+ );
210
+ this.processQueue();
211
+ break;
212
+ case "low-priority":
213
+ if (lowPriorityNode || highPriorityNode) {
214
+ return;
215
+ }
216
+
217
+ this.lowPriorityNodes.set(
218
+ value.id,
219
+ this.lowPriorityPending.push({ value, sendCallback }),
220
+ );
221
+ this.processQueue();
222
+ break;
223
+ }
224
+ }
225
+
226
+ private processing = false;
227
+ /**
228
+ * Process all pending load requests while capacity is available.
229
+ * High-priority requests are processed first, then low-priority.
230
+ */
231
+ private processQueue(): void {
232
+ if (this.processing || !this.canSend()) {
233
+ return;
234
+ }
235
+ this.processing = true;
236
+
237
+ while (this.canSend()) {
238
+ // Try high-priority first
239
+ let next = this.highPriorityPending.shift();
240
+
241
+ if (next) {
242
+ // Remove from the tracking map since we're processing it
243
+ this.highPriorityNodes.delete(next.value.id);
244
+ } else {
245
+ // Fall back to low-priority if high-priority is empty
246
+ next = this.lowPriorityPending.shift();
247
+ if (next) {
248
+ // Remove from the tracking map since we're processing it
249
+ this.lowPriorityNodes.delete(next.value.id);
250
+ }
251
+ }
252
+
253
+ if (!next) {
254
+ break;
255
+ }
256
+
257
+ this.trackSent(next.value);
258
+ next.sendCallback();
259
+ }
260
+
261
+ this.processing = false;
262
+ }
263
+
264
+ /**
265
+ * Clear all state. Called on disconnect.
266
+ * Clears the timeout and all pending/in-flight loads.
267
+ */
268
+ clear(): void {
269
+ if (this.timeoutHandle !== null) {
270
+ clearTimeout(this.timeoutHandle);
271
+ this.timeoutHandle = null;
272
+ }
273
+ this.inFlightLoads.clear();
274
+ this.highPriorityPending = new LinkedList();
275
+ this.lowPriorityPending = new LinkedList();
276
+ this.highPriorityNodes.clear();
277
+ this.lowPriorityNodes.clear();
278
+ }
279
+
280
+ /**
281
+ * Get the number of in-flight loads (for testing/debugging).
282
+ */
283
+ get inFlightCount(): number {
284
+ return this.inFlightLoads.size;
285
+ }
286
+
287
+ /**
288
+ * Get the number of pending loads (for testing/debugging).
289
+ */
290
+ get pendingCount(): number {
291
+ return this.highPriorityPending.length + this.lowPriorityPending.length;
292
+ }
293
+
294
+ /**
295
+ * Get the number of high-priority pending loads (for testing/debugging).
296
+ */
297
+ get highPriorityPendingCount(): number {
298
+ return this.highPriorityPending.length;
299
+ }
300
+
301
+ /**
302
+ * Get the number of low-priority pending loads (for testing/debugging).
303
+ */
304
+ get lowPriorityPendingCount(): number {
305
+ return this.lowPriorityPending.length;
306
+ }
307
+ }
@@ -0,0 +1,124 @@
1
+ import { logger } from "../logger.js";
2
+
3
+ export type DeletedCoValuesEraserSchedulerRunResult = {
4
+ hasMore: boolean;
5
+ };
6
+
7
+ export type DeletedCoValuesEraserSchedulerOpts = {
8
+ throttleMs: number;
9
+ startupDelayMs: number;
10
+ followUpDelayMs: number;
11
+ };
12
+
13
+ type SchedulerState =
14
+ | "idle"
15
+ | "startup_scheduled"
16
+ | "throttle_scheduled"
17
+ | "followup_scheduled"
18
+ | "running"
19
+ | "disposed";
20
+
21
+ export const DEFAULT_DELETE_SCHEDULE_OPTS = {
22
+ throttleMs: 60_000,
23
+ startupDelayMs: 1_000,
24
+ followUpDelayMs: 1_000,
25
+ } satisfies DeletedCoValuesEraserSchedulerOpts;
26
+
27
+ export class DeletedCoValuesEraserScheduler {
28
+ private readonly runCallback: () => Promise<DeletedCoValuesEraserSchedulerRunResult>;
29
+ private readonly opts: DeletedCoValuesEraserSchedulerOpts;
30
+
31
+ private state: SchedulerState = "idle";
32
+
33
+ private isDisposed(): boolean {
34
+ return this.state === "disposed";
35
+ }
36
+
37
+ private scheduledTimeout: ReturnType<typeof setTimeout> | undefined;
38
+
39
+ constructor({
40
+ run,
41
+ opts,
42
+ }: {
43
+ run: () => Promise<DeletedCoValuesEraserSchedulerRunResult>;
44
+ opts?: DeletedCoValuesEraserSchedulerOpts;
45
+ }) {
46
+ this.runCallback = run;
47
+ this.opts = opts || DEFAULT_DELETE_SCHEDULE_OPTS;
48
+ }
49
+
50
+ scheduleStartupDrain() {
51
+ if (this.isDisposed()) return;
52
+
53
+ // Only schedule startup drain if nothing is already scheduled/running.
54
+ if (this.state !== "idle") return;
55
+
56
+ this.scheduleTimer("startup_scheduled", this.opts.startupDelayMs);
57
+ }
58
+
59
+ onEnqueueDeletedCoValue() {
60
+ if (this.isDisposed()) return;
61
+
62
+ // While we're already draining (or have a follow-up scheduled), ignore enqueue
63
+ // to avoid overlapping phases. The active drain loop will pick up new work.
64
+ if (this.state !== "idle") return;
65
+
66
+ // Only idle reaches here.
67
+ this.scheduleTimer("throttle_scheduled", this.opts.throttleMs);
68
+ }
69
+
70
+ dispose() {
71
+ if (this.isDisposed()) return;
72
+ this.state = "disposed";
73
+
74
+ if (this.scheduledTimeout) clearTimeout(this.scheduledTimeout);
75
+ this.scheduledTimeout = undefined;
76
+ }
77
+
78
+ private scheduleTimer(
79
+ state: Exclude<SchedulerState, "idle" | "running" | "disposed">,
80
+ delayMs: number,
81
+ ) {
82
+ if (this.isDisposed()) return;
83
+ if (this.scheduledTimeout) return;
84
+
85
+ this.state = state;
86
+ this.scheduledTimeout = setTimeout(() => {
87
+ this.scheduledTimeout = undefined;
88
+ void this.run();
89
+ }, delayMs);
90
+ }
91
+
92
+ private async run() {
93
+ if (this.isDisposed()) return;
94
+
95
+ // Clear any pre-run scheduled state and enter running state.
96
+ this.state = "running";
97
+
98
+ let result: DeletedCoValuesEraserSchedulerRunResult;
99
+ try {
100
+ result = await this.runCallback();
101
+ } catch (error) {
102
+ logger.error("Error running deleted co values eraser scheduler", {
103
+ err: error,
104
+ });
105
+ // If the run callback fails, recover to idle so future enqueues/startup drains
106
+ // can retry instead of getting stuck in "running".
107
+ if (!this.isDisposed()) {
108
+ this.state = "idle";
109
+ }
110
+ return;
111
+ }
112
+
113
+ if (this.isDisposed()) return;
114
+
115
+ if (result.hasMore) {
116
+ // One follow-up phase at a time. Further enqueues while follow-up is scheduled
117
+ // are ignored.
118
+ this.scheduleTimer("followup_scheduled", this.opts.followUpDelayMs);
119
+ return;
120
+ }
121
+
122
+ this.state = "idle";
123
+ }
124
+ }
@@ -16,6 +16,7 @@ import type {
16
16
  StoredSessionRow,
17
17
  TransactionRow,
18
18
  } from "../types.js";
19
+ import { DeletedCoValueDeletionStatus } from "../types.js";
19
20
  import type { SQLiteDatabaseDriver } from "./types.js";
20
21
 
21
22
  export type RawCoValueRow = {
@@ -29,6 +30,10 @@ export type RawTransactionRow = {
29
30
  tx: string;
30
31
  };
31
32
 
33
+ type DeletedCoValueQueueRow = {
34
+ id: RawCoID;
35
+ };
36
+
32
37
  export function getErrorMessage(error: unknown) {
33
38
  return error instanceof Error ? error.message : "Unknown error";
34
39
  }
@@ -143,6 +148,78 @@ export class SQLiteClient
143
148
  return result.rowID;
144
149
  }
145
150
 
151
+ markCoValueAsDeleted(id: RawCoID) {
152
+ // Work queue entry. Table only stores the coValueID.
153
+ // Idempotent by design.
154
+ this.db.run(
155
+ `INSERT INTO deletedCoValues (coValueID) VALUES (?) ON CONFLICT(coValueID) DO NOTHING`,
156
+ [id],
157
+ );
158
+ }
159
+
160
+ eraseCoValueButKeepTombstone(coValueId: RawCoID) {
161
+ const coValueRow = this.db.get<{ rowID: number }>(
162
+ "SELECT rowID FROM coValues WHERE id = ?",
163
+ [coValueId],
164
+ );
165
+
166
+ if (!coValueRow) {
167
+ logger.warn(`CoValue ${coValueId} not found, skipping deletion`);
168
+ return;
169
+ }
170
+
171
+ this.transaction(() => {
172
+ this.db.run(
173
+ `DELETE FROM transactions
174
+ WHERE ses IN (
175
+ SELECT rowID FROM sessions
176
+ WHERE coValue = ?
177
+ AND sessionID NOT LIKE '%$'
178
+ )`,
179
+ [coValueRow.rowID],
180
+ );
181
+
182
+ this.db.run(
183
+ `DELETE FROM signatureAfter
184
+ WHERE ses IN (
185
+ SELECT rowID FROM sessions
186
+ WHERE coValue = ?
187
+ AND sessionID NOT LIKE '%$'
188
+ )`,
189
+ [coValueRow.rowID],
190
+ );
191
+
192
+ this.db.run(
193
+ `DELETE FROM sessions
194
+ WHERE coValue = ?
195
+ AND sessionID NOT LIKE '%$'`,
196
+ [coValueRow.rowID],
197
+ );
198
+
199
+ // Mark the delete as done
200
+ this.db.run(
201
+ `INSERT INTO deletedCoValues (coValueID, status) VALUES (?, ?)
202
+ ON CONFLICT(coValueID) DO UPDATE SET status=?`,
203
+ [
204
+ coValueId,
205
+ DeletedCoValueDeletionStatus.Done,
206
+ DeletedCoValueDeletionStatus.Done,
207
+ ],
208
+ );
209
+ });
210
+ }
211
+
212
+ getAllCoValuesWaitingForDelete(): RawCoID[] {
213
+ return this.db
214
+ .query<DeletedCoValueQueueRow>(
215
+ `SELECT coValueID as id
216
+ FROM deletedCoValues
217
+ WHERE status = ?`,
218
+ [DeletedCoValueDeletionStatus.Pending],
219
+ )
220
+ .map((r) => r.id);
221
+ }
222
+
146
223
  addSessionUpdate({ sessionUpdate }: { sessionUpdate: SessionRow }): number {
147
224
  const result = this.db.get<{ rowID: number }>(
148
225
  `INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
@@ -40,6 +40,13 @@ export const migrations: Record<number, string[]> = {
40
40
  );`,
41
41
  "CREATE INDEX IF NOT EXISTS idx_unsynced_covalues_co_value_id ON unsynced_covalues(co_value_id);",
42
42
  ],
43
+ 5: [
44
+ `CREATE TABLE IF NOT EXISTS deletedCoValues (
45
+ coValueID TEXT PRIMARY KEY,
46
+ status INTEGER NOT NULL DEFAULT 0
47
+ ) WITHOUT ROWID;`,
48
+ "CREATE INDEX IF NOT EXISTS deletedCoValuesByStatus ON deletedCoValues (status);",
49
+ ],
43
50
  };
44
51
 
45
52
  type Migration = {