@optimystic/db-p2p 0.0.1 → 0.1.2

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 (64) hide show
  1. package/{readme.md → README.md} +7 -0
  2. package/dist/index.min.js +31 -30
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/cluster/cluster-repo.d.ts +27 -0
  5. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  6. package/dist/src/cluster/cluster-repo.js +129 -17
  7. package/dist/src/cluster/cluster-repo.js.map +1 -1
  8. package/dist/src/cluster/service.d.ts +13 -2
  9. package/dist/src/cluster/service.d.ts.map +1 -1
  10. package/dist/src/cluster/service.js +17 -7
  11. package/dist/src/cluster/service.js.map +1 -1
  12. package/dist/src/index.d.ts +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +1 -1
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/libp2p-node.d.ts +13 -2
  17. package/dist/src/libp2p-node.d.ts.map +1 -1
  18. package/dist/src/libp2p-node.js +40 -17
  19. package/dist/src/libp2p-node.js.map +1 -1
  20. package/dist/src/protocol-client.d.ts.map +1 -1
  21. package/dist/src/protocol-client.js +8 -7
  22. package/dist/src/protocol-client.js.map +1 -1
  23. package/dist/src/repo/cluster-coordinator.d.ts +7 -2
  24. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  25. package/dist/src/repo/cluster-coordinator.js +18 -3
  26. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  27. package/dist/src/repo/coordinator-repo.d.ts +26 -3
  28. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  29. package/dist/src/repo/coordinator-repo.js +117 -22
  30. package/dist/src/repo/coordinator-repo.js.map +1 -1
  31. package/dist/src/repo/service.d.ts +13 -2
  32. package/dist/src/repo/service.d.ts.map +1 -1
  33. package/dist/src/repo/service.js +25 -12
  34. package/dist/src/repo/service.js.map +1 -1
  35. package/dist/src/storage/memory-storage.d.ts +15 -0
  36. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  37. package/dist/src/storage/memory-storage.js +23 -4
  38. package/dist/src/storage/memory-storage.js.map +1 -1
  39. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  40. package/dist/src/storage/storage-repo.js.map +1 -1
  41. package/dist/src/sync/service.d.ts.map +1 -1
  42. package/dist/src/sync/service.js +7 -2
  43. package/dist/src/sync/service.js.map +1 -1
  44. package/package.json +27 -20
  45. package/src/cluster/cluster-repo.ts +828 -711
  46. package/src/cluster/service.ts +44 -31
  47. package/src/index.ts +1 -1
  48. package/src/libp2p-key-network.ts +334 -334
  49. package/src/libp2p-node.ts +371 -335
  50. package/src/network/network-manager-service.ts +334 -334
  51. package/src/protocol-client.ts +53 -54
  52. package/src/repo/client.ts +112 -112
  53. package/src/repo/cluster-coordinator.ts +613 -592
  54. package/src/repo/coordinator-repo.ts +269 -137
  55. package/src/repo/service.ts +237 -219
  56. package/src/storage/block-storage.ts +182 -182
  57. package/src/storage/memory-storage.ts +24 -5
  58. package/src/storage/storage-repo.ts +321 -320
  59. package/src/sync/service.ts +7 -6
  60. package/dist/src/storage/file-storage.d.ts +0 -30
  61. package/dist/src/storage/file-storage.d.ts.map +0 -1
  62. package/dist/src/storage/file-storage.js +0 -127
  63. package/dist/src/storage/file-storage.js.map +0 -1
  64. package/src/storage/file-storage.ts +0 -163
@@ -1,182 +1,182 @@
1
- import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
2
- import { Latches, applyTransform } from "@optimystic/db-core";
3
- import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from "./struct.js";
4
- import type { IRawStorage } from "./i-raw-storage.js";
5
- import { mergeRanges } from "./helpers.js";
6
- import type { IBlockStorage } from "./i-block-storage.js";
7
-
8
- export class BlockStorage implements IBlockStorage {
9
- constructor(
10
- private readonly blockId: BlockId,
11
- private readonly storage: IRawStorage,
12
- private readonly restoreCallback?: RestoreCallback
13
- ) { }
14
-
15
- async getLatest(): Promise<ActionRev | undefined> {
16
- const meta = await this.storage.getMetadata(this.blockId);
17
- return meta?.latest;
18
- }
19
-
20
- async getBlock(rev?: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
21
- const meta = await this.storage.getMetadata(this.blockId);
22
- if (!meta) {
23
- return undefined;
24
- }
25
-
26
- const targetRev = rev ?? meta.latest?.rev;
27
- if (targetRev === undefined) {
28
- throw new Error(`No revision specified and no latest revision exists for block ${this.blockId}`);
29
- }
30
-
31
- await this.ensureRevision(meta, targetRev);
32
- return await this.materializeBlock(meta, targetRev);
33
- }
34
-
35
- async getTransaction(actionId: ActionId): Promise<Transform | undefined> {
36
- return await this.storage.getTransaction(this.blockId, actionId);
37
- }
38
-
39
- async getPendingTransaction(actionId: ActionId): Promise<Transform | undefined> {
40
- return await this.storage.getPendingTransaction(this.blockId, actionId);
41
- }
42
-
43
- async *listPendingTransactions(): AsyncIterable<ActionId> {
44
- yield* this.storage.listPendingTransactions(this.blockId);
45
- }
46
-
47
- async savePendingTransaction(actionId: ActionId, transform: Transform): Promise<void> {
48
- let meta = await this.storage.getMetadata(this.blockId);
49
- if (!meta) {
50
- meta = { latest: undefined, ranges: [[0]] };
51
- await this.storage.saveMetadata(this.blockId, meta);
52
- }
53
- await this.storage.savePendingTransaction(this.blockId, actionId, transform);
54
- }
55
-
56
- async deletePendingTransaction(actionId: ActionId): Promise<void> {
57
- await this.storage.deletePendingTransaction(this.blockId, actionId);
58
- }
59
-
60
- async *listRevisions(startRev: number, endRev: number): AsyncIterable<ActionRev> {
61
- yield* this.storage.listRevisions(this.blockId, startRev, endRev);
62
- }
63
-
64
- async saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined): Promise<void> {
65
- await this.storage.saveMaterializedBlock(this.blockId, actionId, block);
66
- }
67
-
68
- async saveRevision(rev: number, actionId: ActionId): Promise<void> {
69
- await this.storage.saveRevision(this.blockId, rev, actionId);
70
- }
71
-
72
- async promotePendingTransaction(actionId: ActionId): Promise<void> {
73
- await this.storage.promotePendingTransaction(this.blockId, actionId);
74
- }
75
-
76
- async setLatest(latest: ActionRev): Promise<void> {
77
- const meta = await this.storage.getMetadata(this.blockId);
78
- if (!meta) {
79
- throw new Error(`Block ${this.blockId} not found`);
80
- }
81
- meta.latest = latest;
82
- await this.storage.saveMetadata(this.blockId, meta);
83
- }
84
-
85
- private async ensureRevision(meta: BlockMetadata, rev: number): Promise<void> {
86
- if (this.inRanges(rev, meta.ranges)) {
87
- return;
88
- }
89
-
90
- const lockId = `BlockStorage.ensureRevision:${this.blockId}`;
91
- const release = await Latches.acquire(lockId);
92
- try {
93
- const currentMeta = await this.storage.getMetadata(this.blockId);
94
- if (!currentMeta) {
95
- throw new Error(`Block ${this.blockId} metadata disappeared unexpectedly.`);
96
- }
97
- if (this.inRanges(rev, currentMeta.ranges)) {
98
- return;
99
- }
100
-
101
- const restored = await this.restoreBlock(rev);
102
- if (!restored) {
103
- throw new Error(`Block ${this.blockId} revision ${rev} not found during restore attempt.`);
104
- }
105
- await this.saveRestored(restored);
106
-
107
- currentMeta.ranges.unshift(restored.range);
108
- currentMeta.ranges = mergeRanges(currentMeta.ranges);
109
- await this.storage.saveMetadata(this.blockId, currentMeta);
110
-
111
- } finally {
112
- release();
113
- }
114
- }
115
-
116
- private async materializeBlock(_meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev }> {
117
- let block: IBlock | undefined;
118
- let materializedActionRev: ActionRev | undefined;
119
- const actions: ActionRev[] = [];
120
-
121
- // Find the materialized block
122
- for await (const actionRev of this.storage.listRevisions(this.blockId, targetRev, 1)) {
123
- const materializedBlock = await this.storage.getMaterializedBlock(this.blockId, actionRev.actionId);
124
- if (materializedBlock) {
125
- block = materializedBlock;
126
- materializedActionRev = actionRev;
127
- break;
128
- } else {
129
- actions.push(actionRev);
130
- }
131
- }
132
-
133
- if (!block || !materializedActionRev) {
134
- // There is an implicit requirement that there must be a materialization of the block somewhere in it's history. If the log is truncated, a materialization must be made at the truncation point..
135
- throw new Error(`Failed to find materialized block ${this.blockId} for revision ${targetRev}`);
136
- }
137
-
138
- // Apply transforms in reverse order
139
- for (let i = actions.length - 1; i >= 0; --i) {
140
- const { actionId } = actions[i]!;
141
- const transform = await this.storage.getTransaction(this.blockId, actionId);
142
- if (!transform) {
143
- throw new Error(`Missing action ${actionId} for block ${this.blockId}`);
144
- }
145
- block = applyTransform(block, transform);
146
- }
147
-
148
- if (!block) {
149
- throw new Error(`Block ${this.blockId} has been deleted`);
150
- }
151
- if (actions.length) {
152
- await this.storage.saveMaterializedBlock(this.blockId, actions[0]!.actionId, block);
153
- return { block, actionRev: actions[0]! };
154
- }
155
- return { block, actionRev: materializedActionRev };
156
- }
157
-
158
- private async restoreBlock(rev: number): Promise<BlockArchive | undefined> {
159
- if (!this.restoreCallback) return undefined;
160
- return await this.restoreCallback(this.blockId, rev);
161
- }
162
-
163
- private async saveRestored(archive: BlockArchive) {
164
- const revisions = Object.entries(archive.revisions)
165
- .map(([rev, data]) => ({ rev: Number(rev), data }));
166
-
167
- // Save all revisions, actions, and materializations
168
- for (const { rev, data: { action, block } } of revisions) {
169
- await Promise.all([
170
- this.storage.saveRevision(this.blockId, rev, action.actionId),
171
- this.storage.saveTransaction(this.blockId, action.actionId, action.transform),
172
- block ? this.storage.saveMaterializedBlock(this.blockId, action.actionId, block) : Promise.resolve()
173
- ]);
174
- }
175
- }
176
-
177
- private inRanges(rev: number, ranges: RevisionRange[]): boolean {
178
- return ranges.some(range =>
179
- rev >= range[0] && (range[1] === undefined || rev < range[1])
180
- );
181
- }
182
- }
1
+ import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
2
+ import { Latches, applyTransform } from "@optimystic/db-core";
3
+ import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from "./struct.js";
4
+ import type { IRawStorage } from "./i-raw-storage.js";
5
+ import { mergeRanges } from "./helpers.js";
6
+ import type { IBlockStorage } from "./i-block-storage.js";
7
+
8
+ export class BlockStorage implements IBlockStorage {
9
+ constructor(
10
+ private readonly blockId: BlockId,
11
+ private readonly storage: IRawStorage,
12
+ private readonly restoreCallback?: RestoreCallback
13
+ ) { }
14
+
15
+ async getLatest(): Promise<ActionRev | undefined> {
16
+ const meta = await this.storage.getMetadata(this.blockId);
17
+ return meta?.latest;
18
+ }
19
+
20
+ async getBlock(rev?: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
21
+ const meta = await this.storage.getMetadata(this.blockId);
22
+ if (!meta) {
23
+ return undefined;
24
+ }
25
+
26
+ const targetRev = rev ?? meta.latest?.rev;
27
+ if (targetRev === undefined) {
28
+ throw new Error(`No revision specified and no latest revision exists for block ${this.blockId}`);
29
+ }
30
+
31
+ await this.ensureRevision(meta, targetRev);
32
+ return await this.materializeBlock(meta, targetRev);
33
+ }
34
+
35
+ async getTransaction(actionId: ActionId): Promise<Transform | undefined> {
36
+ return await this.storage.getTransaction(this.blockId, actionId);
37
+ }
38
+
39
+ async getPendingTransaction(actionId: ActionId): Promise<Transform | undefined> {
40
+ return await this.storage.getPendingTransaction(this.blockId, actionId);
41
+ }
42
+
43
+ async *listPendingTransactions(): AsyncIterable<ActionId> {
44
+ yield* this.storage.listPendingTransactions(this.blockId);
45
+ }
46
+
47
+ async savePendingTransaction(actionId: ActionId, transform: Transform): Promise<void> {
48
+ let meta = await this.storage.getMetadata(this.blockId);
49
+ if (!meta) {
50
+ meta = { latest: undefined, ranges: [[0]] };
51
+ await this.storage.saveMetadata(this.blockId, meta);
52
+ }
53
+ await this.storage.savePendingTransaction(this.blockId, actionId, transform);
54
+ }
55
+
56
+ async deletePendingTransaction(actionId: ActionId): Promise<void> {
57
+ await this.storage.deletePendingTransaction(this.blockId, actionId);
58
+ }
59
+
60
+ async *listRevisions(startRev: number, endRev: number): AsyncIterable<ActionRev> {
61
+ yield* this.storage.listRevisions(this.blockId, startRev, endRev);
62
+ }
63
+
64
+ async saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined): Promise<void> {
65
+ await this.storage.saveMaterializedBlock(this.blockId, actionId, block);
66
+ }
67
+
68
+ async saveRevision(rev: number, actionId: ActionId): Promise<void> {
69
+ await this.storage.saveRevision(this.blockId, rev, actionId);
70
+ }
71
+
72
+ async promotePendingTransaction(actionId: ActionId): Promise<void> {
73
+ await this.storage.promotePendingTransaction(this.blockId, actionId);
74
+ }
75
+
76
+ async setLatest(latest: ActionRev): Promise<void> {
77
+ const meta = await this.storage.getMetadata(this.blockId);
78
+ if (!meta) {
79
+ throw new Error(`Block ${this.blockId} not found`);
80
+ }
81
+ meta.latest = latest;
82
+ await this.storage.saveMetadata(this.blockId, meta);
83
+ }
84
+
85
+ private async ensureRevision(meta: BlockMetadata, rev: number): Promise<void> {
86
+ if (this.inRanges(rev, meta.ranges)) {
87
+ return;
88
+ }
89
+
90
+ const lockId = `BlockStorage.ensureRevision:${this.blockId}`;
91
+ const release = await Latches.acquire(lockId);
92
+ try {
93
+ const currentMeta = await this.storage.getMetadata(this.blockId);
94
+ if (!currentMeta) {
95
+ throw new Error(`Block ${this.blockId} metadata disappeared unexpectedly.`);
96
+ }
97
+ if (this.inRanges(rev, currentMeta.ranges)) {
98
+ return;
99
+ }
100
+
101
+ const restored = await this.restoreBlock(rev);
102
+ if (!restored) {
103
+ throw new Error(`Block ${this.blockId} revision ${rev} not found during restore attempt.`);
104
+ }
105
+ await this.saveRestored(restored);
106
+
107
+ currentMeta.ranges.unshift(restored.range);
108
+ currentMeta.ranges = mergeRanges(currentMeta.ranges);
109
+ await this.storage.saveMetadata(this.blockId, currentMeta);
110
+
111
+ } finally {
112
+ release();
113
+ }
114
+ }
115
+
116
+ private async materializeBlock(_meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev }> {
117
+ let block: IBlock | undefined;
118
+ let materializedActionRev: ActionRev | undefined;
119
+ const actions: ActionRev[] = [];
120
+
121
+ // Find the materialized block
122
+ for await (const actionRev of this.storage.listRevisions(this.blockId, targetRev, 1)) {
123
+ const materializedBlock = await this.storage.getMaterializedBlock(this.blockId, actionRev.actionId);
124
+ if (materializedBlock) {
125
+ block = materializedBlock;
126
+ materializedActionRev = actionRev;
127
+ break;
128
+ } else {
129
+ actions.push(actionRev);
130
+ }
131
+ }
132
+
133
+ if (!block || !materializedActionRev) {
134
+ // There is an implicit requirement that there must be a materialization of the block somewhere in it's history. If the log is truncated, a materialization must be made at the truncation point..
135
+ throw new Error(`Failed to find materialized block ${this.blockId} for revision ${targetRev}`);
136
+ }
137
+
138
+ // Apply transforms in reverse order
139
+ for (let i = actions.length - 1; i >= 0; --i) {
140
+ const { actionId } = actions[i]!;
141
+ const transform = await this.storage.getTransaction(this.blockId, actionId);
142
+ if (!transform) {
143
+ throw new Error(`Missing action ${actionId} for block ${this.blockId}`);
144
+ }
145
+ block = applyTransform(block, transform);
146
+ }
147
+
148
+ if (!block) {
149
+ throw new Error(`Block ${this.blockId} has been deleted`);
150
+ }
151
+ if (actions.length) {
152
+ await this.storage.saveMaterializedBlock(this.blockId, actions[0]!.actionId, block);
153
+ return { block, actionRev: actions[0]! };
154
+ }
155
+ return { block, actionRev: materializedActionRev };
156
+ }
157
+
158
+ private async restoreBlock(rev: number): Promise<BlockArchive | undefined> {
159
+ if (!this.restoreCallback) return undefined;
160
+ return await this.restoreCallback(this.blockId, rev);
161
+ }
162
+
163
+ private async saveRestored(archive: BlockArchive) {
164
+ const revisions = Object.entries(archive.revisions)
165
+ .map(([rev, data]) => ({ rev: Number(rev), data }));
166
+
167
+ // Save all revisions, actions, and materializations
168
+ for (const { rev, data: { action, block } } of revisions) {
169
+ await Promise.all([
170
+ this.storage.saveRevision(this.blockId, rev, action.actionId),
171
+ this.storage.saveTransaction(this.blockId, action.actionId, action.transform),
172
+ block ? this.storage.saveMaterializedBlock(this.blockId, action.actionId, block) : Promise.resolve()
173
+ ]);
174
+ }
175
+ }
176
+
177
+ private inRanges(rev: number, ranges: RevisionRange[]): boolean {
178
+ return ranges.some(range =>
179
+ rev >= range[0] && (range[1] === undefined || rev < range[1])
180
+ );
181
+ }
182
+ }
@@ -60,7 +60,8 @@ export class MemoryRawStorage implements IRawStorage {
60
60
  }
61
61
 
62
62
  async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
63
- this.pendingActions.set(this.getActionKey(blockId, actionId), transform);
63
+ // Clone transform to prevent external modifications from affecting stored data
64
+ this.pendingActions.set(this.getActionKey(blockId, actionId), structuredClone(transform));
64
65
  }
65
66
 
66
67
  async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
@@ -84,14 +85,32 @@ export class MemoryRawStorage implements IRawStorage {
84
85
  this.actions.set(this.getActionKey(blockId, actionId), transform);
85
86
  }
86
87
 
88
+ /**
89
+ * Retrieves a materialized block at a specific revision.
90
+ *
91
+ * @pitfall **MUST return a clone** - `applyTransform()` mutates blocks in place.
92
+ * If we return the stored reference, mutations corrupt ALL revisions that share
93
+ * the same underlying object.
94
+ * @see docs/internals.md "Storage Returns References" pitfall
95
+ */
87
96
  async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
88
- return this.materializedBlocks.get(this.getActionKey(blockId, actionId));
89
- }
90
-
97
+ const block = this.materializedBlocks.get(this.getActionKey(blockId, actionId));
98
+ // Clone to prevent external mutations from affecting stored data
99
+ return block ? structuredClone(block) : undefined;
100
+ }
101
+
102
+ /**
103
+ * Stores a materialized block at a specific revision.
104
+ *
105
+ * @pitfall **MUST store a clone** - callers may continue mutating the block after saving.
106
+ * If we store the reference, those mutations corrupt the stored data.
107
+ * @see docs/internals.md "Storage Returns References" pitfall
108
+ */
91
109
  async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
92
110
  const key = this.getActionKey(blockId, actionId);
93
111
  if (block) {
94
- this.materializedBlocks.set(key, block);
112
+ // Clone to prevent external mutations from affecting stored data
113
+ this.materializedBlocks.set(key, structuredClone(block));
95
114
  } else {
96
115
  this.materializedBlocks.delete(key);
97
116
  }