@optimystic/db-p2p 0.0.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 (189) hide show
  1. package/dist/index.min.js +52 -0
  2. package/dist/index.min.js.map +7 -0
  3. package/dist/src/cluster/client.d.ts +12 -0
  4. package/dist/src/cluster/client.d.ts.map +1 -0
  5. package/dist/src/cluster/client.js +65 -0
  6. package/dist/src/cluster/client.js.map +1 -0
  7. package/dist/src/cluster/cluster-repo.d.ts +79 -0
  8. package/dist/src/cluster/cluster-repo.d.ts.map +1 -0
  9. package/dist/src/cluster/cluster-repo.js +613 -0
  10. package/dist/src/cluster/cluster-repo.js.map +1 -0
  11. package/dist/src/cluster/partition-detector.d.ts +59 -0
  12. package/dist/src/cluster/partition-detector.d.ts.map +1 -0
  13. package/dist/src/cluster/partition-detector.js +129 -0
  14. package/dist/src/cluster/partition-detector.js.map +1 -0
  15. package/dist/src/cluster/service.d.ts +49 -0
  16. package/dist/src/cluster/service.d.ts.map +1 -0
  17. package/dist/src/cluster/service.js +107 -0
  18. package/dist/src/cluster/service.js.map +1 -0
  19. package/dist/src/index.d.ts +29 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +29 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/it-utility.d.ts +4 -0
  24. package/dist/src/it-utility.d.ts.map +1 -0
  25. package/dist/src/it-utility.js +32 -0
  26. package/dist/src/it-utility.js.map +1 -0
  27. package/dist/src/libp2p-key-network.d.ts +59 -0
  28. package/dist/src/libp2p-key-network.d.ts.map +1 -0
  29. package/dist/src/libp2p-key-network.js +278 -0
  30. package/dist/src/libp2p-key-network.js.map +1 -0
  31. package/dist/src/libp2p-node.d.ts +28 -0
  32. package/dist/src/libp2p-node.d.ts.map +1 -0
  33. package/dist/src/libp2p-node.js +270 -0
  34. package/dist/src/libp2p-node.js.map +1 -0
  35. package/dist/src/logger.d.ts +3 -0
  36. package/dist/src/logger.d.ts.map +1 -0
  37. package/dist/src/logger.js +6 -0
  38. package/dist/src/logger.js.map +1 -0
  39. package/dist/src/network/get-network-manager.d.ts +4 -0
  40. package/dist/src/network/get-network-manager.d.ts.map +1 -0
  41. package/dist/src/network/get-network-manager.js +17 -0
  42. package/dist/src/network/get-network-manager.js.map +1 -0
  43. package/dist/src/network/network-manager-service.d.ts +82 -0
  44. package/dist/src/network/network-manager-service.d.ts.map +1 -0
  45. package/dist/src/network/network-manager-service.js +283 -0
  46. package/dist/src/network/network-manager-service.js.map +1 -0
  47. package/dist/src/peer-utils.d.ts +2 -0
  48. package/dist/src/peer-utils.d.ts.map +1 -0
  49. package/dist/src/peer-utils.js +28 -0
  50. package/dist/src/peer-utils.js.map +1 -0
  51. package/dist/src/protocol-client.d.ts +12 -0
  52. package/dist/src/protocol-client.d.ts.map +1 -0
  53. package/dist/src/protocol-client.js +34 -0
  54. package/dist/src/protocol-client.js.map +1 -0
  55. package/dist/src/repo/client.d.ts +17 -0
  56. package/dist/src/repo/client.d.ts.map +1 -0
  57. package/dist/src/repo/client.js +82 -0
  58. package/dist/src/repo/client.js.map +1 -0
  59. package/dist/src/repo/cluster-coordinator.d.ts +59 -0
  60. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -0
  61. package/dist/src/repo/cluster-coordinator.js +539 -0
  62. package/dist/src/repo/cluster-coordinator.js.map +1 -0
  63. package/dist/src/repo/coordinator-repo.d.ts +29 -0
  64. package/dist/src/repo/coordinator-repo.d.ts.map +1 -0
  65. package/dist/src/repo/coordinator-repo.js +102 -0
  66. package/dist/src/repo/coordinator-repo.js.map +1 -0
  67. package/dist/src/repo/redirect.d.ts +14 -0
  68. package/dist/src/repo/redirect.d.ts.map +1 -0
  69. package/dist/src/repo/redirect.js +9 -0
  70. package/dist/src/repo/redirect.js.map +1 -0
  71. package/dist/src/repo/service.d.ts +52 -0
  72. package/dist/src/repo/service.d.ts.map +1 -0
  73. package/dist/src/repo/service.js +181 -0
  74. package/dist/src/repo/service.js.map +1 -0
  75. package/dist/src/repo/types.d.ts +7 -0
  76. package/dist/src/repo/types.d.ts.map +1 -0
  77. package/dist/src/repo/types.js +2 -0
  78. package/dist/src/repo/types.js.map +1 -0
  79. package/dist/src/routing/libp2p-known-peers.d.ts +4 -0
  80. package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -0
  81. package/dist/src/routing/libp2p-known-peers.js +19 -0
  82. package/dist/src/routing/libp2p-known-peers.js.map +1 -0
  83. package/dist/src/routing/responsibility.d.ts +14 -0
  84. package/dist/src/routing/responsibility.d.ts.map +1 -0
  85. package/dist/src/routing/responsibility.js +45 -0
  86. package/dist/src/routing/responsibility.js.map +1 -0
  87. package/dist/src/routing/simple-cluster-coordinator.d.ts +23 -0
  88. package/dist/src/routing/simple-cluster-coordinator.d.ts.map +1 -0
  89. package/dist/src/routing/simple-cluster-coordinator.js +59 -0
  90. package/dist/src/routing/simple-cluster-coordinator.js.map +1 -0
  91. package/dist/src/storage/arachnode-fret-adapter.d.ts +65 -0
  92. package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -0
  93. package/dist/src/storage/arachnode-fret-adapter.js +93 -0
  94. package/dist/src/storage/arachnode-fret-adapter.js.map +1 -0
  95. package/dist/src/storage/block-storage.d.ts +31 -0
  96. package/dist/src/storage/block-storage.d.ts.map +1 -0
  97. package/dist/src/storage/block-storage.js +154 -0
  98. package/dist/src/storage/block-storage.js.map +1 -0
  99. package/dist/src/storage/file-storage.d.ts +30 -0
  100. package/dist/src/storage/file-storage.d.ts.map +1 -0
  101. package/dist/src/storage/file-storage.js +127 -0
  102. package/dist/src/storage/file-storage.js.map +1 -0
  103. package/dist/src/storage/helpers.d.ts +3 -0
  104. package/dist/src/storage/helpers.d.ts.map +1 -0
  105. package/dist/src/storage/helpers.js +28 -0
  106. package/dist/src/storage/helpers.js.map +1 -0
  107. package/dist/src/storage/i-block-storage.d.ts +32 -0
  108. package/dist/src/storage/i-block-storage.d.ts.map +1 -0
  109. package/dist/src/storage/i-block-storage.js +2 -0
  110. package/dist/src/storage/i-block-storage.js.map +1 -0
  111. package/dist/src/storage/i-raw-storage.d.ts +20 -0
  112. package/dist/src/storage/i-raw-storage.d.ts.map +1 -0
  113. package/dist/src/storage/i-raw-storage.js +2 -0
  114. package/dist/src/storage/i-raw-storage.js.map +1 -0
  115. package/dist/src/storage/memory-storage.d.ts +27 -0
  116. package/dist/src/storage/memory-storage.d.ts.map +1 -0
  117. package/dist/src/storage/memory-storage.js +87 -0
  118. package/dist/src/storage/memory-storage.js.map +1 -0
  119. package/dist/src/storage/restoration-coordinator-v2.d.ts +63 -0
  120. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -0
  121. package/dist/src/storage/restoration-coordinator-v2.js +157 -0
  122. package/dist/src/storage/restoration-coordinator-v2.js.map +1 -0
  123. package/dist/src/storage/ring-selector.d.ts +56 -0
  124. package/dist/src/storage/ring-selector.d.ts.map +1 -0
  125. package/dist/src/storage/ring-selector.js +118 -0
  126. package/dist/src/storage/ring-selector.js.map +1 -0
  127. package/dist/src/storage/storage-monitor.d.ts +23 -0
  128. package/dist/src/storage/storage-monitor.d.ts.map +1 -0
  129. package/dist/src/storage/storage-monitor.js +40 -0
  130. package/dist/src/storage/storage-monitor.js.map +1 -0
  131. package/dist/src/storage/storage-repo.d.ts +17 -0
  132. package/dist/src/storage/storage-repo.d.ts.map +1 -0
  133. package/dist/src/storage/storage-repo.js +267 -0
  134. package/dist/src/storage/storage-repo.js.map +1 -0
  135. package/dist/src/storage/struct.d.ts +29 -0
  136. package/dist/src/storage/struct.d.ts.map +1 -0
  137. package/dist/src/storage/struct.js +2 -0
  138. package/dist/src/storage/struct.js.map +1 -0
  139. package/dist/src/sync/client.d.ts +27 -0
  140. package/dist/src/sync/client.d.ts.map +1 -0
  141. package/dist/src/sync/client.js +32 -0
  142. package/dist/src/sync/client.js.map +1 -0
  143. package/dist/src/sync/protocol.d.ts +58 -0
  144. package/dist/src/sync/protocol.d.ts.map +1 -0
  145. package/dist/src/sync/protocol.js +12 -0
  146. package/dist/src/sync/protocol.js.map +1 -0
  147. package/dist/src/sync/service.d.ts +62 -0
  148. package/dist/src/sync/service.d.ts.map +1 -0
  149. package/dist/src/sync/service.js +168 -0
  150. package/dist/src/sync/service.js.map +1 -0
  151. package/package.json +73 -0
  152. package/readme.md +497 -0
  153. package/src/cluster/client.ts +63 -0
  154. package/src/cluster/cluster-repo.ts +711 -0
  155. package/src/cluster/partition-detector.ts +158 -0
  156. package/src/cluster/service.ts +156 -0
  157. package/src/index.ts +30 -0
  158. package/src/it-utility.ts +36 -0
  159. package/src/libp2p-key-network.ts +334 -0
  160. package/src/libp2p-node.ts +335 -0
  161. package/src/logger.ts +9 -0
  162. package/src/network/get-network-manager.ts +17 -0
  163. package/src/network/network-manager-service.ts +334 -0
  164. package/src/peer-utils.ts +24 -0
  165. package/src/protocol-client.ts +54 -0
  166. package/src/repo/client.ts +112 -0
  167. package/src/repo/cluster-coordinator.ts +592 -0
  168. package/src/repo/coordinator-repo.ts +137 -0
  169. package/src/repo/redirect.ts +17 -0
  170. package/src/repo/service.ts +219 -0
  171. package/src/repo/types.ts +7 -0
  172. package/src/routing/libp2p-known-peers.ts +26 -0
  173. package/src/routing/responsibility.ts +63 -0
  174. package/src/routing/simple-cluster-coordinator.ts +70 -0
  175. package/src/storage/arachnode-fret-adapter.ts +128 -0
  176. package/src/storage/block-storage.ts +182 -0
  177. package/src/storage/file-storage.ts +163 -0
  178. package/src/storage/helpers.ts +29 -0
  179. package/src/storage/i-block-storage.ts +40 -0
  180. package/src/storage/i-raw-storage.ts +30 -0
  181. package/src/storage/memory-storage.ts +108 -0
  182. package/src/storage/restoration-coordinator-v2.ts +191 -0
  183. package/src/storage/ring-selector.ts +155 -0
  184. package/src/storage/storage-monitor.ts +59 -0
  185. package/src/storage/storage-repo.ts +320 -0
  186. package/src/storage/struct.ts +34 -0
  187. package/src/sync/client.ts +42 -0
  188. package/src/sync/protocol.ts +71 -0
  189. package/src/sync/service.ts +229 -0
@@ -0,0 +1,320 @@
1
+ import type {
2
+ IRepo, MessageOptions, BlockId, CommitRequest, CommitResult, GetBlockResults, PendRequest, PendResult, ActionBlocks,
3
+ ActionId, BlockGets, ActionPending, PendSuccess, ActionTransform, ActionTransforms,
4
+ Transforms,
5
+ GetBlockResult,
6
+ PendValidationHook
7
+ } from "@optimystic/db-core";
8
+ import {
9
+ Latches, transformForBlockId, applyTransform, groupBy, concatTransform, emptyTransforms,
10
+ blockIdsForTransforms, transformsFromTransform
11
+ } from "@optimystic/db-core";
12
+ import { asyncIteratorToArray } from "../it-utility.js";
13
+ import type { IBlockStorage } from "./i-block-storage.js";
14
+
15
+ export type StorageRepoOptions = {
16
+ /** Optional hook to validate transactions in PendRequests */
17
+ validatePend?: PendValidationHook;
18
+ };
19
+
20
+ export class StorageRepo implements IRepo {
21
+ private readonly validatePend?: PendValidationHook;
22
+
23
+ constructor(
24
+ private readonly createBlockStorage: (blockId: BlockId) => IBlockStorage,
25
+ options?: StorageRepoOptions
26
+ ) {
27
+ this.validatePend = options?.validatePend;
28
+ }
29
+
30
+ async get({ blockIds, context }: BlockGets, options?: MessageOptions): Promise<GetBlockResults> {
31
+ const distinctBlockIds = Array.from(new Set(blockIds));
32
+ const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
33
+ const blockStorage = this.createBlockStorage(blockId);
34
+
35
+ // Ensure that all outstanding transactions in the context are committed
36
+ if (context) {
37
+ const latest = await blockStorage.getLatest();
38
+ const missing = latest
39
+ ? context.committed.filter(c => c.rev > latest.rev)
40
+ : context.committed;
41
+ for (const { actionId, rev } of missing.sort((a, b) => a.rev - b.rev)) {
42
+ const pending = await blockStorage.getPendingTransaction(actionId);
43
+ if (pending) {
44
+ await this.internalCommit(blockId, actionId, rev, blockStorage);
45
+ }
46
+ }
47
+ }
48
+
49
+ const blockRev = await blockStorage.getBlock(context?.rev);
50
+ if (!blockRev) {
51
+ return [blockId, { state: {} } as GetBlockResult];
52
+ }
53
+
54
+ // Include pending action if requested
55
+ if (context?.actionId !== undefined) {
56
+ const pendingTransform = await blockStorage.getPendingTransaction(context.actionId);
57
+ if (!pendingTransform) {
58
+ throw new Error(`Pending action ${context.actionId} not found`);
59
+ }
60
+ const block = applyTransform(blockRev.block, pendingTransform);
61
+ return [blockId, {
62
+ block,
63
+ state: {
64
+ latest: await blockStorage.getLatest(),
65
+ pendings: [context.actionId]
66
+ }
67
+ }];
68
+ }
69
+
70
+ const pendings = await asyncIteratorToArray(blockStorage.listPendingTransactions());
71
+ return [blockId, {
72
+ block: blockRev.block,
73
+ state: {
74
+ latest: await blockStorage.getLatest(),
75
+ pendings
76
+ }
77
+ }];
78
+ }));
79
+ return Object.fromEntries(results);
80
+ }
81
+
82
+ async pend(request: PendRequest, _options?: MessageOptions): Promise<PendResult> {
83
+ // Validate transaction if present and validation hook is configured
84
+ if (this.validatePend && request.transaction && request.operationsHash) {
85
+ const validationResult = await this.validatePend(request.transaction, request.operationsHash);
86
+ if (!validationResult.valid) {
87
+ return {
88
+ success: false,
89
+ reason: validationResult.reason ?? 'Transaction validation failed'
90
+ };
91
+ }
92
+ }
93
+
94
+ const blockIds = blockIdsForTransforms(request.transforms);
95
+ const pendings: ActionPending[] = [];
96
+ const missing: ActionTransforms[] = [];
97
+
98
+ // Potential race condition: A concurrent commit operation could complete
99
+ // between the conflict checks (latest.rev, listPendingTransactions) and the
100
+ // savePendingTransaction call below. This pend operation might succeed based on
101
+ // stale information, but the subsequent commit for this pend would likely
102
+ // fail correctly later if a conflict arose. Locking here could make the initial
103
+ // check more accurate but adds overhead. The current approach prioritizes
104
+ // letting the commit be the final arbiter.
105
+ for (const blockId of blockIds) {
106
+ const blockStorage = this.createBlockStorage(blockId);
107
+ const transforms = transformForBlockId(request.transforms, blockId);
108
+
109
+ // First handle any pending actions
110
+ const pending = await asyncIteratorToArray(blockStorage.listPendingTransactions());
111
+ pendings.push(...pending.map(actionId => ({ blockId, actionId })));
112
+
113
+ // Handle any conflicting revisions
114
+ if (request.rev !== undefined || transforms.insert) {
115
+ const latest = await blockStorage.getLatest();
116
+ if (latest && latest.rev >= (request.rev ?? 0)) {
117
+ const transforms = await asyncIteratorToArray(blockStorage.listRevisions(request.rev ?? 0, latest.rev));
118
+ for (const actionRev of transforms) {
119
+ const transform = await blockStorage.getTransaction(actionRev.actionId);
120
+ if (!transform) {
121
+ throw new Error(`Missing action ${actionRev.actionId} for block ${blockId}`);
122
+ }
123
+ missing.push({
124
+ actionId: actionRev.actionId,
125
+ rev: actionRev.rev,
126
+ transforms: transformsFromTransform(transform, blockId)
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ if (missing.length) {
134
+ return {
135
+ success: false,
136
+ missing
137
+ };
138
+ }
139
+
140
+ if (pendings.length > 0) {
141
+ if (request.policy === 'f') { // Fail on pending actions
142
+ return { success: false, pending: pendings };
143
+ } else if (request.policy === 'r') { // Return populated pending actions
144
+ return {
145
+ success: false,
146
+ pending: await Promise.all(pendings.map(async action => {
147
+ const blockStorage = this.createBlockStorage(action.blockId);
148
+ return {
149
+ blockId: action.blockId,
150
+ actionId: action.actionId,
151
+ transform: (await blockStorage.getPendingTransaction(action.actionId))
152
+ ?? (await blockStorage.getTransaction(action.actionId))! // Possible that since enumeration, the action has been promoted
153
+ }
154
+ }))
155
+ };
156
+ }
157
+ }
158
+
159
+
160
+ // Simultaneously save pending action for each block
161
+ // Note: that this is not atomic, after we checked for conflicts and pending actions
162
+ // new pending or committed actions may have been added. This is okay, because
163
+ // this check during pend is conservative.
164
+ await Promise.all(blockIds.map(blockId => {
165
+ const blockStorage = this.createBlockStorage(blockId);
166
+ const blockTransform = transformForBlockId(request.transforms, blockId);
167
+ return blockStorage.savePendingTransaction(request.actionId, blockTransform);
168
+ }));
169
+
170
+ return {
171
+ success: true,
172
+ pending: pendings,
173
+ blockIds
174
+ } as PendSuccess;
175
+ }
176
+
177
+ async cancel(actionRef: ActionBlocks, _options?: MessageOptions): Promise<void> {
178
+ await Promise.all(actionRef.blockIds.map(blockId => {
179
+ const blockStorage = this.createBlockStorage(blockId);
180
+ return blockStorage.deletePendingTransaction(actionRef.actionId);
181
+ }));
182
+ }
183
+
184
+ async commit(request: CommitRequest, options?: MessageOptions): Promise<CommitResult> {
185
+ const uniqueBlockIds = Array.from(new Set(request.blockIds)).sort();
186
+ const releases: (() => void)[] = [];
187
+
188
+ try {
189
+ // Acquire locks sequentially based on sorted IDs to prevent deadlocks
190
+ for (const id of uniqueBlockIds) {
191
+ const lockId = `StorageRepo.commit:${id}`;
192
+ const release = await Latches.acquire(lockId);
193
+ releases.push(release);
194
+ }
195
+
196
+ // --- Start of Critical Section ---
197
+
198
+ const blockStorages = request.blockIds.map(blockId => ({
199
+ blockId,
200
+ storage: this.createBlockStorage(blockId)
201
+ }));
202
+
203
+ // Check for stale revisions and collect missing actions
204
+ const missedCommits: { blockId: BlockId, transforms: ActionTransform[] }[] = [];
205
+ for (const { blockId, storage } of blockStorages) {
206
+ const latest = await storage.getLatest();
207
+ if (latest && latest.rev >= request.rev) {
208
+ const transforms: ActionTransform[] = [];
209
+ for await (const actionRev of storage.listRevisions(request.rev, latest.rev)) {
210
+ const transform = await storage.getTransaction(actionRev.actionId);
211
+ if (!transform) {
212
+ throw new Error(`Missing action ${actionRev.actionId} for block ${blockId}`);
213
+ }
214
+ transforms.push({
215
+ actionId: actionRev.actionId,
216
+ rev: actionRev.rev,
217
+ transform
218
+ });
219
+ }
220
+ missedCommits.push({ blockId, transforms }); // Push, even if transforms is empty, because we want to reject the older version
221
+ }
222
+ }
223
+
224
+ if (missedCommits.length) {
225
+ return { // Return directly, locks will be released in finally
226
+ success: false,
227
+ missing: perBlockActionTransformsToPerAction(missedCommits)
228
+ };
229
+ }
230
+
231
+ // Check for missing pending actions
232
+ const missingPends: { blockId: BlockId, actionId: ActionId }[] = [];
233
+ for (const { blockId, storage } of blockStorages) {
234
+ const pendingAction = await storage.getPendingTransaction(request.actionId);
235
+ if (!pendingAction) {
236
+ missingPends.push({ blockId, actionId: request.actionId });
237
+ }
238
+ }
239
+
240
+ if (missingPends.length) {
241
+ throw new Error(`Pending action ${request.actionId} not found for block(s): ${missingPends.map(p => p.blockId).join(', ')}`);
242
+ }
243
+
244
+ // Commit the action for each block
245
+ // This loop will execute atomically for all blocks due to the acquired locks
246
+ for (const { blockId, storage } of blockStorages) {
247
+ try {
248
+ // internalCommit will throw if it encounters an issue
249
+ await this.internalCommit(blockId, request.actionId, request.rev, storage);
250
+ } catch (err) {
251
+ // TODO: Recover as best we can. Rollback or handle partial commit? For now, return failure.
252
+ return {
253
+ success: false,
254
+ reason: err instanceof Error ? err.message : 'Unknown error during commit'
255
+ };
256
+ }
257
+ }
258
+ }
259
+ finally {
260
+ // Release locks in reverse order of acquisition
261
+ releases.reverse().forEach(release => release());
262
+ }
263
+
264
+ return { success: true };
265
+ }
266
+
267
+ private async internalCommit(blockId: BlockId, actionId: ActionId, rev: number, storage: IBlockStorage): Promise<void> {
268
+ // Note: This method is called within the locked critical section of commit()
269
+ // So, operations like getPendingTransaction, getLatest, getBlock, saveMaterializedBlock,
270
+ // saveRevision, promotePendingTransaction, setLatest are protected against
271
+ // concurrent commits for the *same blockId*.
272
+
273
+ const transform = await storage.getPendingTransaction(actionId);
274
+ // No need to check if !transform here, as the caller (commit) already verified this.
275
+ // If it's null here, it indicates a logic error or race condition bypassed the lock (unlikely).
276
+ if (!transform) {
277
+ throw new Error(`Consistency Error: Pending action ${actionId} disappeared for block ${blockId} within critical section.`);
278
+ }
279
+
280
+ // Get prior materialized block if it exists
281
+ const latest = await storage.getLatest();
282
+ const priorBlock = latest
283
+ ? (await storage.getBlock(latest.rev))?.block
284
+ : undefined;
285
+
286
+ // Apply transform and save materialized block
287
+ // applyTransform handles undefined priorBlock correctly for inserts
288
+ const newBlock = applyTransform(priorBlock, transform);
289
+ if (newBlock) {
290
+ await storage.saveMaterializedBlock(actionId, newBlock);
291
+ }
292
+
293
+ // Save revision and promote action *before* updating latest
294
+ // This ensures that if the process crashes between these steps,
295
+ // the 'latest' pointer doesn't point to a revision that hasn't been fully recorded.
296
+ await storage.saveRevision(rev, actionId);
297
+ await storage.promotePendingTransaction(actionId);
298
+
299
+ // Update latest revision *last*
300
+ await storage.setLatest({ actionId, rev });
301
+ }
302
+ }
303
+
304
+ /** Converts list of missing actions per block into a list of missing actions across blocks. */
305
+ function perBlockActionTransformsToPerAction(missing: { blockId: BlockId; transforms: ActionTransform[]; }[]) {
306
+ const missingFlat = missing.flatMap(({ blockId, transforms }) =>
307
+ transforms.map(transform => ({ blockId, transform }))
308
+ );
309
+ const missingByActionId = groupBy(missingFlat, ({ transform }) => transform.actionId);
310
+ return Object.entries(missingByActionId).map(([actionId, items]) =>
311
+ items.reduce((acc, { blockId, transform }) => {
312
+ concatTransform(acc.transforms, blockId, transform.transform);
313
+ return acc;
314
+ }, {
315
+ actionId: actionId as ActionId,
316
+ rev: items[0]!.transform.rev, // Assumption: all missing actionIds share the same revision
317
+ transforms: emptyTransforms()
318
+ })
319
+ );
320
+ }
@@ -0,0 +1,34 @@
1
+ import type { BlockId, IBlock, ActionId, ActionRev, ActionTransform, ActionTransforms } from "@optimystic/db-core";
2
+
3
+ export type RevisionRange = [
4
+ /** Inclusive start */
5
+ startRev: number,
6
+ /** Exclusive end, or open-ended if undefined */
7
+ endRev?: number,
8
+ ];
9
+
10
+ export type BlockMetadata = {
11
+ // Revision ranges that are present in storage
12
+ ranges: RevisionRange[];
13
+ /** Latest revision - present if the repo is not empty */
14
+ latest?: ActionRev;
15
+ };
16
+
17
+ export type ArchiveRevisions = Record<number, { action: ActionTransform, block?: IBlock }>;
18
+
19
+ export type BlockArchive = {
20
+ blockId: BlockId;
21
+ /** Revisions in this archive */
22
+ revisions: ArchiveRevisions;
23
+ /** Explicit range covered by this archive since revisions may be sparse */
24
+ range: RevisionRange;
25
+ /** Pending actions - present if this range is open-ended */
26
+ pending?: Record<ActionId, ActionTransforms>;
27
+ }
28
+
29
+ /** Should return a BlockRepo with the given rev (materialized) if given,
30
+ * else (no rev) at least the latest revision and any given pending transactions */
31
+ export type RestoreCallback = (blockId: BlockId, rev?: number) => Promise<BlockArchive | undefined>;
32
+
33
+
34
+
@@ -0,0 +1,42 @@
1
+ import type { PeerId } from '@libp2p/interface';
2
+ import type { IPeerNetwork } from '@optimystic/db-core';
3
+ import { ProtocolClient } from '../protocol-client.js';
4
+ import { buildSyncProtocol, type SyncRequest, type SyncResponse } from './protocol.js';
5
+
6
+ /**
7
+ * Client for sending sync requests to remote peers.
8
+ *
9
+ * Used by storage tiers to request missing blocks from other nodes in the network.
10
+ * Extends ProtocolClient for consistent error handling and timeout behavior.
11
+ */
12
+ export class SyncClient extends ProtocolClient {
13
+ private readonly protocol: string;
14
+
15
+ constructor(
16
+ peerId: PeerId,
17
+ peerNetwork: IPeerNetwork,
18
+ protocolPrefix: string = ''
19
+ ) {
20
+ super(peerId, peerNetwork);
21
+ this.protocol = buildSyncProtocol(protocolPrefix);
22
+ }
23
+
24
+ /**
25
+ * Request a block from the remote peer.
26
+ *
27
+ * @param request - Sync request specifying block and options
28
+ * @returns Response with archive if successful
29
+ * @throws Error if request fails or times out
30
+ */
31
+ async requestBlock(request: SyncRequest): Promise<SyncResponse> {
32
+ return await this.processMessage<SyncResponse>(request, this.protocol);
33
+ }
34
+
35
+ /**
36
+ * Get the protocol string used by this client.
37
+ */
38
+ getProtocol(): string {
39
+ return this.protocol;
40
+ }
41
+ }
42
+
@@ -0,0 +1,71 @@
1
+ import type { BlockId } from '@optimystic/db-core';
2
+ import type { BlockArchive } from '../storage/struct.js';
3
+
4
+ /**
5
+ * Request to sync a specific block or revision from a peer.
6
+ *
7
+ * This protocol is used for block restoration across storage tiers,
8
+ * allowing nodes to request missing blocks from cluster peers or storage rings.
9
+ */
10
+ export interface SyncRequest {
11
+ /** Block ID to retrieve */
12
+ blockId: BlockId;
13
+
14
+ /**
15
+ * Optional specific revision to retrieve.
16
+ * If undefined, retrieve the latest available revision.
17
+ */
18
+ rev?: number;
19
+
20
+ /**
21
+ * If true, include pending transactions in the response.
22
+ * Typically true when requesting latest state, false for historical revisions.
23
+ */
24
+ includePending?: boolean;
25
+
26
+ /**
27
+ * Maximum number of revisions to return.
28
+ * Prevents excessive response sizes.
29
+ * @default 100
30
+ */
31
+ maxRevisions?: number;
32
+
33
+ /**
34
+ * Optional hint about which tier is requesting (for logging/metrics).
35
+ * Values: 'ring-zulu', 'ring-0', 'ring-N', etc.
36
+ */
37
+ requestingTier?: string;
38
+ }
39
+
40
+ /**
41
+ * Response containing block archive data.
42
+ */
43
+ export interface SyncResponse {
44
+ /** True if the peer has the requested data */
45
+ success: boolean;
46
+
47
+ /** Block archive if found */
48
+ archive?: BlockArchive;
49
+
50
+ /** Error message if unsuccessful */
51
+ error?: string;
52
+
53
+ /** Peer ID of responder (for tracking/metrics) */
54
+ responderId?: string;
55
+ }
56
+
57
+ /** Sync protocol prefix - namespaced under db-p2p */
58
+ export const SYNC_PROTOCOL_PREFIX = '/db-p2p/sync/';
59
+
60
+ /** Sync protocol version */
61
+ export const SYNC_PROTOCOL_VERSION = '1.0.0';
62
+
63
+ /**
64
+ * Builds the full protocol string for the sync protocol.
65
+ *
66
+ * @param protocolPrefix - Optional prefix (e.g., '/optimystic/testnet')
67
+ * @returns Full protocol string
68
+ */
69
+ export const buildSyncProtocol = (protocolPrefix: string = ''): string =>
70
+ `${protocolPrefix}${SYNC_PROTOCOL_PREFIX}${SYNC_PROTOCOL_VERSION}`;
71
+
@@ -0,0 +1,229 @@
1
+ import type { ComponentLogger, Startable, Stream } from '@libp2p/interface';
2
+ import type { IRepo } from '@optimystic/db-core';
3
+ import { buildSyncProtocol, type SyncRequest, type SyncResponse } from './protocol.js';
4
+ import { pipe } from 'it-pipe';
5
+ import { fromString as u8FromString } from 'uint8arrays/from-string';
6
+ import { toString as u8ToString } from 'uint8arrays/to-string';
7
+ import * as lp from 'it-length-prefixed';
8
+
9
+ export interface SyncServiceInit {
10
+ protocolPrefix?: string;
11
+ }
12
+
13
+ export interface SyncServiceComponents {
14
+ logger: ComponentLogger;
15
+ registrar: { handle: (...args: any[]) => Promise<void>, unhandle: (...args: any[]) => Promise<void> };
16
+ repo: IRepo;
17
+ }
18
+
19
+ type Logger = ReturnType<ComponentLogger['forComponent']>;
20
+
21
+ /**
22
+ * Service for handling incoming sync requests from other cluster peers.
23
+ *
24
+ * Listens on the sync protocol and responds to block requests by:
25
+ * 1. Extracting the block from local storage
26
+ * 2. Building a BlockArchive with requested revisions
27
+ * 3. Sending the response back to the requester
28
+ *
29
+ * This is the server-side of the block restoration mechanism.
30
+ */
31
+ export class SyncService implements Startable {
32
+ private running = false;
33
+ private readonly log: Logger;
34
+ private readonly protocol: string;
35
+ private readonly repo: IRepo;
36
+ private readonly registrar: { handle: (...args: any[]) => Promise<void>, unhandle: (...args: any[]) => Promise<void> };
37
+
38
+ constructor(
39
+ private readonly components: SyncServiceComponents,
40
+ init: SyncServiceInit = {}
41
+ ) {
42
+ this.log = components.logger.forComponent('db-p2p:sync-service');
43
+ this.protocol = buildSyncProtocol(init.protocolPrefix ?? '');
44
+ this.repo = components.repo;
45
+ this.registrar = components.registrar;
46
+ }
47
+
48
+ async start(): Promise<void> {
49
+ if (this.running) return;
50
+
51
+ await this.registrar.handle(this.protocol, async (data: any) => {
52
+ await this.handleSyncRequest(data.stream);
53
+ });
54
+
55
+ this.running = true;
56
+ this.log('Sync service started on protocol %s', this.protocol);
57
+ }
58
+
59
+ async stop(): Promise<void> {
60
+ if (!this.running) return;
61
+ await this.registrar.unhandle(this.protocol);
62
+ this.running = false;
63
+ this.log('Sync service stopped');
64
+ }
65
+
66
+ /**
67
+ * Handle an incoming sync request stream.
68
+ */
69
+ private async handleSyncRequest(stream: Stream): Promise<void> {
70
+ try {
71
+ // Read request using length-prefixed protocol
72
+ const request = await this.readRequest(stream);
73
+
74
+ this.log(
75
+ '[Ring Zulu] Received sync request for block %s revision %s',
76
+ request.blockId,
77
+ request.rev ?? 'latest'
78
+ );
79
+
80
+ // Build archive from local storage
81
+ const archive = await this.buildArchive(
82
+ request.blockId,
83
+ request.rev,
84
+ request.includePending,
85
+ request.maxRevisions
86
+ );
87
+
88
+ // Send response
89
+ const response: SyncResponse = archive
90
+ ? {
91
+ success: true,
92
+ archive,
93
+ responderId: stream.id
94
+ }
95
+ : {
96
+ success: false,
97
+ error: 'Block not found in local storage'
98
+ };
99
+
100
+ await this.sendResponse(stream, response);
101
+
102
+ this.log(
103
+ '[Ring Zulu] %s sync request for block %s',
104
+ response.success ? 'Fulfilled' : 'Failed',
105
+ request.blockId
106
+ );
107
+ } catch (error) {
108
+ this.log.error('Error handling sync request:', error);
109
+
110
+ // Try to send error response
111
+ try {
112
+ const errorResponse: SyncResponse = {
113
+ success: false,
114
+ error: error instanceof Error ? error.message : 'Unknown error'
115
+ };
116
+ await this.sendResponse(stream, errorResponse);
117
+ } catch (sendError) {
118
+ this.log.error('Failed to send error response:', sendError);
119
+ }
120
+ } finally {
121
+ try {
122
+ await stream.close();
123
+ } catch (closeError) {
124
+ // Ignore close errors
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Read and parse a sync request from the stream.
131
+ */
132
+ private async readRequest(stream: Stream): Promise<SyncRequest> {
133
+ const messages: Uint8Array[] = [];
134
+
135
+ await pipe(
136
+ stream.source,
137
+ lp.decode,
138
+ async (source) => {
139
+ for await (const msg of source) {
140
+ messages.push(msg.subarray());
141
+ }
142
+ }
143
+ );
144
+
145
+ if (messages.length === 0) {
146
+ throw new Error('No request received');
147
+ }
148
+
149
+ const json = u8ToString(messages[0]!, 'utf8');
150
+ return JSON.parse(json) as SyncRequest;
151
+ }
152
+
153
+ /**
154
+ * Send a sync response to the stream.
155
+ */
156
+ private async sendResponse(stream: Stream, response: SyncResponse): Promise<void> {
157
+ const json = JSON.stringify(response);
158
+ const bytes = u8FromString(json, 'utf8');
159
+
160
+ await pipe(
161
+ [bytes],
162
+ lp.encode,
163
+ stream.sink
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Build a block archive from local storage.
169
+ *
170
+ * @param blockId - Block to retrieve
171
+ * @param rev - Optional specific revision
172
+ * @param includePending - Whether to include pending transactions
173
+ * @param maxRevisions - Maximum number of revisions to include
174
+ * @returns BlockArchive if found, undefined otherwise
175
+ */
176
+ private async buildArchive(
177
+ blockId: string,
178
+ rev?: number,
179
+ _includePending?: boolean,
180
+ _maxRevisions?: number
181
+ ): Promise<import('../storage/struct.js').BlockArchive | undefined> {
182
+ try {
183
+ // Get the block from local storage
184
+ const context = rev !== undefined
185
+ ? { rev, committed: [], pending: [] }
186
+ : undefined;
187
+
188
+ const result = await this.repo.get({
189
+ blockIds: [blockId],
190
+ context
191
+ });
192
+
193
+ const blockResult = result[blockId];
194
+ if (!blockResult || !blockResult.state.latest) {
195
+ return undefined;
196
+ }
197
+
198
+ const latest = blockResult.state.latest;
199
+
200
+ // Return minimal archive with just the requested block
201
+ const archive: import('../storage/struct.js').BlockArchive = {
202
+ blockId,
203
+ revisions: {
204
+ [latest.rev]: {
205
+ action: {
206
+ actionId: latest.actionId,
207
+ transform: { insert: blockResult.block }
208
+ },
209
+ block: blockResult.block
210
+ }
211
+ },
212
+ range: [latest.rev, latest.rev + 1]
213
+ };
214
+
215
+ return archive;
216
+ } catch (error) {
217
+ this.log.error('Error building archive for block %s:', blockId, error);
218
+ return undefined;
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Factory function for creating a SyncService.
225
+ * Follows the libp2p service pattern.
226
+ */
227
+ export const syncService = (init: SyncServiceInit = {}) =>
228
+ (components: SyncServiceComponents) => new SyncService(components, init);
229
+