@optimystic/db-core 0.5.2 → 0.7.0

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 (93) hide show
  1. package/dist/src/btree/btree.d.ts +2 -0
  2. package/dist/src/btree/btree.d.ts.map +1 -1
  3. package/dist/src/btree/btree.js +72 -52
  4. package/dist/src/btree/btree.js.map +1 -1
  5. package/dist/src/cluster/structs.d.ts +13 -0
  6. package/dist/src/cluster/structs.d.ts.map +1 -1
  7. package/dist/src/collection/collection.d.ts +10 -0
  8. package/dist/src/collection/collection.d.ts.map +1 -1
  9. package/dist/src/collection/collection.js +34 -0
  10. package/dist/src/collection/collection.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +1 -0
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/log/log.js +1 -1
  16. package/dist/src/log/log.js.map +1 -1
  17. package/dist/src/logger.d.ts +4 -0
  18. package/dist/src/logger.d.ts.map +1 -0
  19. package/dist/src/logger.js +8 -0
  20. package/dist/src/logger.js.map +1 -0
  21. package/dist/src/transaction/coordinator.d.ts +31 -8
  22. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  23. package/dist/src/transaction/coordinator.js +206 -53
  24. package/dist/src/transaction/coordinator.js.map +1 -1
  25. package/dist/src/transaction/index.d.ts +2 -2
  26. package/dist/src/transaction/index.d.ts.map +1 -1
  27. package/dist/src/transaction/index.js +1 -1
  28. package/dist/src/transaction/index.js.map +1 -1
  29. package/dist/src/transaction/session.d.ts +11 -7
  30. package/dist/src/transaction/session.d.ts.map +1 -1
  31. package/dist/src/transaction/session.js +27 -14
  32. package/dist/src/transaction/session.js.map +1 -1
  33. package/dist/src/transaction/transaction.d.ts +9 -3
  34. package/dist/src/transaction/transaction.d.ts.map +1 -1
  35. package/dist/src/transaction/transaction.js +14 -7
  36. package/dist/src/transaction/transaction.js.map +1 -1
  37. package/dist/src/transaction/validator.d.ts +9 -2
  38. package/dist/src/transaction/validator.d.ts.map +1 -1
  39. package/dist/src/transaction/validator.js +26 -6
  40. package/dist/src/transaction/validator.js.map +1 -1
  41. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  42. package/dist/src/transactor/network-transactor.js +84 -9
  43. package/dist/src/transactor/network-transactor.js.map +1 -1
  44. package/dist/src/transactor/transactor-source.d.ts +4 -0
  45. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  46. package/dist/src/transactor/transactor-source.js +25 -9
  47. package/dist/src/transactor/transactor-source.js.map +1 -1
  48. package/dist/src/transform/atomic-proxy.d.ts +26 -0
  49. package/dist/src/transform/atomic-proxy.d.ts.map +1 -0
  50. package/dist/src/transform/atomic-proxy.js +47 -0
  51. package/dist/src/transform/atomic-proxy.js.map +1 -0
  52. package/dist/src/transform/cache-source.d.ts +3 -2
  53. package/dist/src/transform/cache-source.d.ts.map +1 -1
  54. package/dist/src/transform/cache-source.js +15 -3
  55. package/dist/src/transform/cache-source.js.map +1 -1
  56. package/dist/src/transform/index.d.ts +1 -0
  57. package/dist/src/transform/index.d.ts.map +1 -1
  58. package/dist/src/transform/index.js +1 -0
  59. package/dist/src/transform/index.js.map +1 -1
  60. package/dist/src/utility/batch-coordinator.d.ts +2 -0
  61. package/dist/src/utility/batch-coordinator.d.ts.map +1 -1
  62. package/dist/src/utility/batch-coordinator.js +6 -1
  63. package/dist/src/utility/batch-coordinator.js.map +1 -1
  64. package/dist/src/utility/hash-string.d.ts +3 -6
  65. package/dist/src/utility/hash-string.d.ts.map +1 -1
  66. package/dist/src/utility/hash-string.js +8 -11
  67. package/dist/src/utility/hash-string.js.map +1 -1
  68. package/dist/src/utility/lru-map.d.ts +18 -0
  69. package/dist/src/utility/lru-map.d.ts.map +1 -0
  70. package/dist/src/utility/lru-map.js +52 -0
  71. package/dist/src/utility/lru-map.js.map +1 -0
  72. package/package.json +15 -8
  73. package/src/btree/btree.ts +71 -50
  74. package/src/cluster/structs.ts +11 -0
  75. package/src/collection/collection.ts +44 -0
  76. package/src/index.ts +1 -0
  77. package/src/log/log.ts +1 -1
  78. package/src/logger.ts +10 -0
  79. package/src/transaction/coordinator.ts +244 -57
  80. package/src/transaction/index.ts +4 -2
  81. package/src/transaction/session.ts +38 -14
  82. package/src/transaction/transaction.ts +23 -10
  83. package/src/transaction/validator.ts +34 -7
  84. package/src/transactor/network-transactor.ts +94 -13
  85. package/src/transactor/transactor-source.ts +28 -9
  86. package/src/transform/atomic-proxy.ts +49 -0
  87. package/src/transform/cache-source.ts +18 -4
  88. package/src/transform/index.ts +1 -0
  89. package/src/utility/batch-coordinator.ts +9 -1
  90. package/src/utility/hash-string.ts +14 -17
  91. package/src/utility/lru-map.ts +55 -0
  92. package/dist/index.min.js +0 -9
  93. package/dist/index.min.js.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimystic/db-core",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Core database functionality for Optimystic",
6
6
  "main": "dist/src/index.js",
@@ -15,6 +15,10 @@
15
15
  ".": {
16
16
  "types": "./dist/src/index.d.ts",
17
17
  "import": "./dist/src/index.js"
18
+ },
19
+ "./test": {
20
+ "types": "./dist/test/test-transactor.d.ts",
21
+ "import": "./dist/test/test-transactor.js"
18
22
  }
19
23
  },
20
24
  "repository": {
@@ -36,12 +40,9 @@
36
40
  "acid"
37
41
  ],
38
42
  "scripts": {
39
- "clean": "aegir clean",
40
- "build": "aegir build --env node",
41
- "lint": "aegir lint",
42
- "test": "aegir test",
43
- "test:node": "aegir test -t node",
44
- "dep-check": "aegir dep-check"
43
+ "clean": "rimraf dist",
44
+ "build": "tsc",
45
+ "test": "node --import ./register.mjs node_modules/mocha/bin/mocha.js \"test/**/*.spec.ts\" --colors --reporter min"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@libp2p/crypto": "^5.1.13",
@@ -49,9 +50,15 @@
49
50
  "@libp2p/peer-id": "^6.0.4",
50
51
  "@libp2p/peer-id-factory": "^4.2.4",
51
52
  "@multiformats/multiaddr": "^13.0.1",
53
+ "@types/chai-as-promised": "^8.0.2",
54
+ "@types/debug": "^4.1.12",
52
55
  "@types/mocha": "^10.0.10",
53
56
  "@types/node": "^25.1.0",
54
- "aegir": "^47.0.26",
57
+ "chai": "^6.2.2",
58
+ "chai-as-promised": "^8.0.2",
59
+ "mocha": "^11.7.5",
60
+ "rimraf": "^6.1.2",
61
+ "ts-node": "^10.9.2",
55
62
  "typescript": "^5.9.3"
56
63
  },
57
64
  "dependencies": {
@@ -4,6 +4,7 @@ import { apply, get } from "../blocks/index.js";
4
4
  import { TreeLeafBlockType, TreeBranchBlockType, entries$, nodes$, partitions$ } from "./nodes.js";
5
5
  import type { BranchNode, ITreeNode, LeafNode } from "./nodes.js";
6
6
  import type { TreeBlock } from "./tree-block.js";
7
+ import { AtomicProxy } from "../transform/atomic-proxy.js";
7
8
 
8
9
  export const NodeCapacity = 64;
9
10
 
@@ -15,6 +16,7 @@ export const NodeCapacity = 64;
15
16
  */
16
17
  export class BTree<TKey, TEntry> {
17
18
  protected _version = 0; // only for path invalidation
19
+ private _proxy?: AtomicProxy<ITreeNode>;
18
20
 
19
21
  /**
20
22
  * @param [compare=(a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0] a comparison function for keys. The default uses < and > operators.
@@ -28,6 +30,10 @@ export class BTree<TKey, TEntry> {
28
30
  ) {
29
31
  }
30
32
 
33
+ private atomic<T>(fn: () => Promise<T>): Promise<T> {
34
+ return this._proxy ? this._proxy.atomic(fn) : fn();
35
+ }
36
+
31
37
  static createRoot(
32
38
  store: BlockStore<ITreeNode>
33
39
  ) {
@@ -41,10 +47,13 @@ export class BTree<TKey, TEntry> {
41
47
  compare = (a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0,
42
48
  newId?: BlockId,
43
49
  ) {
44
- const root = BTree.createRoot(store as BlockStore<TreeBlock>);
45
- store.insert(root);
46
- const trunk = createTrunk(store as BlockStore<TreeBlock>, root.header.id, newId);
47
- return new BTree(store, trunk, keyFromEntry, compare);
50
+ const proxy = new AtomicProxy(store);
51
+ const root = BTree.createRoot(proxy as BlockStore<TreeBlock>);
52
+ proxy.insert(root);
53
+ const trunk = createTrunk(proxy as BlockStore<TreeBlock>, root.header.id, newId);
54
+ const tree = new BTree(proxy, trunk, keyFromEntry, compare);
55
+ tree._proxy = proxy;
56
+ return tree;
48
57
  }
49
58
 
50
59
  /** @returns a path to the first entry (on = false if no entries) */
@@ -118,12 +127,14 @@ export class BTree<TKey, TEntry> {
118
127
  * Added entries are frozen to ensure immutability
119
128
  * @returns path to the new (on = true) or conflicting (on = false) row. */
120
129
  async insert(entry: TEntry): Promise<Path<TKey, TEntry>> {
121
- Object.freeze(entry); // Ensure immutability
122
- const path = await this.internalInsert(entry);
123
- if (path.on) {
124
- path.version = ++this._version;
125
- }
126
- return path;
130
+ return this.atomic(async () => {
131
+ Object.freeze(entry); // Ensure immutability
132
+ const path = await this.internalInsert(entry);
133
+ if (path.on) {
134
+ path.version = ++this._version;
135
+ }
136
+ return path;
137
+ });
127
138
  }
128
139
 
129
140
  /** Updates the entry at the given path to the given value. Deletes and inserts if the key changes.
@@ -135,30 +146,34 @@ export class BTree<TKey, TEntry> {
135
146
  * * wasUpdate = true, given path is not on an entry
136
147
  * * else newEntry's new key already present; returned path is "near" existing entry */
137
148
  async updateAt(path: Path<TKey, TEntry>, newEntry: TEntry): Promise<[path: Path<TKey, TEntry>, wasUpdate: boolean]> {
138
- this.validatePath(path);
139
- if (path.on) {
140
- Object.freeze(newEntry);
141
- }
142
- const result = await this.internalUpdate(path, newEntry);
143
- if (result[0].on) {
144
- result[0].version = ++this._version;
145
- }
146
- return result;
149
+ return this.atomic(async () => {
150
+ this.validatePath(path);
151
+ if (path.on) {
152
+ Object.freeze(newEntry);
153
+ }
154
+ const result = await this.internalUpdate(path, newEntry);
155
+ if (result[0].on) {
156
+ result[0].version = ++this._version;
157
+ }
158
+ return result;
159
+ });
147
160
  }
148
161
 
149
162
  /** Inserts the entry if it doesn't exist, or updates it if it does.
150
163
  * The entry is frozen to ensure immutability.
151
164
  * @returns path to the new entry. on = true if existing; on = false if new. */
152
165
  async upsert(entry: TEntry): Promise<Path<TKey, TEntry>> {
153
- const path = await this.find(this.keyFromEntry(entry));
154
- Object.freeze(entry);
155
- if (path.on) {
156
- this.updateEntry(path, entry);
157
- } else {
158
- await this.internalInsertAt(path, entry);
159
- }
160
- path.version = ++this._version;
161
- return path;
166
+ return this.atomic(async () => {
167
+ const path = await this.find(this.keyFromEntry(entry));
168
+ Object.freeze(entry);
169
+ if (path.on) {
170
+ this.updateEntry(path, entry);
171
+ } else {
172
+ await this.internalInsertAt(path, entry);
173
+ }
174
+ path.version = ++this._version;
175
+ return path;
176
+ });
162
177
  }
163
178
 
164
179
  /** Inserts or updates depending on the existence of the given key, using callbacks to generate the new value.
@@ -167,18 +182,20 @@ export class BTree<TKey, TEntry> {
167
182
  * @returns path to new entry and whether an update or insert attempted.
168
183
  * If getUpdated callback returns a row that is already present, the resulting path will not be on. */
169
184
  async merge(newEntry: TEntry, getUpdated: (existing: TEntry) => TEntry): Promise<[path: Path<TKey, TEntry>, wasUpdate: boolean]> {
170
- const newKey = await this.keyFromEntry(newEntry);
171
- const path = await this.find(newKey);
172
- if (path.on) {
173
- const result = await this.updateAt(path, getUpdated(this.getEntry(path))); // Don't use internalUpdate - need to freeze and check for mutation
174
- // Note: updateAt already increments version, so don't double-increment here
175
- return result;
176
- } else {
177
- await this.internalInsertAt(path, Object.freeze(newEntry));
178
- path.on = true;
179
- path.version = ++this._version;
180
- return [path, false];
181
- }
185
+ return this.atomic(async () => {
186
+ const newKey = await this.keyFromEntry(newEntry);
187
+ const path = await this.find(newKey);
188
+ if (path.on) {
189
+ const result = await this.updateAt(path, getUpdated(this.getEntry(path))); // Don't use internalUpdate - need to freeze and check for mutation
190
+ // Note: updateAt already increments version, so don't double-increment here
191
+ return result;
192
+ } else {
193
+ await this.internalInsertAt(path, Object.freeze(newEntry));
194
+ path.on = true;
195
+ path.version = ++this._version;
196
+ return [path, false];
197
+ }
198
+ });
182
199
  }
183
200
 
184
201
  /** Deletes the entry at the given path.
@@ -186,19 +203,23 @@ export class BTree<TKey, TEntry> {
186
203
  * @returns true if the delete succeeded (the key was found); false otherwise.
187
204
  */
188
205
  async deleteAt(path: Path<TKey, TEntry>): Promise<boolean> {
189
- this.validatePath(path);
190
- const result = await this.internalDelete(path);
191
- if (result) {
192
- ++this._version;
193
- }
194
- return result;
206
+ return this.atomic(async () => {
207
+ this.validatePath(path);
208
+ const result = await this.internalDelete(path);
209
+ if (result) {
210
+ ++this._version;
211
+ }
212
+ return result;
213
+ });
195
214
  }
196
215
 
197
216
  async drop() { // Node: only when a root treeBlock
198
- const root = await this.trunk.get();
199
- for await (const id of this.nodeIds(root)) {
200
- this.store.delete(id);
201
- }
217
+ return this.atomic(async () => {
218
+ const root = await this.trunk.get();
219
+ for await (const id of this.nodeIds(root)) {
220
+ this.store.delete(id);
221
+ }
222
+ });
202
223
  }
203
224
 
204
225
  /** Iterates forward starting from the path location (inclusive) to the end.
@@ -27,6 +27,13 @@ export type ClusterRecord = {
27
27
  networkSizeHint?: number;
28
28
  /** Confidence in the network size estimate (0-1) */
29
29
  networkSizeConfidence?: number;
30
+ /** Transaction proceeded despite minority rejections */
31
+ disputed?: boolean;
32
+ /** Evidence of the dispute: which peers rejected and why */
33
+ disputeEvidence?: {
34
+ rejectingPeers: string[];
35
+ rejectReasons: { [peerId: string]: string };
36
+ };
30
37
  }
31
38
 
32
39
  export interface ClusterConsensusConfig {
@@ -42,4 +49,8 @@ export interface ClusterConsensusConfig {
42
49
  clusterSizeTolerance: number;
43
50
  /** Window for detecting partition in milliseconds (default 60000 = 1 min) */
44
51
  partitionDetectionWindow: number;
52
+ /** Enable dispute escalation protocol (default false) */
53
+ disputeEnabled?: boolean;
54
+ /** Timeout for dispute arbitration in milliseconds (default 60000) */
55
+ disputeArbitrationTimeoutMs?: number;
45
56
  }
@@ -1,6 +1,7 @@
1
1
  import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore } from "../index.js";
2
2
  import { Log, Atomic, Tracker, copyTransforms, CacheSource, isTransformsEmpty, TransactorSource } from "../index.js";
3
3
  import type { CollectionHeaderBlock, CollectionId, ICollection } from "./index.js";
4
+ import type { ReadDependency } from "../transaction/transaction.js";
4
5
  import { randomBytes } from '@noble/hashes/utils.js';
5
6
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
6
7
  import { Latches } from "../utility/latches.js";
@@ -45,6 +46,10 @@ export class Collection<TAction> implements ICollection<TAction> {
45
46
  const header = await source.tryGet(id) as CollectionHeaderBlock | undefined;
46
47
 
47
48
  if (header) { // Collection already exists
49
+ // Bootstrap ActionContext from the committed tail before walking the chain.
50
+ // This allows the transactor to serve pending non-tail blocks during Log.open.
51
+ await Collection.bootstrapContext(source, transactor, header);
52
+
48
53
  const log = (await Log.open<Action<TAction>>(tracker, id))!;
49
54
  source.actionContext = await log.getActionContext();
50
55
  } else { // Collection does not exist
@@ -100,6 +105,13 @@ export class Collection<TAction> implements ICollection<TAction> {
100
105
  const source = new TransactorSource(this.id, this.transactor, undefined);
101
106
  const tracker = new Tracker(source);
102
107
 
108
+ // Bootstrap context from committed tail so pending blocks are accessible.
109
+ // Read through tracker so Chain.open inside Log.open reuses the cached header.
110
+ const header = await tracker.tryGet(this.id);
111
+ if (header) {
112
+ await Collection.bootstrapContext(source, this.transactor, header);
113
+ }
114
+
103
115
  // Get the latest entries from the log, starting from where we left off
104
116
  const actionContext = this.source.actionContext;
105
117
  const log = await Log.open<Action<TAction>>(tracker, this.id);
@@ -207,6 +219,14 @@ export class Collection<TAction> implements ICollection<TAction> {
207
219
  }
208
220
  }
209
221
 
222
+ getReadDependencies(): ReadDependency[] {
223
+ return this.source.getReadDependencies();
224
+ }
225
+
226
+ clearReadDependencies(): void {
227
+ this.source.clearReadDependencies();
228
+ }
229
+
210
230
  /** Called for each local action that may be in conflict with a remote action (always called under latch).
211
231
  * @param action - The local action to check
212
232
  * @param potential - The remote action that is potentially in conflict
@@ -224,4 +244,28 @@ export class Collection<TAction> implements ICollection<TAction> {
224
244
  }
225
245
  return true;
226
246
  }
247
+
248
+ /** Bootstrap ActionContext from the committed tail block's state.
249
+ * The tail is always committed first (commit protocol guarantee), so it's readable
250
+ * with context=undefined. Its state.latest contains the ActionRev of the most recent
251
+ * committed action — exactly the proof needed for the transactor to serve pending
252
+ * non-tail blocks during chain walks.
253
+ */
254
+ private static async bootstrapContext(
255
+ source: TransactorSource<IBlock>,
256
+ transactor: ITransactor,
257
+ header: IBlock,
258
+ ): Promise<void> {
259
+ const tailId = Object.hasOwn(header, 'tailId') ? (header as any).tailId as BlockId : undefined;
260
+ if (tailId) {
261
+ const tailResult = await transactor.get({ blockIds: [tailId] });
262
+ const tailState = tailResult?.[tailId]?.state;
263
+ if (tailState?.latest) {
264
+ source.actionContext = {
265
+ committed: [{ actionId: tailState.latest.actionId, rev: tailState.latest.rev }],
266
+ rev: tailState.latest.rev,
267
+ };
268
+ }
269
+ }
270
+ }
227
271
  }
package/src/index.ts CHANGED
@@ -16,3 +16,4 @@ export * from "./utility/nameof.js";
16
16
  export * from "./utility/ensured.js";
17
17
  export * from "./utility/pending.js";
18
18
  export * from "./utility/block-id-to-bytes.js";
19
+ export * from "./utility/lru-map.js";
package/src/log/log.ts CHANGED
@@ -56,7 +56,7 @@ export class Log<TAction> {
56
56
  /** Gets the action context of the log. */
57
57
  async getActionContext(): Promise<ActionContext | undefined> {
58
58
  const tailPath = await this.chain.getTail();
59
- if (!tailPath) {
59
+ if (!tailPath || tailPath.block.entries.length === 0) {
60
60
  return undefined;
61
61
  }
62
62
  const checkpoint = await this.findCheckpoint(tailPath);
package/src/logger.ts ADDED
@@ -0,0 +1,10 @@
1
+ import debug from 'debug'
2
+
3
+ const BASE_NAMESPACE = 'optimystic:db-core'
4
+
5
+ export function createLogger(subNamespace: string): debug.Debugger {
6
+ return debug(`${BASE_NAMESPACE}:${subNamespace}`)
7
+ }
8
+
9
+ export const verbose = typeof process !== 'undefined'
10
+ && (process.env.OPTIMYSTIC_VERBOSE === '1' || process.env.OPTIMYSTIC_VERBOSE === 'true');