@optimystic/db-core 0.0.1 → 0.4.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 (86) hide show
  1. package/README.md +8 -0
  2. package/dist/index.min.js +1 -1
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/blocks/block-types.d.ts +1 -1
  5. package/dist/src/blocks/block-types.d.ts.map +1 -1
  6. package/dist/src/btree/btree.d.ts.map +1 -1
  7. package/dist/src/btree/btree.js +3 -5
  8. package/dist/src/btree/btree.js.map +1 -1
  9. package/dist/src/btree/independent-trunk.d.ts +4 -4
  10. package/dist/src/btree/independent-trunk.d.ts.map +1 -1
  11. package/dist/src/btree/independent-trunk.js +2 -2
  12. package/dist/src/btree/independent-trunk.js.map +1 -1
  13. package/dist/src/btree/path.d.ts +1 -1
  14. package/dist/src/btree/path.d.ts.map +1 -1
  15. package/dist/src/btree/tree-block.d.ts +1 -1
  16. package/dist/src/btree/tree-block.d.ts.map +1 -1
  17. package/dist/src/btree/tree-block.js +2 -2
  18. package/dist/src/btree/tree-block.js.map +1 -1
  19. package/dist/src/btree/trunk.d.ts +3 -3
  20. package/dist/src/btree/trunk.d.ts.map +1 -1
  21. package/dist/src/collection/collection.d.ts +5 -1
  22. package/dist/src/collection/collection.d.ts.map +1 -1
  23. package/dist/src/collection/collection.js +77 -50
  24. package/dist/src/collection/collection.js.map +1 -1
  25. package/dist/src/collections/diary/diary.d.ts +2 -0
  26. package/dist/src/collections/diary/diary.d.ts.map +1 -1
  27. package/dist/src/collections/diary/diary.js +4 -0
  28. package/dist/src/collections/diary/diary.js.map +1 -1
  29. package/dist/src/index.d.ts +1 -0
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +1 -0
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/transaction/coordinator.d.ts +16 -1
  34. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  35. package/dist/src/transaction/coordinator.js +42 -16
  36. package/dist/src/transaction/coordinator.js.map +1 -1
  37. package/dist/src/transaction/transaction.d.ts.map +1 -1
  38. package/dist/src/transaction/transaction.js +1 -13
  39. package/dist/src/transaction/transaction.js.map +1 -1
  40. package/dist/src/transaction/validator.d.ts.map +1 -1
  41. package/dist/src/transaction/validator.js +5 -9
  42. package/dist/src/transaction/validator.js.map +1 -1
  43. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  44. package/dist/src/transactor/network-transactor.js +29 -3
  45. package/dist/src/transactor/network-transactor.js.map +1 -1
  46. package/dist/src/transactor/transactor-source.js +1 -1
  47. package/dist/src/transactor/transactor-source.js.map +1 -1
  48. package/dist/src/transform/cache-source.js +3 -3
  49. package/dist/src/transform/cache-source.js.map +1 -1
  50. package/dist/src/transform/helpers.d.ts +27 -2
  51. package/dist/src/transform/helpers.d.ts.map +1 -1
  52. package/dist/src/transform/helpers.js +44 -14
  53. package/dist/src/transform/helpers.js.map +1 -1
  54. package/dist/src/transform/struct.d.ts +5 -4
  55. package/dist/src/transform/struct.d.ts.map +1 -1
  56. package/dist/src/transform/tracker.d.ts.map +1 -1
  57. package/dist/src/transform/tracker.js +18 -9
  58. package/dist/src/transform/tracker.js.map +1 -1
  59. package/dist/src/utility/batch-coordinator.js +0 -1
  60. package/dist/src/utility/batch-coordinator.js.map +1 -1
  61. package/dist/src/utility/hash-string.d.ts +11 -0
  62. package/dist/src/utility/hash-string.d.ts.map +1 -0
  63. package/dist/src/utility/hash-string.js +17 -0
  64. package/dist/src/utility/hash-string.js.map +1 -0
  65. package/package.json +14 -8
  66. package/src/blocks/block-types.ts +1 -1
  67. package/src/blocks/structs.ts +1 -1
  68. package/src/btree/btree.ts +3 -5
  69. package/src/btree/independent-trunk.ts +6 -6
  70. package/src/btree/path.ts +1 -1
  71. package/src/btree/tree-block.ts +3 -3
  72. package/src/btree/trunk.ts +3 -3
  73. package/src/collection/collection.ts +78 -51
  74. package/src/collections/diary/diary.ts +5 -0
  75. package/src/index.ts +1 -0
  76. package/src/transaction/coordinator.ts +45 -15
  77. package/src/transaction/session.ts +182 -182
  78. package/src/transaction/transaction.ts +2 -13
  79. package/src/transaction/validator.ts +147 -150
  80. package/src/transactor/network-transactor.ts +32 -2
  81. package/src/transactor/transactor-source.ts +1 -1
  82. package/src/transform/cache-source.ts +3 -3
  83. package/src/transform/helpers.ts +44 -14
  84. package/src/transform/struct.ts +5 -6
  85. package/src/transform/tracker.ts +16 -9
  86. package/src/utility/hash-string.ts +17 -0
@@ -1,5 +1,5 @@
1
- import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, ActionEntry, BlockStore } from "../index.js";
2
- import { Log, Atomic, Tracker, copyTransforms, CacheSource, isTransformsEmpty, TransactorSource, blockIdsForTransforms, transformsFromTransform } from "../index.js";
1
+ import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore } from "../index.js";
2
+ import { Log, Atomic, Tracker, copyTransforms, CacheSource, isTransformsEmpty, TransactorSource } from "../index.js";
3
3
  import type { CollectionHeaderBlock, CollectionId, ICollection } from "./index.js";
4
4
  import { randomBytes } from '@libp2p/crypto';
5
5
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
@@ -21,6 +21,7 @@ export type CollectionInitOptions<TAction> = {
21
21
 
22
22
  export class Collection<TAction> implements ICollection<TAction> {
23
23
  private pending: Action<TAction>[] = [];
24
+ private readonly latchId: string;
24
25
 
25
26
  protected constructor(
26
27
  public readonly id: CollectionId,
@@ -33,6 +34,7 @@ export class Collection<TAction> implements ICollection<TAction> {
33
34
  public readonly tracker: Tracker<IBlock>,
34
35
  private readonly filterConflict?: (action: Action<TAction>, potential: Action<TAction>[]) => Action<TAction> | undefined,
35
36
  ) {
37
+ this.latchId = `Collection:${this.id}`;
36
38
  }
37
39
 
38
40
  static async createOrOpen<TAction>(transactor: ITransactor, id: CollectionId, init: CollectionInitOptions<TAction>) {
@@ -56,6 +58,15 @@ export class Collection<TAction> implements ICollection<TAction> {
56
58
  }
57
59
 
58
60
  async act(...actions: Action<TAction>[]) {
61
+ const release = await Latches.acquire(this.latchId);
62
+ try {
63
+ await this.actInternal(...actions);
64
+ } finally {
65
+ release();
66
+ }
67
+ }
68
+
69
+ private async actInternal(...actions: Action<TAction>[]) {
59
70
  await this.internalTransact(...actions);
60
71
  this.pending.push(...actions);
61
72
  }
@@ -76,6 +87,15 @@ export class Collection<TAction> implements ICollection<TAction> {
76
87
 
77
88
  /** Load external changes and update our context to the latest log revision - resolve any conflicts with our pending actions. */
78
89
  async update() {
90
+ const release = await Latches.acquire(this.latchId);
91
+ try {
92
+ await this.updateInternal();
93
+ } finally {
94
+ release();
95
+ }
96
+ }
97
+
98
+ private async updateInternal() {
79
99
  // Start with a context that can see to the end of the log
80
100
  const source = new TransactorSource(this.id, this.transactor, undefined);
81
101
  const tracker = new Tracker(source);
@@ -92,7 +112,7 @@ export class Collection<TAction> implements ICollection<TAction> {
92
112
  this.pending = this.pending.map(p => this.doFilterConflict(p, entry.actions) ? p : undefined)
93
113
  .filter(Boolean) as Action<TAction>[];
94
114
  this.sourceCache.clear(entry.blockIds);
95
- anyConflicts = anyConflicts || tracker.conflicts(new Set(entry.blockIds)).length > 0;
115
+ anyConflicts = anyConflicts || this.tracker.conflicts(new Set(entry.blockIds)).length > 0;
96
116
  }
97
117
 
98
118
  // On conflicts, clear related caching and block-tracking and replay logical operations
@@ -106,57 +126,65 @@ export class Collection<TAction> implements ICollection<TAction> {
106
126
 
107
127
  /** Push our pending actions to the transactor */
108
128
  async sync() {
109
- const lockId = `Collection.sync:${this.id}`;
110
- const release = await Latches.acquire(lockId);
129
+ const release = await Latches.acquire(this.latchId);
111
130
  try {
112
- const bytes = randomBytes(16);
113
- const actionId = uint8ArrayToString(bytes, 'base64url');
131
+ await this.syncInternal();
132
+ } finally {
133
+ release();
134
+ }
135
+ }
114
136
 
115
- while (this.pending.length || !isTransformsEmpty(this.tracker.transforms)) {
116
- // Snapshot the pending actions so that any new actions aren't assumed to be part of this action
117
- const pending = [...this.pending];
137
+ private async syncInternal() {
138
+ const bytes = randomBytes(16);
139
+ const actionId = uint8ArrayToString(bytes, 'base64url');
118
140
 
119
- // Create a snapshot tracker for the action, so that we can ditch the log changes if we have to retry the action
120
- const snapshot = copyTransforms(this.tracker.transforms);
121
- const tracker = new Tracker(this.sourceCache, snapshot);
141
+ while (this.pending.length || !isTransformsEmpty(this.tracker.transforms)) {
142
+ // Snapshot the pending actions so that any new actions aren't assumed to be part of this action
143
+ const pending = [...this.pending];
122
144
 
123
- // Add the action to the log (in local tracking space)
124
- const log = await Log.open<Action<TAction>>(tracker, this.id);
125
- if (!log) {
126
- throw new Error(`Log not found for collection ${this.id}`);
127
- }
128
- const newRev = (this.source.actionContext?.rev ?? 0) + 1;
129
- const addResult = await log.addActions(pending, actionId, newRev, () => tracker.transformedBlockIds());
130
-
131
- // Commit the action to the transactor
132
- const staleFailure = await this.source.transact(tracker.transforms, actionId, newRev, this.id, addResult.tailPath.block.header.id);
133
- if (staleFailure) {
134
- if (staleFailure.pending) {
135
- // Wait for short time to allow the pending actions to commit (bounded backoff)
136
- await new Promise(resolve => setTimeout(resolve, PendingRetryDelayMs));
137
- }
138
- await this.update();
139
- } else {
140
- // Clear the pending actions that were part of this action
141
- this.pending = this.pending.slice(pending.length);
142
- // Reset cache and replay any actions that were added during the action
143
- const transforms = tracker.reset();
144
- await this.replayActions();
145
- this.sourceCache.transformCache(transforms);
146
- this.source.actionContext = this.source.actionContext
147
- ? { committed: [...this.source.actionContext.committed, { actionId, rev: newRev }], rev: newRev }
148
- : { committed: [{ actionId, rev: newRev }], rev: newRev };
145
+ // Create a snapshot tracker for the action, so that we can ditch the log changes if we have to retry the action
146
+ const snapshot = copyTransforms(this.tracker.transforms);
147
+ const tracker = new Tracker(this.sourceCache, snapshot);
148
+
149
+ // Add the action to the log (in local tracking space)
150
+ const log = await Log.open<Action<TAction>>(tracker, this.id);
151
+ if (!log) {
152
+ throw new Error(`Log not found for collection ${this.id}`);
153
+ }
154
+ const newRev = (this.source.actionContext?.rev ?? 0) + 1;
155
+ const addResult = await log.addActions(pending, actionId, newRev, () => tracker.transformedBlockIds());
156
+
157
+ // Commit the action to the transactor
158
+ const staleFailure = await this.source.transact(tracker.transforms, actionId, newRev, this.id, addResult.tailPath.block.header.id);
159
+ if (staleFailure) {
160
+ if (staleFailure.pending) {
161
+ // Wait for short time to allow the pending actions to commit (bounded backoff)
162
+ await new Promise(resolve => setTimeout(resolve, PendingRetryDelayMs));
149
163
  }
164
+ // Fetch latest state - updateInternal() will call replayActions() if there are conflicts
165
+ await this.updateInternal();
166
+ } else {
167
+ // Clear the pending actions that were part of this action
168
+ this.pending = this.pending.slice(pending.length);
169
+ // Reset cache and replay any actions that were added during the action
170
+ const transforms = tracker.reset();
171
+ await this.replayActions();
172
+ this.sourceCache.transformCache(transforms);
173
+ this.source.actionContext = this.source.actionContext
174
+ ? { committed: [...this.source.actionContext.committed, { actionId, rev: newRev }], rev: newRev }
175
+ : { committed: [{ actionId, rev: newRev }], rev: newRev };
150
176
  }
151
- } finally {
152
- release();
153
177
  }
154
178
  }
155
179
 
156
180
  async updateAndSync() {
157
- // TODO: introduce timer and potentially change stats to determine when to sync, rather than always syncing
158
- await this.update();
159
- await this.sync();
181
+ const release = await Latches.acquire(this.latchId);
182
+ try {
183
+ await this.updateInternal();
184
+ await this.syncInternal();
185
+ } finally {
186
+ release();
187
+ }
160
188
  }
161
189
 
162
190
  async *selectLog(forward = true): AsyncIterableIterator<Action<TAction>> {
@@ -173,15 +201,13 @@ export class Collection<TAction> implements ICollection<TAction> {
173
201
 
174
202
  private async replayActions() {
175
203
  this.tracker.reset();
176
- // Because pending could be appended while we're async, we need to snapshot and repeat until empty
177
- while (this.pending.length) {
178
- const pending = [...this.pending];
179
- this.pending = [];
180
- await this.internalTransact(...pending);
204
+ // Replay pending actions against the fresh tracker state (always called under latch)
205
+ for (const action of this.pending) {
206
+ await this.internalTransact(action);
181
207
  }
182
208
  }
183
209
 
184
- /** Called for each local action that may be in conflict with a remote action.
210
+ /** Called for each local action that may be in conflict with a remote action (always called under latch).
185
211
  * @param action - The local action to check
186
212
  * @param potential - The remote action that is potentially in conflict
187
213
  * @returns true if the action should be kept, false to discard it
@@ -192,7 +218,8 @@ export class Collection<TAction> implements ICollection<TAction> {
192
218
  if (!replacement) {
193
219
  return false;
194
220
  } else if (replacement !== action) {
195
- this.act(replacement);
221
+ // Queue replacement - it will be applied in replayActions()
222
+ this.pending.push(replacement);
196
223
  }
197
224
  }
198
225
  return true;
@@ -35,6 +35,11 @@ export class Diary<TEntry> {
35
35
  await this.collection.updateAndSync();
36
36
  }
37
37
 
38
+ /** Fetch the latest state from the network */
39
+ async update(): Promise<void> {
40
+ await this.collection.update();
41
+ }
42
+
38
43
  async *select(forward = true): AsyncIterableIterator<TEntry> {
39
44
  for await (const entry of this.collection.selectLog(forward)) {
40
45
  yield entry.data;
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./transaction/index.js";
10
10
  export * from "./transactor/index.js";
11
11
  export * from "./transform/index.js";
12
12
  export * from "./utility/groupby.js";
13
+ export * from "./utility/hash-string.js";
13
14
  export * from "./utility/latches.js";
14
15
  export * from "./utility/nameof.js";
15
16
  export * from "./utility/ensured.js";
@@ -5,7 +5,7 @@ import type { Collection } from "../collection/collection.js";
5
5
  import { TransactionContext } from "./context.js";
6
6
  import { createActionsStatements } from "./actions-engine.js";
7
7
  import { createTransactionStamp, createTransactionId } from "./transaction.js";
8
- import { Log, blockIdsForTransforms, transformsFromTransform } from "../index.js";
8
+ import { Log, blockIdsForTransforms, transformsFromTransform, hashString } from "../index.js";
9
9
 
10
10
  /**
11
11
  * Represents an operation on a block within a collection.
@@ -78,9 +78,9 @@ export class TransactionCoordinator {
78
78
  transforms: collection.tracker.transforms
79
79
  }))
80
80
  .filter(({ transforms }) =>
81
- Object.keys(transforms.inserts).length +
82
- Object.keys(transforms.updates).length +
83
- transforms.deletes.length > 0
81
+ Object.keys(transforms.inserts ?? {}).length +
82
+ Object.keys(transforms.updates ?? {}).length +
83
+ (transforms.deletes?.length ?? 0) > 0
84
84
  );
85
85
 
86
86
  if (collectionData.length === 0) {
@@ -111,13 +111,13 @@ export class TransactionCoordinator {
111
111
  // This hash is used for validation - validators re-execute the transaction
112
112
  // and compare their computed operations hash with this one
113
113
  const allOperations = collectionData.flatMap(({ collectionId, transforms }) => [
114
- ...Object.entries(transforms.inserts).map(([blockId, block]) =>
114
+ ...Object.entries(transforms.inserts ?? {}).map(([blockId, block]) =>
115
115
  ({ type: 'insert' as const, collectionId, blockId, block })
116
116
  ),
117
- ...Object.entries(transforms.updates).map(([blockId, operations]) =>
117
+ ...Object.entries(transforms.updates ?? {}).map(([blockId, operations]) =>
118
118
  ({ type: 'update' as const, collectionId, blockId, operations })
119
119
  ),
120
- ...transforms.deletes.map(blockId =>
120
+ ...(transforms.deletes ?? []).map(blockId =>
121
121
  ({ type: 'delete' as const, collectionId, blockId })
122
122
  )
123
123
  ]);
@@ -156,6 +156,40 @@ export class TransactionCoordinator {
156
156
  }
157
157
  }
158
158
 
159
+ /**
160
+ * Get current transforms from all collections.
161
+ *
162
+ * This collects transforms from each collection's tracker. Useful for
163
+ * validation scenarios where transforms need to be extracted after
164
+ * engine execution.
165
+ */
166
+ getTransforms(): Map<CollectionId, Transforms> {
167
+ const transforms = new Map<CollectionId, Transforms>();
168
+ for (const [collectionId, collection] of this.collections.entries()) {
169
+ const collectionTransforms = collection.tracker.transforms;
170
+ const hasChanges =
171
+ Object.keys(collectionTransforms.inserts ?? {}).length > 0 ||
172
+ Object.keys(collectionTransforms.updates ?? {}).length > 0 ||
173
+ (collectionTransforms.deletes?.length ?? 0) > 0;
174
+ if (hasChanges) {
175
+ transforms.set(collectionId, collectionTransforms);
176
+ }
177
+ }
178
+ return transforms;
179
+ }
180
+
181
+ /**
182
+ * Reset all collection trackers.
183
+ *
184
+ * This clears pending transforms from all collections. Useful for
185
+ * cleaning up after validation or when starting a new transaction.
186
+ */
187
+ resetTransforms(): void {
188
+ for (const collection of this.collections.values()) {
189
+ collection.tracker.reset();
190
+ }
191
+ }
192
+
159
193
  /**
160
194
  * Compute hash of all operations in a transaction.
161
195
  * This hash is used for validation - validators re-execute the transaction
@@ -163,11 +197,7 @@ export class TransactionCoordinator {
163
197
  */
164
198
  private hashOperations(operations: readonly Operation[]): string {
165
199
  const operationsData = JSON.stringify(operations);
166
- const hash = Array.from(operationsData).reduce((acc, char) => {
167
- const charCode = char.charCodeAt(0);
168
- return ((acc << 5) - acc + charCode) & acc;
169
- }, 0);
170
- return `ops:${Math.abs(hash).toString(36)}`;
200
+ return `ops:${hashString(operationsData)}`;
171
201
  }
172
202
 
173
203
  /**
@@ -263,13 +293,13 @@ export class TransactionCoordinator {
263
293
 
264
294
  // 3. Compute operations hash for validation
265
295
  const allOperations = Array.from(collectionTransforms.entries()).flatMap(([collectionId, transforms]) => [
266
- ...Object.entries(transforms.inserts).map(([blockId, block]) =>
296
+ ...Object.entries(transforms.inserts ?? {}).map(([blockId, block]) =>
267
297
  ({ type: 'insert' as const, collectionId, blockId, block })
268
298
  ),
269
- ...Object.entries(transforms.updates).map(([blockId, operations]) =>
299
+ ...Object.entries(transforms.updates ?? {}).map(([blockId, operations]) =>
270
300
  ({ type: 'update' as const, collectionId, blockId, operations })
271
301
  ),
272
- ...transforms.deletes.map(blockId =>
302
+ ...(transforms.deletes ?? []).map(blockId =>
273
303
  ({ type: 'delete' as const, collectionId, blockId })
274
304
  )
275
305
  ]);