@optimystic/db-core 0.22.0 → 0.24.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 +336 -336
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +17 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +24 -2
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +121 -8
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +48 -13
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/package.json +1 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +25 -2
- package/src/collections/diary/diary.ts +68 -68
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/tree.ts +320 -312
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -502
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +49 -14
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/helpers.ts +159 -159
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- package/dist/src/transaction/context.js.map +0 -1
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import { Atomic } from './atomic.js';
|
|
2
|
-
import type { IBlock, BlockId, BlockStore, BlockType, BlockHeader, BlockOperation, ReadPurpose } from '../index.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Opaque handle to an in-flight atomic scope.
|
|
6
|
-
*
|
|
7
|
-
* A method that opens a scope (via {@link AtomicProxy.atomic}) receives the scope as its
|
|
8
|
-
* callback argument. To make a *nested* call reuse the enclosing scope — rather than open
|
|
9
|
-
* (and separately commit) a second one — pass that handle back as the `parent` argument of
|
|
10
|
-
* the nested `atomic()` call. This is how genuine nesting is distinguished from a second,
|
|
11
|
-
* unrelated concurrent scope, portably (no `AsyncLocalStorage`, which is Node-only).
|
|
12
|
-
*/
|
|
13
|
-
export interface AtomicScope { readonly __atomicScope: unique symbol; }
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A BlockStore proxy that enables scoped atomic operations.
|
|
17
|
-
* Operations normally delegate directly to the underlying store,
|
|
18
|
-
* but during an `atomic()` call, they route through an Atomic tracker
|
|
19
|
-
* that commits all-or-nothing on success, or rolls back on error.
|
|
20
|
-
*
|
|
21
|
-
* Both the BTree and its trunk should share the same AtomicProxy instance
|
|
22
|
-
* so that all mutations (including root pointer updates) are part of the
|
|
23
|
-
* same atomic batch.
|
|
24
|
-
*
|
|
25
|
-
* Concurrency model: top-level `atomic()` calls are *serialized* through a promise queue,
|
|
26
|
-
* so a second, un-awaited scope started while the first is still in flight waits for the
|
|
27
|
-
* first to commit/roll back before opening its own tracker — it never shares the first's
|
|
28
|
-
* tracker. A genuinely nested call (one that hands back its enclosing scope) bypasses the
|
|
29
|
-
* queue and reuses that scope, so it neither deadlocks on itself nor double-commits.
|
|
30
|
-
*/
|
|
31
|
-
export class AtomicProxy<T extends IBlock> implements BlockStore<T> {
|
|
32
|
-
private _base: BlockStore<T>;
|
|
33
|
-
private _active: BlockStore<T>;
|
|
34
|
-
/** Tail of the serialization queue; each new top-level scope awaits the prior one. */
|
|
35
|
-
private _tail: Promise<void> = Promise.resolve();
|
|
36
|
-
/** The scope currently executing, or undefined between scopes. Serialization guarantees
|
|
37
|
-
* at most one is active at a time, so identity against `parent` cleanly separates a
|
|
38
|
-
* nested call (parent === current) from a foreign concurrent one (parent absent/stale). */
|
|
39
|
-
private _current?: AtomicScope;
|
|
40
|
-
|
|
41
|
-
constructor(store: BlockStore<T>) {
|
|
42
|
-
this._base = store;
|
|
43
|
-
this._active = store;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async tryGet(id: BlockId, purpose?: ReadPurpose): Promise<T | undefined> { return this._active.tryGet(id, purpose); }
|
|
47
|
-
/** Forward a leaf-value upgrade to the active store (duck-typed; the Tracker/CacheSource chain
|
|
48
|
-
* implements it). Keeps navigation-read filtering working for a B-tree bound to this proxy. */
|
|
49
|
-
markReadValue(id: BlockId): void { (this._active as { markReadValue?: (id: BlockId) => void }).markReadValue?.(id); }
|
|
50
|
-
insert(block: T): void { this._active.insert(block); }
|
|
51
|
-
update(blockId: BlockId, op: BlockOperation): void { this._active.update(blockId, op); }
|
|
52
|
-
delete(blockId: BlockId): void { this._active.delete(blockId); }
|
|
53
|
-
generateId(): BlockId { return this._active.generateId(); }
|
|
54
|
-
createBlockHeader(type: BlockType, newId?: BlockId): BlockHeader { return this._active.createBlockHeader(type, newId); }
|
|
55
|
-
|
|
56
|
-
/** Execute fn within an atomic scope. All store mutations are collected and committed on
|
|
57
|
-
* success, or discarded on error. `fn` receives a scope handle; pass it as the `parent`
|
|
58
|
-
* of any nested `atomic()` call that should join this scope instead of opening its own.
|
|
59
|
-
* Re-entrant safe (nesting reuses the parent scope) and concurrency safe (unrelated
|
|
60
|
-
* overlapping scopes serialize rather than sharing a tracker). */
|
|
61
|
-
async atomic<R>(fn: (scope: AtomicScope) => Promise<R>, parent?: AtomicScope): Promise<R> {
|
|
62
|
-
// Genuine nesting: the caller handed back the scope it is already inside. Reuse it —
|
|
63
|
-
// no second tracker, no second commit — and skip the queue, since waiting on the very
|
|
64
|
-
// scope we are running inside would deadlock.
|
|
65
|
-
if (parent !== undefined && parent === this._current) {
|
|
66
|
-
return fn(parent);
|
|
67
|
-
}
|
|
68
|
-
// Top-level scope (first, or a foreign call that overlapped one in flight): take a
|
|
69
|
-
// place in the serialization queue so it runs against its own tracker only after the
|
|
70
|
-
// prior scope has committed/rolled back.
|
|
71
|
-
const prior = this._tail;
|
|
72
|
-
let release!: () => void;
|
|
73
|
-
this._tail = new Promise<void>(resolve => { release = resolve; });
|
|
74
|
-
await prior;
|
|
75
|
-
const atomic = new Atomic<T>(this._base);
|
|
76
|
-
const scope = atomic as unknown as AtomicScope;
|
|
77
|
-
this._current = scope;
|
|
78
|
-
this._active = atomic;
|
|
79
|
-
try {
|
|
80
|
-
const result = await fn(scope);
|
|
81
|
-
atomic.commit();
|
|
82
|
-
return result;
|
|
83
|
-
} catch (e) {
|
|
84
|
-
atomic.reset();
|
|
85
|
-
throw e;
|
|
86
|
-
} finally {
|
|
87
|
-
this._active = this._base;
|
|
88
|
-
this._current = undefined;
|
|
89
|
-
release();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
1
|
+
import { Atomic } from './atomic.js';
|
|
2
|
+
import type { IBlock, BlockId, BlockStore, BlockType, BlockHeader, BlockOperation, ReadPurpose } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Opaque handle to an in-flight atomic scope.
|
|
6
|
+
*
|
|
7
|
+
* A method that opens a scope (via {@link AtomicProxy.atomic}) receives the scope as its
|
|
8
|
+
* callback argument. To make a *nested* call reuse the enclosing scope — rather than open
|
|
9
|
+
* (and separately commit) a second one — pass that handle back as the `parent` argument of
|
|
10
|
+
* the nested `atomic()` call. This is how genuine nesting is distinguished from a second,
|
|
11
|
+
* unrelated concurrent scope, portably (no `AsyncLocalStorage`, which is Node-only).
|
|
12
|
+
*/
|
|
13
|
+
export interface AtomicScope { readonly __atomicScope: unique symbol; }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A BlockStore proxy that enables scoped atomic operations.
|
|
17
|
+
* Operations normally delegate directly to the underlying store,
|
|
18
|
+
* but during an `atomic()` call, they route through an Atomic tracker
|
|
19
|
+
* that commits all-or-nothing on success, or rolls back on error.
|
|
20
|
+
*
|
|
21
|
+
* Both the BTree and its trunk should share the same AtomicProxy instance
|
|
22
|
+
* so that all mutations (including root pointer updates) are part of the
|
|
23
|
+
* same atomic batch.
|
|
24
|
+
*
|
|
25
|
+
* Concurrency model: top-level `atomic()` calls are *serialized* through a promise queue,
|
|
26
|
+
* so a second, un-awaited scope started while the first is still in flight waits for the
|
|
27
|
+
* first to commit/roll back before opening its own tracker — it never shares the first's
|
|
28
|
+
* tracker. A genuinely nested call (one that hands back its enclosing scope) bypasses the
|
|
29
|
+
* queue and reuses that scope, so it neither deadlocks on itself nor double-commits.
|
|
30
|
+
*/
|
|
31
|
+
export class AtomicProxy<T extends IBlock> implements BlockStore<T> {
|
|
32
|
+
private _base: BlockStore<T>;
|
|
33
|
+
private _active: BlockStore<T>;
|
|
34
|
+
/** Tail of the serialization queue; each new top-level scope awaits the prior one. */
|
|
35
|
+
private _tail: Promise<void> = Promise.resolve();
|
|
36
|
+
/** The scope currently executing, or undefined between scopes. Serialization guarantees
|
|
37
|
+
* at most one is active at a time, so identity against `parent` cleanly separates a
|
|
38
|
+
* nested call (parent === current) from a foreign concurrent one (parent absent/stale). */
|
|
39
|
+
private _current?: AtomicScope;
|
|
40
|
+
|
|
41
|
+
constructor(store: BlockStore<T>) {
|
|
42
|
+
this._base = store;
|
|
43
|
+
this._active = store;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async tryGet(id: BlockId, purpose?: ReadPurpose): Promise<T | undefined> { return this._active.tryGet(id, purpose); }
|
|
47
|
+
/** Forward a leaf-value upgrade to the active store (duck-typed; the Tracker/CacheSource chain
|
|
48
|
+
* implements it). Keeps navigation-read filtering working for a B-tree bound to this proxy. */
|
|
49
|
+
markReadValue(id: BlockId): void { (this._active as { markReadValue?: (id: BlockId) => void }).markReadValue?.(id); }
|
|
50
|
+
insert(block: T): void { this._active.insert(block); }
|
|
51
|
+
update(blockId: BlockId, op: BlockOperation): void { this._active.update(blockId, op); }
|
|
52
|
+
delete(blockId: BlockId): void { this._active.delete(blockId); }
|
|
53
|
+
generateId(): BlockId { return this._active.generateId(); }
|
|
54
|
+
createBlockHeader(type: BlockType, newId?: BlockId): BlockHeader { return this._active.createBlockHeader(type, newId); }
|
|
55
|
+
|
|
56
|
+
/** Execute fn within an atomic scope. All store mutations are collected and committed on
|
|
57
|
+
* success, or discarded on error. `fn` receives a scope handle; pass it as the `parent`
|
|
58
|
+
* of any nested `atomic()` call that should join this scope instead of opening its own.
|
|
59
|
+
* Re-entrant safe (nesting reuses the parent scope) and concurrency safe (unrelated
|
|
60
|
+
* overlapping scopes serialize rather than sharing a tracker). */
|
|
61
|
+
async atomic<R>(fn: (scope: AtomicScope) => Promise<R>, parent?: AtomicScope): Promise<R> {
|
|
62
|
+
// Genuine nesting: the caller handed back the scope it is already inside. Reuse it —
|
|
63
|
+
// no second tracker, no second commit — and skip the queue, since waiting on the very
|
|
64
|
+
// scope we are running inside would deadlock.
|
|
65
|
+
if (parent !== undefined && parent === this._current) {
|
|
66
|
+
return fn(parent);
|
|
67
|
+
}
|
|
68
|
+
// Top-level scope (first, or a foreign call that overlapped one in flight): take a
|
|
69
|
+
// place in the serialization queue so it runs against its own tracker only after the
|
|
70
|
+
// prior scope has committed/rolled back.
|
|
71
|
+
const prior = this._tail;
|
|
72
|
+
let release!: () => void;
|
|
73
|
+
this._tail = new Promise<void>(resolve => { release = resolve; });
|
|
74
|
+
await prior;
|
|
75
|
+
const atomic = new Atomic<T>(this._base);
|
|
76
|
+
const scope = atomic as unknown as AtomicScope;
|
|
77
|
+
this._current = scope;
|
|
78
|
+
this._active = atomic;
|
|
79
|
+
try {
|
|
80
|
+
const result = await fn(scope);
|
|
81
|
+
atomic.commit();
|
|
82
|
+
return result;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
atomic.reset();
|
|
85
|
+
throw e;
|
|
86
|
+
} finally {
|
|
87
|
+
this._active = this._base;
|
|
88
|
+
this._current = undefined;
|
|
89
|
+
release();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/transform/helpers.ts
CHANGED
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
import type { BlockId, BlockOperation, BlockOperations, BlockStore, IBlock, Transform, Transforms } from "../index.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mutates the given block with a copy of the given operation.
|
|
5
|
-
*
|
|
6
|
-
* @warning **MUTATES IN PLACE** - Callers must clone the block first if the original needs preservation.
|
|
7
|
-
* Storage implementations must clone on get/save to prevent cross-revision contamination.
|
|
8
|
-
* @see docs/internals.md for mutation contracts
|
|
9
|
-
*/
|
|
10
|
-
export function applyOperation(block: IBlock, [entity, index, deleteCount, inserted]: BlockOperation) {
|
|
11
|
-
if (Array.isArray(inserted)) {
|
|
12
|
-
(block as unknown as any)[entity].splice(index, deleteCount, ...structuredClone(inserted));
|
|
13
|
-
} else {
|
|
14
|
-
(block as unknown as any)[entity] = structuredClone(inserted);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Mutates the given block with the given set of operations.
|
|
20
|
-
*
|
|
21
|
-
* @warning **MUTATES IN PLACE** - Callers must clone the block first if the original needs preservation.
|
|
22
|
-
* @see docs/internals.md for mutation contracts
|
|
23
|
-
*/
|
|
24
|
-
export function applyOperations(block: IBlock, operations: BlockOperations) {
|
|
25
|
-
for (const op of operations) {
|
|
26
|
-
applyOperation(block, op);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Returns a copy of the block with the given operation applied */
|
|
31
|
-
export function withOperation(block: IBlock, [entity, index, deleteCount, inserted]: BlockOperation) {
|
|
32
|
-
if (Array.isArray(inserted)) {
|
|
33
|
-
const source = (block as any)[entity];
|
|
34
|
-
return { ...block, [entity]: [...source.slice(0, index), ...structuredClone(inserted), ...source.slice(index + deleteCount)] };
|
|
35
|
-
} else {
|
|
36
|
-
return { ...block, [entity]: structuredClone(inserted) };
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** The set of distinct block ids affected by the transform */
|
|
41
|
-
export function blockIdsForTransforms(transforms: Transforms | undefined) {
|
|
42
|
-
if (!transforms) return [];
|
|
43
|
-
const insertIds = Object.keys(transforms.inserts ?? {});
|
|
44
|
-
const updateIds = Object.keys(transforms.updates ?? {});
|
|
45
|
-
const deleteIds = transforms.deletes ?? [];
|
|
46
|
-
return [...new Set([...insertIds, ...updateIds, ...deleteIds])];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Returns an empty transform */
|
|
50
|
-
export function emptyTransforms(): Transforms {
|
|
51
|
-
return { inserts: {}, updates: {}, deletes: [] };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Creates a deep copy of a Transforms object.
|
|
56
|
-
*
|
|
57
|
-
* @pitfall Both `inserts` and `updates` MUST be deep cloned. A shallow copy like
|
|
58
|
-
* `{ ...transform.updates }` or `{ ...transform.inserts }` shares references, causing
|
|
59
|
-
* mutations in one consumer to affect others. Specifically, `Tracker.update` mutates
|
|
60
|
-
* inserted block objects in place via `applyOperation`, so a shallow-cloned snapshot
|
|
61
|
-
* tracker will leak ops back into the original `Transforms.inserts`.
|
|
62
|
-
* @see docs/internals.md "Shallow Copy of Transforms" pitfall
|
|
63
|
-
*/
|
|
64
|
-
export function copyTransforms(transform: Transforms): Transforms {
|
|
65
|
-
const inserts = transform.inserts
|
|
66
|
-
? Object.fromEntries(Object.entries(transform.inserts).map(([k, v]) => [k, structuredClone(v)]))
|
|
67
|
-
: {};
|
|
68
|
-
const updates = transform.updates
|
|
69
|
-
? Object.fromEntries(Object.entries(transform.updates).map(([k, v]) => [k, structuredClone(v)]))
|
|
70
|
-
: undefined;
|
|
71
|
-
return { inserts, updates, deletes: transform.deletes ? [...transform.deletes] : undefined };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function mergeTransforms(a: Transforms, b: Transforms): Transforms {
|
|
75
|
-
// updates: for a block id present in both sides, concatenate a's ops then b's ops
|
|
76
|
-
// (order-preserving) rather than letting b's array replace a's. inserts stay last-wins.
|
|
77
|
-
const updates: { [blockId: BlockId]: BlockOperation[] } = { ...a.updates };
|
|
78
|
-
for (const [id, ops] of Object.entries(b.updates ?? {})) {
|
|
79
|
-
updates[id] = updates[id] ? [...updates[id], ...ops] : ops;
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
inserts: { ...a.inserts, ...b.inserts },
|
|
83
|
-
updates,
|
|
84
|
-
deletes: [...new Set([...(a.deletes ?? []), ...(b.deletes ?? [])])]
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function isTransformsEmpty(transform: Transforms): boolean {
|
|
89
|
-
return Object.keys(transform.inserts ?? {}).length === 0
|
|
90
|
-
&& Object.keys(transform.updates ?? {}).length === 0
|
|
91
|
-
&& (transform.deletes?.length ?? 0) === 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function concatTransforms(...transforms: Transforms[]): Transforms {
|
|
95
|
-
return transforms.reduce((acc, m) => mergeTransforms(acc, m), emptyTransforms());
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Extracts the transform for a specific block from a Transforms object.
|
|
101
|
-
*
|
|
102
|
-
* @pitfall Updates array MUST be deep cloned - extracting without cloning shares
|
|
103
|
-
* the array reference, causing mutations to affect the original Transforms.
|
|
104
|
-
* @see docs/internals.md "Shallow Copy of Transforms" pitfall
|
|
105
|
-
*/
|
|
106
|
-
export function transformForBlockId(transform: Transforms, blockId: BlockId): Transform {
|
|
107
|
-
return {
|
|
108
|
-
...(transform.inserts && blockId in transform.inserts ? { insert: transform.inserts[blockId] } : {}),
|
|
109
|
-
// Clone updates array to prevent shared references
|
|
110
|
-
...(transform.updates && blockId in transform.updates ? { updates: structuredClone(transform.updates[blockId]) } : {}),
|
|
111
|
-
...(transform.deletes?.includes(blockId) ? { delete: true } : {})
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function transformsFromTransform(transform: Transform, blockId: BlockId): Transforms {
|
|
116
|
-
return {
|
|
117
|
-
inserts: transform.insert ? { [blockId]: transform.insert } : {},
|
|
118
|
-
updates: transform.updates ? { [blockId]: transform.updates } : {},
|
|
119
|
-
deletes: transform.delete ? [blockId] : []
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function applyTransformToStore<T extends IBlock>(transform: Transforms, store: BlockStore<T>) {
|
|
124
|
-
for (const blockId of transform.deletes ?? []) {
|
|
125
|
-
store.delete(blockId);
|
|
126
|
-
}
|
|
127
|
-
for (const [, block] of Object.entries(transform.inserts ?? {})) {
|
|
128
|
-
store.insert(block as T);
|
|
129
|
-
}
|
|
130
|
-
for (const [blockId, operations] of Object.entries(transform.updates ?? {})) {
|
|
131
|
-
for (const op of operations) {
|
|
132
|
-
store.update(blockId, op);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Applies a transform to the given block */
|
|
138
|
-
export function applyTransform(block: IBlock | undefined, transform: Transform): IBlock | undefined {
|
|
139
|
-
if (transform.insert) {
|
|
140
|
-
block = transform.insert;
|
|
141
|
-
}
|
|
142
|
-
if (block && transform.updates) {
|
|
143
|
-
applyOperations(block, transform.updates);
|
|
144
|
-
}
|
|
145
|
-
if (transform.delete) {
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
return block;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Concatenates a single block's transform onto the given transforms.
|
|
153
|
-
*
|
|
154
|
-
* Delegates to `mergeTransforms` so the two helpers cannot drift: updates for a shared
|
|
155
|
-
* block id concatenate (order-preserving), deletes dedupe, inserts stay last-wins.
|
|
156
|
-
*/
|
|
157
|
-
export function concatTransform(transforms: Transforms, blockId: BlockId, transform: Transform): Transforms {
|
|
158
|
-
return mergeTransforms(transforms, transformsFromTransform(transform, blockId));
|
|
159
|
-
}
|
|
1
|
+
import type { BlockId, BlockOperation, BlockOperations, BlockStore, IBlock, Transform, Transforms } from "../index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mutates the given block with a copy of the given operation.
|
|
5
|
+
*
|
|
6
|
+
* @warning **MUTATES IN PLACE** - Callers must clone the block first if the original needs preservation.
|
|
7
|
+
* Storage implementations must clone on get/save to prevent cross-revision contamination.
|
|
8
|
+
* @see docs/internals.md for mutation contracts
|
|
9
|
+
*/
|
|
10
|
+
export function applyOperation(block: IBlock, [entity, index, deleteCount, inserted]: BlockOperation) {
|
|
11
|
+
if (Array.isArray(inserted)) {
|
|
12
|
+
(block as unknown as any)[entity].splice(index, deleteCount, ...structuredClone(inserted));
|
|
13
|
+
} else {
|
|
14
|
+
(block as unknown as any)[entity] = structuredClone(inserted);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mutates the given block with the given set of operations.
|
|
20
|
+
*
|
|
21
|
+
* @warning **MUTATES IN PLACE** - Callers must clone the block first if the original needs preservation.
|
|
22
|
+
* @see docs/internals.md for mutation contracts
|
|
23
|
+
*/
|
|
24
|
+
export function applyOperations(block: IBlock, operations: BlockOperations) {
|
|
25
|
+
for (const op of operations) {
|
|
26
|
+
applyOperation(block, op);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns a copy of the block with the given operation applied */
|
|
31
|
+
export function withOperation(block: IBlock, [entity, index, deleteCount, inserted]: BlockOperation) {
|
|
32
|
+
if (Array.isArray(inserted)) {
|
|
33
|
+
const source = (block as any)[entity];
|
|
34
|
+
return { ...block, [entity]: [...source.slice(0, index), ...structuredClone(inserted), ...source.slice(index + deleteCount)] };
|
|
35
|
+
} else {
|
|
36
|
+
return { ...block, [entity]: structuredClone(inserted) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The set of distinct block ids affected by the transform */
|
|
41
|
+
export function blockIdsForTransforms(transforms: Transforms | undefined) {
|
|
42
|
+
if (!transforms) return [];
|
|
43
|
+
const insertIds = Object.keys(transforms.inserts ?? {});
|
|
44
|
+
const updateIds = Object.keys(transforms.updates ?? {});
|
|
45
|
+
const deleteIds = transforms.deletes ?? [];
|
|
46
|
+
return [...new Set([...insertIds, ...updateIds, ...deleteIds])];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Returns an empty transform */
|
|
50
|
+
export function emptyTransforms(): Transforms {
|
|
51
|
+
return { inserts: {}, updates: {}, deletes: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a deep copy of a Transforms object.
|
|
56
|
+
*
|
|
57
|
+
* @pitfall Both `inserts` and `updates` MUST be deep cloned. A shallow copy like
|
|
58
|
+
* `{ ...transform.updates }` or `{ ...transform.inserts }` shares references, causing
|
|
59
|
+
* mutations in one consumer to affect others. Specifically, `Tracker.update` mutates
|
|
60
|
+
* inserted block objects in place via `applyOperation`, so a shallow-cloned snapshot
|
|
61
|
+
* tracker will leak ops back into the original `Transforms.inserts`.
|
|
62
|
+
* @see docs/internals.md "Shallow Copy of Transforms" pitfall
|
|
63
|
+
*/
|
|
64
|
+
export function copyTransforms(transform: Transforms): Transforms {
|
|
65
|
+
const inserts = transform.inserts
|
|
66
|
+
? Object.fromEntries(Object.entries(transform.inserts).map(([k, v]) => [k, structuredClone(v)]))
|
|
67
|
+
: {};
|
|
68
|
+
const updates = transform.updates
|
|
69
|
+
? Object.fromEntries(Object.entries(transform.updates).map(([k, v]) => [k, structuredClone(v)]))
|
|
70
|
+
: undefined;
|
|
71
|
+
return { inserts, updates, deletes: transform.deletes ? [...transform.deletes] : undefined };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function mergeTransforms(a: Transforms, b: Transforms): Transforms {
|
|
75
|
+
// updates: for a block id present in both sides, concatenate a's ops then b's ops
|
|
76
|
+
// (order-preserving) rather than letting b's array replace a's. inserts stay last-wins.
|
|
77
|
+
const updates: { [blockId: BlockId]: BlockOperation[] } = { ...a.updates };
|
|
78
|
+
for (const [id, ops] of Object.entries(b.updates ?? {})) {
|
|
79
|
+
updates[id] = updates[id] ? [...updates[id], ...ops] : ops;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
inserts: { ...a.inserts, ...b.inserts },
|
|
83
|
+
updates,
|
|
84
|
+
deletes: [...new Set([...(a.deletes ?? []), ...(b.deletes ?? [])])]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isTransformsEmpty(transform: Transforms): boolean {
|
|
89
|
+
return Object.keys(transform.inserts ?? {}).length === 0
|
|
90
|
+
&& Object.keys(transform.updates ?? {}).length === 0
|
|
91
|
+
&& (transform.deletes?.length ?? 0) === 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function concatTransforms(...transforms: Transforms[]): Transforms {
|
|
95
|
+
return transforms.reduce((acc, m) => mergeTransforms(acc, m), emptyTransforms());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Extracts the transform for a specific block from a Transforms object.
|
|
101
|
+
*
|
|
102
|
+
* @pitfall Updates array MUST be deep cloned - extracting without cloning shares
|
|
103
|
+
* the array reference, causing mutations to affect the original Transforms.
|
|
104
|
+
* @see docs/internals.md "Shallow Copy of Transforms" pitfall
|
|
105
|
+
*/
|
|
106
|
+
export function transformForBlockId(transform: Transforms, blockId: BlockId): Transform {
|
|
107
|
+
return {
|
|
108
|
+
...(transform.inserts && blockId in transform.inserts ? { insert: transform.inserts[blockId] } : {}),
|
|
109
|
+
// Clone updates array to prevent shared references
|
|
110
|
+
...(transform.updates && blockId in transform.updates ? { updates: structuredClone(transform.updates[blockId]) } : {}),
|
|
111
|
+
...(transform.deletes?.includes(blockId) ? { delete: true } : {})
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function transformsFromTransform(transform: Transform, blockId: BlockId): Transforms {
|
|
116
|
+
return {
|
|
117
|
+
inserts: transform.insert ? { [blockId]: transform.insert } : {},
|
|
118
|
+
updates: transform.updates ? { [blockId]: transform.updates } : {},
|
|
119
|
+
deletes: transform.delete ? [blockId] : []
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function applyTransformToStore<T extends IBlock>(transform: Transforms, store: BlockStore<T>) {
|
|
124
|
+
for (const blockId of transform.deletes ?? []) {
|
|
125
|
+
store.delete(blockId);
|
|
126
|
+
}
|
|
127
|
+
for (const [, block] of Object.entries(transform.inserts ?? {})) {
|
|
128
|
+
store.insert(block as T);
|
|
129
|
+
}
|
|
130
|
+
for (const [blockId, operations] of Object.entries(transform.updates ?? {})) {
|
|
131
|
+
for (const op of operations) {
|
|
132
|
+
store.update(blockId, op);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Applies a transform to the given block */
|
|
138
|
+
export function applyTransform(block: IBlock | undefined, transform: Transform): IBlock | undefined {
|
|
139
|
+
if (transform.insert) {
|
|
140
|
+
block = transform.insert;
|
|
141
|
+
}
|
|
142
|
+
if (block && transform.updates) {
|
|
143
|
+
applyOperations(block, transform.updates);
|
|
144
|
+
}
|
|
145
|
+
if (transform.delete) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return block;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Concatenates a single block's transform onto the given transforms.
|
|
153
|
+
*
|
|
154
|
+
* Delegates to `mergeTransforms` so the two helpers cannot drift: updates for a shared
|
|
155
|
+
* block id concatenate (order-preserving), deletes dedupe, inserts stay last-wins.
|
|
156
|
+
*/
|
|
157
|
+
export function concatTransform(transforms: Transforms, blockId: BlockId, transform: Transform): Transforms {
|
|
158
|
+
return mergeTransforms(transforms, transformsFromTransform(transform, blockId));
|
|
159
|
+
}
|