@optimystic/db-core 0.20.0 → 0.22.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.
- package/dist/src/btree/btree.d.ts +2 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +1 -1
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +1 -1
- package/dist/src/chain/chain.js.map +1 -1
- package/dist/src/collection/collection.d.ts +73 -9
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +83 -17
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +2 -1
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/diary/struct.js +1 -1
- package/dist/src/collections/diary/struct.js.map +1 -1
- package/dist/src/collections/tree/collection-trunk.js +1 -1
- package/dist/src/collections/tree/collection-trunk.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +1 -1
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +2 -1
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +20 -8
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +24 -8
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/log/log.d.ts +1 -1
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +2 -2
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/network/struct.d.ts +13 -0
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +1 -1
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +37 -8
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +2 -1
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +16 -5
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +5 -0
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +15 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +13 -1
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +26 -2
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +7 -6
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +2 -1
- package/dist/src/transform/tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/btree/btree.ts +2 -1
- package/src/chain/chain.ts +2 -1
- package/src/collection/collection.ts +114 -18
- package/src/collections/diary/diary.ts +2 -1
- package/src/collections/diary/struct.ts +1 -1
- package/src/collections/tree/collection-trunk.ts +1 -1
- package/src/collections/tree/readme.md +32 -0
- package/src/collections/tree/struct.ts +3 -1
- package/src/collections/tree/tree.ts +312 -293
- package/src/log/log.ts +2 -2
- package/src/network/struct.ts +13 -0
- package/src/testing/test-transactor.ts +38 -8
- package/src/transaction/coordinator.ts +2 -1
- package/src/transactor/network-transactor.ts +16 -5
- package/src/transactor/transactor-source.ts +16 -2
- package/src/transform/cache-source.ts +26 -1
- package/src/transform/helpers.ts +7 -6
- package/src/transform/tracker.ts +2 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ITransactor, GetBlockResults, ActionBlocks, BlockActionStatus, PendResult, CommitResult, PendRequest, BlockId, CommitRequest, BlockGets, IBlock, ActionId, ActionTransforms, Transform, Transforms, ClusterNomineesResult } from "../index.js";
|
|
2
|
+
import { ensuredMap } from "../utility/ensured.js";
|
|
3
|
+
import { Latches } from "../utility/latches.js";
|
|
2
4
|
import { applyTransform, blockIdsForTransforms, transformForBlockId, emptyTransforms, concatTransform, transformsFromTransform } from "../transform/index.js";
|
|
3
5
|
|
|
4
6
|
type RevisionNumber = number;
|
|
@@ -61,6 +63,11 @@ export class TestTransactor implements ITransactor {
|
|
|
61
63
|
|
|
62
64
|
// Get the appropriate materialized block based on context
|
|
63
65
|
let block: IBlock | undefined;
|
|
66
|
+
// The revision `block` was actually materialized at, reported as
|
|
67
|
+
// GetBlockResult.materializedRev. Equals `latestRev` on every unpinned path; only a
|
|
68
|
+
// revision-pinned read of a block committed further since the pin makes them differ,
|
|
69
|
+
// and there the pinned value is what the reader observed (see the field's doc).
|
|
70
|
+
let materializedRev: number | undefined;
|
|
64
71
|
if (blockGets.context?.actionId !== undefined) {
|
|
65
72
|
// If requesting a specific action, apply pending transform if it exists
|
|
66
73
|
const pendingTransform = blockState.pendingActions.get(blockGets.context.actionId);
|
|
@@ -68,6 +75,9 @@ export class TestTransactor implements ITransactor {
|
|
|
68
75
|
// Read latest committed block as base for pending transform
|
|
69
76
|
const baseBlock = blockState.materializedBlocks.get(blockState.latestRev);
|
|
70
77
|
block = applyTransformSafe(baseBlock, pendingTransform);
|
|
78
|
+
// A pending carries no revision of its own — report the committed base it was
|
|
79
|
+
// applied over. Absent when there was no base (a pending-only insert).
|
|
80
|
+
if (baseBlock) materializedRev = blockState.latestRev;
|
|
71
81
|
} else {
|
|
72
82
|
// Action not pending, maybe committed? Or maybe invalid actionId for context.
|
|
73
83
|
// For simplicity, return undefined block if specific pending action not found.
|
|
@@ -83,29 +93,37 @@ export class TestTransactor implements ITransactor {
|
|
|
83
93
|
if (pendingTransform) {
|
|
84
94
|
const baseBlock = blockState.materializedBlocks.get(blockState.latestRev);
|
|
85
95
|
block = applyTransformSafe(baseBlock, pendingTransform);
|
|
96
|
+
if (baseBlock) materializedRev = blockState.latestRev;
|
|
86
97
|
break;
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
// Fall through to standard resolution if no pending match
|
|
90
101
|
if (block === undefined) {
|
|
91
102
|
if (blockGets.context.rev !== undefined) {
|
|
92
|
-
|
|
103
|
+
const found = latestMaterializedAt(blockState, blockGets.context.rev);
|
|
104
|
+
block = structuredClone(found?.block);
|
|
105
|
+
materializedRev = found?.rev;
|
|
93
106
|
} else {
|
|
94
107
|
block = structuredClone(blockState.materializedBlocks.get(blockState.latestRev));
|
|
108
|
+
if (block) materializedRev = blockState.latestRev;
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
} else if (blockGets.context?.rev !== undefined) {
|
|
98
112
|
// Return the materialized block at the highest revision ≤ requested
|
|
99
|
-
|
|
113
|
+
const found = latestMaterializedAt(blockState, blockGets.context.rev);
|
|
114
|
+
block = structuredClone(found?.block);
|
|
115
|
+
materializedRev = found?.rev;
|
|
100
116
|
} else {
|
|
101
117
|
// Otherwise return latest materialized block
|
|
102
118
|
block = structuredClone(blockState.materializedBlocks.get(blockState.latestRev));
|
|
119
|
+
if (block) materializedRev = blockState.latestRev;
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
|
|
106
123
|
const actionId = blockState.revisionActions.get(blockState.latestRev);
|
|
107
124
|
results[blockId] = {
|
|
108
125
|
block,
|
|
126
|
+
...(materializedRev !== undefined ? { materializedRev } : {}),
|
|
109
127
|
state: {
|
|
110
128
|
latest: actionId !== undefined ? {
|
|
111
129
|
rev: blockState.latestRev,
|
|
@@ -457,16 +475,28 @@ function newBlockState(): BlockState {
|
|
|
457
475
|
};
|
|
458
476
|
}
|
|
459
477
|
|
|
460
|
-
/** Returns the materialized block at the highest revision ≤ the given revision
|
|
461
|
-
|
|
478
|
+
/** Returns the materialized block at the highest revision ≤ the given revision, together with
|
|
479
|
+
* that revision — the caller reports it as {@link GetBlockResult.materializedRev}. */
|
|
480
|
+
function latestMaterializedAt(blockState: BlockState, maxRev: number): { block: IBlock, rev: number } | undefined {
|
|
462
481
|
for (let rev = maxRev; rev >= 0; rev--) {
|
|
463
482
|
const block = blockState.materializedBlocks.get(rev);
|
|
464
|
-
if (block) return block;
|
|
483
|
+
if (block) return { block, rev };
|
|
465
484
|
}
|
|
466
485
|
return undefined;
|
|
467
486
|
}
|
|
468
487
|
|
|
488
|
+
/**
|
|
489
|
+
* `applyTransform` over cloned inputs, so a returned block never aliases stored state.
|
|
490
|
+
*
|
|
491
|
+
* An absent base is NOT a short circuit: an insert needs no base — `applyTransform` adopts it as
|
|
492
|
+
* the block. Bailing out on `!block` made this double silently drop a pending-only insert read
|
|
493
|
+
* through the pending overlay, the one shape `StorageRepo.get` serves with content but no
|
|
494
|
+
* `materializedRev` (see docs/internals.md § the `unavailable`/`materializedRev` bullets). A
|
|
495
|
+
* pending UPDATE over an absent base still resolves to undefined, matching the real repo.
|
|
496
|
+
*/
|
|
469
497
|
function applyTransformSafe(block: IBlock | undefined, transform: Transform): IBlock | undefined {
|
|
470
|
-
|
|
471
|
-
|
|
498
|
+
return applyTransform(
|
|
499
|
+
block ? structuredClone(block) : undefined,
|
|
500
|
+
transform.insert ? { ...transform, insert: structuredClone(transform.insert) } : transform
|
|
501
|
+
);
|
|
472
502
|
}
|
|
@@ -5,7 +5,8 @@ import { isConflictFailure } from "../network/stale-failure.js";
|
|
|
5
5
|
import type { Collection } from "../collection/collection.js";
|
|
6
6
|
import type { SyncOptions } from "../collection/index.js";
|
|
7
7
|
import { isTransactionExpired, clampPriority } from "./transaction.js";
|
|
8
|
-
import { Log
|
|
8
|
+
import { Log } from "../log/log.js";
|
|
9
|
+
import { blockIdsForTransforms } from "../transform/helpers.js";
|
|
9
10
|
import { collectOperations, hashOperations } from "./operations-hash.js";
|
|
10
11
|
import { CoordinatorPartialCommitError, CoordinatorStaleLossError } from "./errors.js";
|
|
11
12
|
import { jitteredBackoffMs, abortableDelay, makeAbortError } from "../utility/backoff.js";
|
|
@@ -4,7 +4,12 @@ import { highestStaleAt, isConflictFailure } from "../network/stale-failure.js";
|
|
|
4
4
|
import { BlockUnavailableError } from "../network/struct.js";
|
|
5
5
|
import type { ActionTransforms, ActionBlocks, BlockActionStatus, ITransactor, PendSuccess, StaleFailure, IKeyNetwork, BlockId, GetBlockResults, PendResult, CommitResult, PendRequest, IRepo, BlockGets, Transforms, CommitRequest, ActionId, RepoCommitRequest, ClusterNomineesResult, CollectionId, IBlock, CoordinatorIntent } from "../index.js";
|
|
6
6
|
import type { IBlockChangeNotifier, CollectionChangeListener } from "./change-notifier.js";
|
|
7
|
-
import { transformForBlockId,
|
|
7
|
+
import { transformForBlockId, concatTransforms, concatTransform, transformsFromTransform, blockIdsForTransforms } from "../transform/helpers.js";
|
|
8
|
+
import { Tracker } from "../transform/tracker.js";
|
|
9
|
+
import { CacheSource } from "../transform/cache-source.js";
|
|
10
|
+
import { TransactorSource } from "./transactor-source.js";
|
|
11
|
+
import { Log } from "../log/log.js";
|
|
12
|
+
import { groupBy } from "../utility/groupby.js";
|
|
8
13
|
import { blockIdToBytes } from "../utility/block-id-to-bytes.js";
|
|
9
14
|
import { isRecordEmpty } from "../utility/is-record-empty.js";
|
|
10
15
|
import { type CoordinatorBatch, makeBatchesByPeer, incompleteBatches, everyBatch, allBatches, mergeBlocks, processBatches, createBatchesForPayload } from "../utility/batch-coordinator.js";
|
|
@@ -215,6 +220,13 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
215
220
|
// absent, which beats an `unavailable` guess — one peer that positively knows the
|
|
216
221
|
// block is absent outranks another that could not find out. Non-object junk ranks
|
|
217
222
|
// below everything so any real entry replaces it.
|
|
223
|
+
// NOTE: `materializedRev` is not part of the ranking, so two peers answering the same
|
|
224
|
+
// pinned get with block-carrying entries at DIFFERENT materialized revisions resolve
|
|
225
|
+
// to whichever arrived first. Not a concern today — cohort peers share the block's
|
|
226
|
+
// revision log, so they agree on the highest committed rev at or below a pin — and the
|
|
227
|
+
// failure direction is safe (a lower recorded revision spuriously stale-rejects rather
|
|
228
|
+
// than wrongly accepting). If peers are ever seen to disagree here, break the tie on
|
|
229
|
+
// the HIGHEST materializedRev among rank-2 entries.
|
|
218
230
|
const rankOf = (r: unknown): number => {
|
|
219
231
|
if (!r || typeof r !== 'object') return -1;
|
|
220
232
|
const entry = r as GetBlockResults[BlockId];
|
|
@@ -634,7 +646,6 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
634
646
|
async commit(request: CommitRequest): Promise<CommitResult> {
|
|
635
647
|
const t0 = Date.now();
|
|
636
648
|
log('commit actionId=%s rev=%d blockIds=%d', request.actionId, request.rev, request.blockIds.length);
|
|
637
|
-
const allBlockIds = [...new Set([...request.blockIds, request.tailId])];
|
|
638
649
|
|
|
639
650
|
// Commit the header block if provided and not already in blockIds.
|
|
640
651
|
// `request.tailId` is threaded into every per-block commit so the coordinator carries it into the
|
|
@@ -642,14 +653,14 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
642
653
|
// CollectionChangeEvent (the reactivity topic anchor). Without this the per-block RepoCommitRequest
|
|
643
654
|
// drops the collection tail and reactivity origination is gated off (undefined tail → non-member).
|
|
644
655
|
if (request.headerId && !request.blockIds.includes(request.headerId)) {
|
|
645
|
-
const headerResult = await this.commitBlock(request.headerId,
|
|
656
|
+
const headerResult = await this.commitBlock(request.headerId, request.actionId, request.rev, request.tailId);
|
|
646
657
|
if (!headerResult.success) {
|
|
647
658
|
return headerResult;
|
|
648
659
|
}
|
|
649
660
|
}
|
|
650
661
|
|
|
651
662
|
// Commit the tail block
|
|
652
|
-
const tailResult = await this.commitBlock(request.tailId,
|
|
663
|
+
const tailResult = await this.commitBlock(request.tailId, request.actionId, request.rev, request.tailId);
|
|
653
664
|
if (!tailResult.success) {
|
|
654
665
|
return tailResult;
|
|
655
666
|
}
|
|
@@ -672,7 +683,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
672
683
|
return { success: true };
|
|
673
684
|
}
|
|
674
685
|
|
|
675
|
-
private async commitBlock(blockId: BlockId,
|
|
686
|
+
private async commitBlock(blockId: BlockId, actionId: ActionId, rev: number, tailId?: BlockId): Promise<CommitResult> {
|
|
676
687
|
const { batches: tailBatches, error: tailError } = await this.commitBlocks({ blockIds: [blockId], actionId, rev, tailId });
|
|
677
688
|
if (tailError) {
|
|
678
689
|
// commit is a pure attempt: stale → { success:false }, transient → throw. Cancellation
|
|
@@ -43,7 +43,7 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
43
43
|
// `result[id]` is undefined. Destructuring that would throw a TypeError.
|
|
44
44
|
const entry = result?.[id];
|
|
45
45
|
if (entry) {
|
|
46
|
-
const { block, state, unavailable } = entry;
|
|
46
|
+
const { block, state, materializedRev, unavailable } = entry;
|
|
47
47
|
// An entry flagged `unavailable` with no block is the repo saying "I could not find
|
|
48
48
|
// out whether this exists" — an answer that must not be read as absent. Throw rather
|
|
49
49
|
// than return undefined, and record no read dependency (dependencies are recorded
|
|
@@ -60,7 +60,13 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
60
60
|
// Record read dependency for optimistic concurrency control, carrying the caller's
|
|
61
61
|
// read purpose (default `value`) so a purely-structural navigation read can later be
|
|
62
62
|
// dropped from the conflict set (see ReadDependencyCollector / Theorem 5).
|
|
63
|
-
|
|
63
|
+
// Record the revision the content was MATERIALIZED at, not the newest the repo holds —
|
|
64
|
+
// see {@link GetBlockResult.materializedRev} for why `state.latest` is the wrong number
|
|
65
|
+
// and why the fallback preserves today's behaviour for repos that omit the field.
|
|
66
|
+
// Both sinks must take the SAME value: CacheSource learns it via getReadRevision on a
|
|
67
|
+
// miss-load and re-emits it on every later hit, so a split would stamp the cache
|
|
68
|
+
// differently from the collector.
|
|
69
|
+
const rev = materializedRev ?? state.latest?.rev ?? 0;
|
|
64
70
|
this.collector.record(id, rev, purpose);
|
|
65
71
|
this.readRevisions.set(id, rev);
|
|
66
72
|
}
|
|
@@ -81,6 +87,14 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
81
87
|
return this.collector.getReadDependencies();
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/** The collector this source records into — the one shared with the collection's
|
|
91
|
+
* CacheSource. Exposed ONLY so a pinned read view built with `recordReads: true`
|
|
92
|
+
* can feed the same per-transaction read set (see Collection.createReadTracker);
|
|
93
|
+
* every other consumer should go through {@link getReadDependencies}. */
|
|
94
|
+
getCollector(): ReadDependencyCollector {
|
|
95
|
+
return this.collector;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
clearReadDependencies(): void {
|
|
85
99
|
this.collector.clear();
|
|
86
100
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IBlock, BlockHeader, BlockId, BlockSource, BlockType, ReadPurpose, Transforms } from "../index.js";
|
|
2
|
-
import { applyOperation } from "
|
|
2
|
+
import { applyOperation } from "./helpers.js";
|
|
3
3
|
import { LruMap } from "../utility/lru-map.js";
|
|
4
4
|
import { createLogger } from "../logger.js";
|
|
5
5
|
import type { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
|
|
@@ -44,8 +44,20 @@ export class CacheSource<T extends IBlock> implements BlockSource<T> {
|
|
|
44
44
|
/** Shared per-transaction read-dependency accumulator (same instance the collection's
|
|
45
45
|
* TransactorSource holds). Optional: log-walk caches that never form a transaction omit it. */
|
|
46
46
|
private readonly collector?: ReadDependencyCollector,
|
|
47
|
+
/** Pre-warm entries for a pinned read view — the output of another cache's
|
|
48
|
+
* {@link snapshotEntries}. Entries are already cloned by snapshotEntries, so they are
|
|
49
|
+
* adopted as-is; per-id revisions ride along so a seeded HIT still records at the
|
|
50
|
+
* revision the block was committed at. Seeding does not bump generations (a fresh
|
|
51
|
+
* cache has no consumers with stale memos). */
|
|
52
|
+
seed?: ReadonlyArray<[BlockId, T, number]>,
|
|
47
53
|
) {
|
|
48
54
|
this.cache = new LruMap(maxSize);
|
|
55
|
+
if (seed) {
|
|
56
|
+
for (const [id, block, revision] of seed) {
|
|
57
|
+
this.cache.set(id, block);
|
|
58
|
+
this.revisions.set(id, revision);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
private bump(id: BlockId) {
|
|
@@ -121,6 +133,19 @@ export class CacheSource<T extends IBlock> implements BlockSource<T> {
|
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
/** A cloned copy of the current cache contents with each id's committed revision, in LRU
|
|
137
|
+
* order (oldest first, so replaying into another LruMap preserves eviction order). For
|
|
138
|
+
* building a pinned read view ONLY (see {@link Collection.createReadTracker}): pass the
|
|
139
|
+
* result as the `seed` of a fresh, PRIVATE CacheSource. Blocks are cloned on the way out,
|
|
140
|
+
* so the seeded cache shares no mutable state with this one. */
|
|
141
|
+
snapshotEntries(): Array<[BlockId, T, number]> {
|
|
142
|
+
const entries: Array<[BlockId, T, number]> = [];
|
|
143
|
+
for (const [id, block] of this.cache) {
|
|
144
|
+
entries.push([id, structuredClone(block), this.revisions.get(id) ?? 0]);
|
|
145
|
+
}
|
|
146
|
+
return entries;
|
|
147
|
+
}
|
|
148
|
+
|
|
124
149
|
/** Mutates the cache without affecting the source. `revision` is the committed revision this
|
|
125
150
|
* transform lands at; the stored per-id revision advances to it so a later read records a
|
|
126
151
|
* dependency at the NEW revision (recording the old one would spuriously fail validation). */
|
package/src/transform/helpers.ts
CHANGED
|
@@ -148,11 +148,12 @@ export function applyTransform(block: IBlock | undefined, transform: Transform):
|
|
|
148
148
|
return block;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
/**
|
|
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
|
+
*/
|
|
152
157
|
export function concatTransform(transforms: Transforms, blockId: BlockId, transform: Transform): Transforms {
|
|
153
|
-
return
|
|
154
|
-
inserts: { ...transforms.inserts, ...(transform.insert ? { [blockId]: transform.insert } : {}) },
|
|
155
|
-
updates: { ...transforms.updates, ...(transform.updates ? { [blockId]: transform.updates } : {}) },
|
|
156
|
-
deletes: [...(transforms.deletes ?? []), ...(transform.delete ? [blockId] : [])]
|
|
157
|
-
};
|
|
158
|
+
return mergeTransforms(transforms, transformsFromTransform(transform, blockId));
|
|
158
159
|
}
|
package/src/transform/tracker.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IBlock, BlockId, BlockStore as IBlockStore, BlockHeader, BlockOperation, BlockType, BlockSource as IBlockSource, ReadPurpose } from "../index.js";
|
|
2
|
-
import { applyOperation, applyOperations, emptyTransforms, blockIdsForTransforms
|
|
2
|
+
import { applyOperation, applyOperations, emptyTransforms, blockIdsForTransforms } from "./helpers.js";
|
|
3
|
+
import { ensured } from "../utility/ensured.js";
|
|
3
4
|
|
|
4
5
|
/** A block store that collects transformations, without applying them to the underlying source.
|
|
5
6
|
* Transformations are also applied to the retrieved blocks, making it seem like the source has been modified.
|