@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.
- package/README.md +8 -0
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +4 -4
- package/dist/src/blocks/block-types.d.ts +1 -1
- package/dist/src/blocks/block-types.d.ts.map +1 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +3 -5
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/btree/independent-trunk.d.ts +4 -4
- package/dist/src/btree/independent-trunk.d.ts.map +1 -1
- package/dist/src/btree/independent-trunk.js +2 -2
- package/dist/src/btree/independent-trunk.js.map +1 -1
- package/dist/src/btree/path.d.ts +1 -1
- package/dist/src/btree/path.d.ts.map +1 -1
- package/dist/src/btree/tree-block.d.ts +1 -1
- package/dist/src/btree/tree-block.d.ts.map +1 -1
- package/dist/src/btree/tree-block.js +2 -2
- package/dist/src/btree/tree-block.js.map +1 -1
- package/dist/src/btree/trunk.d.ts +3 -3
- package/dist/src/btree/trunk.d.ts.map +1 -1
- package/dist/src/collection/collection.d.ts +5 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +77 -50
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts +2 -0
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +4 -0
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +16 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +42 -16
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +1 -13
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transaction/validator.d.ts.map +1 -1
- package/dist/src/transaction/validator.js +5 -9
- package/dist/src/transaction/validator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +29 -3
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.js +1 -1
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/cache-source.js +3 -3
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +27 -2
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +44 -14
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/struct.d.ts +5 -4
- package/dist/src/transform/struct.d.ts.map +1 -1
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +18 -9
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/batch-coordinator.js +0 -1
- package/dist/src/utility/batch-coordinator.js.map +1 -1
- package/dist/src/utility/hash-string.d.ts +11 -0
- package/dist/src/utility/hash-string.d.ts.map +1 -0
- package/dist/src/utility/hash-string.js +17 -0
- package/dist/src/utility/hash-string.js.map +1 -0
- package/package.json +14 -8
- package/src/blocks/block-types.ts +1 -1
- package/src/blocks/structs.ts +1 -1
- package/src/btree/btree.ts +3 -5
- package/src/btree/independent-trunk.ts +6 -6
- package/src/btree/path.ts +1 -1
- package/src/btree/tree-block.ts +3 -3
- package/src/btree/trunk.ts +3 -3
- package/src/collection/collection.ts +78 -51
- package/src/collections/diary/diary.ts +5 -0
- package/src/index.ts +1 -0
- package/src/transaction/coordinator.ts +45 -15
- package/src/transaction/session.ts +182 -182
- package/src/transaction/transaction.ts +2 -13
- package/src/transaction/validator.ts +147 -150
- package/src/transactor/network-transactor.ts +32 -2
- package/src/transactor/transactor-source.ts +1 -1
- package/src/transform/cache-source.ts +3 -3
- package/src/transform/helpers.ts +44 -14
- package/src/transform/struct.ts +5 -6
- package/src/transform/tracker.ts +16 -9
- package/src/utility/hash-string.ts +17 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor,
|
|
2
|
-
import { Log, Atomic, Tracker, copyTransforms, CacheSource, isTransformsEmpty, TransactorSource
|
|
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
|
|
110
|
-
const release = await Latches.acquire(lockId);
|
|
129
|
+
const release = await Latches.acquire(this.latchId);
|
|
111
130
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
await this.syncInternal();
|
|
132
|
+
} finally {
|
|
133
|
+
release();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
private async syncInternal() {
|
|
138
|
+
const bytes = randomBytes(16);
|
|
139
|
+
const actionId = uint8ArrayToString(bytes, 'base64url');
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
]);
|