@optimystic/db-core 1.0.0-beta.3 → 1.0.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/cluster/structs.d.ts +30 -6
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +5 -3
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts +2 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js +10 -3
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js.map +1 -1
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/codec.js +8 -2
- package/dist/src/cohort-topic/wire/codec.js.map +1 -1
- package/dist/src/collection/collection.d.ts +439 -34
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +843 -148
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collection/struct.d.ts +109 -4
- package/dist/src/collection/struct.d.ts.map +1 -1
- package/dist/src/collection/struct.js +88 -1
- package/dist/src/collection/struct.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts +6 -2
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +5 -2
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +124 -16
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +86 -11
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +13 -3
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +106 -40
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +8 -2
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/durability.d.ts +45 -0
- package/dist/src/network/durability.d.ts.map +1 -0
- package/dist/src/network/durability.js +114 -0
- package/dist/src/network/durability.js.map +1 -0
- package/dist/src/network/i-key-network.d.ts +14 -6
- package/dist/src/network/i-key-network.d.ts.map +1 -1
- package/dist/src/network/index.d.ts +3 -0
- package/dist/src/network/index.d.ts.map +1 -1
- package/dist/src/network/index.js +3 -0
- package/dist/src/network/index.js.map +1 -1
- package/dist/src/network/lineage.d.ts +39 -0
- package/dist/src/network/lineage.d.ts.map +1 -0
- package/dist/src/network/lineage.js +61 -0
- package/dist/src/network/lineage.js.map +1 -0
- package/dist/src/network/routing-key.d.ts +24 -0
- package/dist/src/network/routing-key.d.ts.map +1 -0
- package/dist/src/network/routing-key.js +9 -0
- package/dist/src/network/routing-key.js.map +1 -0
- package/dist/src/network/stale-failure.d.ts +5 -1
- package/dist/src/network/stale-failure.d.ts.map +1 -1
- package/dist/src/network/stale-failure.js +5 -1
- package/dist/src/network/stale-failure.js.map +1 -1
- package/dist/src/network/struct.d.ts +151 -8
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/reactivity/wire.d.ts.map +1 -1
- package/dist/src/reactivity/wire.js +8 -2
- package/dist/src/reactivity/wire.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +64 -7
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +150 -23
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +141 -21
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +408 -122
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +39 -10
- package/dist/src/transaction/errors.d.ts.map +1 -1
- package/dist/src/transaction/errors.js +34 -9
- package/dist/src/transaction/errors.js.map +1 -1
- package/dist/src/transaction/index.d.ts +1 -0
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transactor/block-floors.d.ts +78 -0
- package/dist/src/transactor/block-floors.d.ts.map +1 -0
- package/dist/src/transactor/block-floors.js +62 -0
- package/dist/src/transactor/block-floors.js.map +1 -0
- package/dist/src/transactor/change-notifier.d.ts +43 -0
- package/dist/src/transactor/change-notifier.d.ts.map +1 -1
- package/dist/src/transactor/change-notifier.js +3 -0
- package/dist/src/transactor/change-notifier.js.map +1 -1
- package/dist/src/transactor/index.d.ts +1 -0
- package/dist/src/transactor/index.d.ts.map +1 -1
- package/dist/src/transactor/index.js +1 -0
- package/dist/src/transactor/index.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +43 -6
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +279 -86
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +94 -4
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +120 -42
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transactor/transactor.d.ts +14 -1
- package/dist/src/transactor/transactor.d.ts.map +1 -1
- package/dist/src/transform/atomic.d.ts.map +1 -1
- package/dist/src/transform/atomic.js +12 -9
- package/dist/src/transform/atomic.js.map +1 -1
- package/dist/src/transform/base-pins.d.ts +50 -17
- package/dist/src/transform/base-pins.d.ts.map +1 -1
- package/dist/src/transform/base-pins.js +58 -11
- package/dist/src/transform/base-pins.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +75 -11
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +162 -36
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/digest.d.ts +8 -1
- package/dist/src/transform/digest.d.ts.map +1 -1
- package/dist/src/transform/digest.js +19 -9
- package/dist/src/transform/digest.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -2
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +8 -3
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +94 -14
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +222 -68
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/structural-equals.d.ts +38 -0
- package/dist/src/utility/structural-equals.d.ts.map +1 -0
- package/dist/src/utility/structural-equals.js +77 -0
- package/dist/src/utility/structural-equals.js.map +1 -0
- package/package.json +2 -2
- package/src/cluster/structs.ts +28 -7
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +260 -253
- package/src/cohort-topic/wire/codec.ts +222 -216
- package/src/collection/collection.ts +960 -157
- package/src/collection/struct.ts +99 -4
- package/src/collections/diary/diary.ts +71 -68
- package/src/collections/tree/struct.ts +155 -23
- package/src/collections/tree/tree.ts +127 -40
- package/src/index.ts +1 -1
- package/src/matchmaking/wire.ts +8 -2
- package/src/network/durability.ts +123 -0
- package/src/network/i-key-network.ts +60 -52
- package/src/network/index.ts +10 -7
- package/src/network/lineage.ts +80 -0
- package/src/network/routing-key.ts +27 -0
- package/src/network/stale-failure.ts +5 -1
- package/src/network/struct.ts +156 -8
- package/src/reactivity/wire.ts +230 -224
- package/src/testing/test-transactor.ts +152 -24
- package/src/transaction/coordinator.ts +470 -131
- package/src/transaction/errors.ts +34 -9
- package/src/transaction/index.ts +1 -0
- package/src/transactor/block-floors.ts +105 -0
- package/src/transactor/change-notifier.ts +129 -80
- package/src/transactor/index.ts +1 -0
- package/src/transactor/network-transactor.ts +297 -89
- package/src/transactor/transactor-source.ts +131 -42
- package/src/transactor/transactor.ts +13 -1
- package/src/transform/atomic.ts +25 -22
- package/src/transform/base-pins.ts +76 -18
- package/src/transform/cache-source.ts +327 -194
- package/src/transform/digest.ts +21 -10
- package/src/transform/helpers.ts +8 -3
- package/src/transform/tracker.ts +423 -270
- package/src/utility/structural-equals.ts +72 -0
- package/dist/src/utility/block-id-to-bytes.d.ts +0 -3
- package/dist/src/utility/block-id-to-bytes.d.ts.map +0 -1
- package/dist/src/utility/block-id-to-bytes.js +0 -7
- package/dist/src/utility/block-id-to-bytes.js.map +0 -1
- package/src/utility/block-id-to-bytes.ts +0 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ITransactor, GetBlockResults, ActionBlocks, BlockActionStatus, PendResult, CommitResult, PendRequest, BlockId, CommitRequest, BlockGets, IBlock, ActionId, ActionRev, ActionTransforms, StaleFailure, Transform, Transforms, ClusterNomineesResult, CollectionId } from "../index.js";
|
|
1
|
+
import type { ITransactor, GetBlockResults, ActionBlocks, ActionLineage, BlockLineage, BlockActionStatus, PendResult, CommitResult, PendRequest, BlockId, CommitRequest, BlockGets, IBlock, ActionId, ActionRev, ActionTransforms, StaleFailure, Transform, Transforms, ClusterNomineesResult, CollectionId } from "../index.js";
|
|
2
2
|
import { highestStaleAt, isOwnRevision } from "../network/stale-failure.js";
|
|
3
|
+
import { localDurability } from "../network/durability.js";
|
|
3
4
|
import { ensuredMap } from "../utility/ensured.js";
|
|
4
5
|
import { Latches } from "../utility/latches.js";
|
|
5
6
|
import { applyTransform, blockIdsForTransforms, transformForBlockId, emptyTransforms, concatTransform, transformsFromTransform } from "../transform/index.js";
|
|
@@ -164,6 +165,26 @@ export class TestTransactor implements ITransactor {
|
|
|
164
165
|
}));
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
/** One store, no replicas: every revision was applied here onto the one before it, so the
|
|
169
|
+
* revision index answers for lineage outright — mirroring `BlockStorage.lineageOf`, including
|
|
170
|
+
* the one way a later revision does NOT build on its predecessor (an insert replaces the block
|
|
171
|
+
* wholesale). The "cohort" is this store alone, which is what the durability says. */
|
|
172
|
+
async getLineage(ref: ActionBlocks & { rev: number }): Promise<ActionLineage> {
|
|
173
|
+
this.checkAvailable();
|
|
174
|
+
const blocks = ref.blockIds.map((blockId): BlockLineage => {
|
|
175
|
+
const blockState = this.blocks.get(blockId);
|
|
176
|
+
if (!blockState || blockState.latestRev < ref.rev) return 'behind';
|
|
177
|
+
if (blockState.revisionActions.get(ref.rev) !== ref.actionId) return 'excludes';
|
|
178
|
+
const replacedSince = Array.from(blockState.revisionActions.entries()).some(([rev, actionId]) =>
|
|
179
|
+
rev > ref.rev && blockState.committedActions.get(actionId)?.insert !== undefined);
|
|
180
|
+
return replacedSince ? 'unknown' : 'contains';
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
blocks,
|
|
184
|
+
...(blocks.length > 0 && blocks.every(lineage => lineage === 'contains') ? { durability: localDurability() } : {})
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
167
188
|
async pend(request: PendRequest): Promise<PendResult> {
|
|
168
189
|
this.checkAvailable();
|
|
169
190
|
const { actionId, transforms, policy, rev } = request;
|
|
@@ -174,6 +195,12 @@ export class TestTransactor implements ITransactor {
|
|
|
174
195
|
// storage is already at or past the requested revision, held by someone other than this same
|
|
175
196
|
// action. Reported as `staleAt` so a caller reading the shared harness sees the real shape.
|
|
176
197
|
const staleCandidates: StaleFailure['staleAt'][] = [];
|
|
198
|
+
// Blocks this SAME action already committed at exactly the requested revision — the durable
|
|
199
|
+
// half of a torn action, met again by its own retry. Mirrors StorageRepo.pend's `satisfied`
|
|
200
|
+
// set: such a block is neither a committed conflict nor a pending one, and no pending record
|
|
201
|
+
// is written for it (commit's own-revision arm below never promotes one, so it would never
|
|
202
|
+
// clear). It still rides in the returned `blockIds`, as in the real repo, so `cancel` covers it.
|
|
203
|
+
const satisfied = new Set<BlockId>();
|
|
177
204
|
|
|
178
205
|
// Check for conflicts (pending or committed based on rev/insert)
|
|
179
206
|
for (const blockId of blockIds) {
|
|
@@ -181,6 +208,11 @@ export class TestTransactor implements ITransactor {
|
|
|
181
208
|
const blockTransform = transformForBlockId(transforms, blockId);
|
|
182
209
|
if (!blockTransform) continue; // Should not happen
|
|
183
210
|
|
|
211
|
+
if (blockState && isOwnRevision(latestActionRev(blockState), rev, actionId)) {
|
|
212
|
+
satisfied.add(blockId);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
184
216
|
if (blockState) {
|
|
185
217
|
// Check for existing pending actions
|
|
186
218
|
if (blockState.pendingActions.size > 0) {
|
|
@@ -196,8 +228,9 @@ export class TestTransactor implements ITransactor {
|
|
|
196
228
|
// Mirrors StorageRepo.pend exactly: only a real revision race yields a
|
|
197
229
|
// meaningful `staleAt`. A rev-less pend reaches here as an insert collision
|
|
198
230
|
// (`checkRev` degraded to 0), where the block's revision answers a question
|
|
199
|
-
// nobody asked
|
|
200
|
-
|
|
231
|
+
// nobody asked. (Our own durable half of a torn action never reaches here —
|
|
232
|
+
// it was set aside as `satisfied` above.)
|
|
233
|
+
if (rev !== undefined) {
|
|
201
234
|
staleCandidates.push({ blockId, rev: blockState.latestRev });
|
|
202
235
|
}
|
|
203
236
|
// Collect conflicting committed actions
|
|
@@ -269,6 +302,7 @@ export class TestTransactor implements ITransactor {
|
|
|
269
302
|
|
|
270
303
|
// No fatal conflicts found, proceed to pend
|
|
271
304
|
for (const blockId of blockIds) {
|
|
305
|
+
if (satisfied.has(blockId)) continue;
|
|
272
306
|
const blockTransform = transformForBlockId(transforms, blockId);
|
|
273
307
|
if (blockTransform) {
|
|
274
308
|
const blockState = ensuredMap(this.blocks, blockId, () => newBlockState());
|
|
@@ -276,11 +310,13 @@ export class TestTransactor implements ITransactor {
|
|
|
276
310
|
}
|
|
277
311
|
}
|
|
278
312
|
|
|
279
|
-
// Return success, include pending list as per StorageRepo behavior
|
|
313
|
+
// Return success, include pending list as per StorageRepo behavior. The in-memory double is one
|
|
314
|
+
// node with no cohort, so its honest class is `local` — exactly what a bare StorageRepo answers.
|
|
280
315
|
return {
|
|
281
316
|
success: true,
|
|
282
317
|
pending: conflictingPendings,
|
|
283
|
-
blockIds
|
|
318
|
+
blockIds,
|
|
319
|
+
durability: localDurability()
|
|
284
320
|
} as PendResult;
|
|
285
321
|
}
|
|
286
322
|
|
|
@@ -310,10 +346,19 @@ export class TestTransactor implements ITransactor {
|
|
|
310
346
|
|
|
311
347
|
// --- Start of Critical Section (Simulated) ---
|
|
312
348
|
|
|
349
|
+
// Blocks this same action already committed at exactly this revision: an idempotent no-op,
|
|
350
|
+
// mirroring StorageRepo.commit's `alreadyDone` partition. They are neither stale nor in need
|
|
351
|
+
// of a pending record (pend wrote none for them — see `satisfied` there), which is what lets
|
|
352
|
+
// a torn action's retry roll its remaining blocks forward.
|
|
353
|
+
const alreadyDone = new Set(blockIds.filter(blockId => {
|
|
354
|
+
const blockState = this.blocks.get(blockId);
|
|
355
|
+
return blockState !== undefined && isOwnRevision(latestActionRev(blockState), rev, actionId);
|
|
356
|
+
}));
|
|
357
|
+
|
|
313
358
|
// Check for stale revisions
|
|
314
359
|
const staleBlocks = blockIds.filter(blockId => {
|
|
315
360
|
const blockState = this.blocks.get(blockId);
|
|
316
|
-
return blockState && blockState.latestRev >= rev;
|
|
361
|
+
return blockState && blockState.latestRev >= rev && !alreadyDone.has(blockId);
|
|
317
362
|
});
|
|
318
363
|
|
|
319
364
|
if (staleBlocks.length > 0) {
|
|
@@ -341,19 +386,17 @@ export class TestTransactor implements ITransactor {
|
|
|
341
386
|
transforms
|
|
342
387
|
}));
|
|
343
388
|
// Same rule as StorageRepo.commit's missedCommits branch: report the highest confirmed
|
|
344
|
-
// revision a stale block is already at
|
|
345
|
-
//
|
|
346
|
-
const staleAt = highestStaleAt(staleBlocks.map(blockId =>
|
|
347
|
-
|
|
348
|
-
return isOwnRevision(latestActionRev(blockState), rev, actionId)
|
|
349
|
-
? undefined
|
|
350
|
-
: { blockId, rev: blockState.latestRev };
|
|
351
|
-
}));
|
|
389
|
+
// revision a stale block is already at. A block held by this very action never reaches
|
|
390
|
+
// here — it was partitioned out as `alreadyDone` above.
|
|
391
|
+
const staleAt = highestStaleAt(staleBlocks.map(blockId =>
|
|
392
|
+
({ blockId, rev: this.blocks.get(blockId)!.latestRev })));
|
|
352
393
|
return { success: false, missing, ...(staleAt ? { staleAt } : {}) };
|
|
353
394
|
}
|
|
354
395
|
|
|
355
|
-
|
|
356
|
-
|
|
396
|
+
const toCommit = blockIds.filter(blockId => !alreadyDone.has(blockId));
|
|
397
|
+
|
|
398
|
+
// Verify all blocks that still need committing have the pending action
|
|
399
|
+
for (const blockId of toCommit) {
|
|
357
400
|
const blockState = this.blocks.get(blockId);
|
|
358
401
|
if (!blockState || !blockState.pendingActions.has(actionId)) {
|
|
359
402
|
return {
|
|
@@ -364,7 +407,7 @@ export class TestTransactor implements ITransactor {
|
|
|
364
407
|
}
|
|
365
408
|
|
|
366
409
|
// Commit the action for each block
|
|
367
|
-
for (const blockId of
|
|
410
|
+
for (const blockId of toCommit) {
|
|
368
411
|
const blockState = this.blocks.get(blockId)!;
|
|
369
412
|
const transform = blockState.pendingActions.get(actionId)!;
|
|
370
413
|
|
|
@@ -397,7 +440,7 @@ export class TestTransactor implements ITransactor {
|
|
|
397
440
|
|
|
398
441
|
// --- End of Critical Section (Simulated) ---
|
|
399
442
|
|
|
400
|
-
return { success: true };
|
|
443
|
+
return { success: true, durability: localDurability() };
|
|
401
444
|
|
|
402
445
|
} finally {
|
|
403
446
|
// Release locks in reverse order
|
|
@@ -480,6 +523,7 @@ export abstract class DelegatingTransactor implements ITransactor {
|
|
|
480
523
|
pend(r: PendRequest): Promise<PendResult> { return this.inner.pend(r); }
|
|
481
524
|
cancel(a: ActionBlocks): Promise<void> { return this.inner.cancel(a); }
|
|
482
525
|
commit(r: CommitRequest): Promise<CommitResult> { return this.inner.commit(r); }
|
|
526
|
+
getLineage(ref: ActionBlocks & { rev: number }): Promise<ActionLineage> { return this.inner.getLineage(ref); }
|
|
483
527
|
|
|
484
528
|
get queryClusterNominees(): ((blockId: BlockId) => Promise<ClusterNomineesResult>) | undefined {
|
|
485
529
|
return this.inner.queryClusterNominees?.bind(this.inner);
|
|
@@ -519,16 +563,29 @@ export class FlakyCommitTransactor extends DelegatingTransactor {
|
|
|
519
563
|
}
|
|
520
564
|
|
|
521
565
|
/**
|
|
522
|
-
* Commits durably on the inner {@link TestTransactor}, then reports a stale
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
566
|
+
* Commits the WHOLE action durably on the inner {@link TestTransactor}, then reports a stale
|
|
567
|
+
* failure anyway — a write that fully landed but whose writer was told it failed.
|
|
568
|
+
*
|
|
569
|
+
* This is NOT the shape `NetworkTransactor.commit` produces when it refuses a write whose log tail
|
|
570
|
+
* landed: there the tail is committed BEFORE the sweep of the remaining blocks, so the blocks after
|
|
571
|
+
* the tail did NOT land. That shape is {@link TailLandsButReportsStale}, and it is the one that
|
|
572
|
+
* matters in production. This double makes "my own log entry is visible" and "my action is fully
|
|
573
|
+
* durable" the same thing, so on its own it cannot tell a writer that finishes its half-landed
|
|
574
|
+
* action from one that merely assumes it is finished. Keep it for the case it does model — every
|
|
575
|
+
* block already holds the action's revision, so the writer's completion pass has nothing left to
|
|
576
|
+
* land and must not duplicate or move the entry.
|
|
527
577
|
*
|
|
528
578
|
* Safe against the inner transactor's bookkeeping because `TransactorSource.transact` cancels
|
|
529
579
|
* the pend on the reported failure and {@link TestTransactor.cancel} only deletes PENDING records
|
|
530
580
|
* — the real commit already promoted them, so the cancel is a no-op.
|
|
531
581
|
*
|
|
582
|
+
* `afterLanding`, when given, runs a competing writer ONCE, after the first masked commit has
|
|
583
|
+
* landed and before its failure is reported: the rival reads the landed write and commits the next
|
|
584
|
+
* revision ON TOP of it, so by the time the writer looks again every block it wrote has moved past
|
|
585
|
+
* its revision while still containing it. Safe to await from inside `commit` here (unlike
|
|
586
|
+
* {@link TailLandsButReportsStale}'s rival, which has to wait for the cancel): the whole action
|
|
587
|
+
* landed, so it left no pending record for the rival's pend to collide with.
|
|
588
|
+
*
|
|
532
589
|
* Used by both write paths' own-entry regression suites (collection-own-action-replay.spec.ts and
|
|
533
590
|
* coordinator-own-action-replay.spec.ts); it lives here so the two cannot drift apart.
|
|
534
591
|
*/
|
|
@@ -537,10 +594,12 @@ export class CommitLandsButReportsStale extends DelegatingTransactor {
|
|
|
537
594
|
private injections: number;
|
|
538
595
|
/** Commits that actually landed on the inner transactor (masked or not). */
|
|
539
596
|
landedCommits = 0;
|
|
597
|
+
private rivalDue: boolean;
|
|
540
598
|
|
|
541
|
-
constructor(inner: TestTransactor, injections = 1) {
|
|
599
|
+
constructor(inner: TestTransactor, injections = 1, private readonly afterLanding?: RivalWrite) {
|
|
542
600
|
super(inner);
|
|
543
601
|
this.injections = injections;
|
|
602
|
+
this.rivalDue = afterLanding !== undefined;
|
|
544
603
|
}
|
|
545
604
|
|
|
546
605
|
override async commit(request: CommitRequest): Promise<CommitResult> {
|
|
@@ -548,6 +607,10 @@ export class CommitLandsButReportsStale extends DelegatingTransactor {
|
|
|
548
607
|
if (result.success) {
|
|
549
608
|
this.landedCommits++;
|
|
550
609
|
if (this.injections-- > 0) {
|
|
610
|
+
if (this.rivalDue) {
|
|
611
|
+
this.rivalDue = false;
|
|
612
|
+
await this.afterLanding!(this.inner);
|
|
613
|
+
}
|
|
551
614
|
return { success: false, conflict: true, reason: 'stale commit: injected torn-action conflict' };
|
|
552
615
|
}
|
|
553
616
|
}
|
|
@@ -555,6 +618,64 @@ export class CommitLandsButReportsStale extends DelegatingTransactor {
|
|
|
555
618
|
}
|
|
556
619
|
}
|
|
557
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Lands ONLY the action's log tail on the inner {@link TestTransactor}, then reports a retryable
|
|
623
|
+
* failure — the shape `NetworkTransactor.commit` actually produces for a half-landed write. It
|
|
624
|
+
* commits the tail first and sweeps the remaining blocks only if the tail answered success, and a
|
|
625
|
+
* failed tail answer does not mean the tail is absent: the coordinator's durability gate answers
|
|
626
|
+
* `commit-not-durable` whenever fewer than a majority of the cohort hold the revision, even though
|
|
627
|
+
* some members stored it. So the writer is told "failed" over a log entry that is durable, while
|
|
628
|
+
* none of the entry's other blocks were ever committed — and the writer's own cancel then drops
|
|
629
|
+
* their pending records.
|
|
630
|
+
*
|
|
631
|
+
* A commit carrying nothing but the tail is delegated untouched (there is nothing to abandon), and
|
|
632
|
+
* does not consume an injection.
|
|
633
|
+
*
|
|
634
|
+
* `afterCancel`, when given, runs a competing writer ONCE, right after the writer's cancel of the
|
|
635
|
+
* torn attempt has been delegated. It is on `cancel` and not inside `commit` for the reason
|
|
636
|
+
* {@link CompetingWriterTransactor} documents: until that cancel lands, the torn action's data
|
|
637
|
+
* blocks still carry its pending records, a rival pending with policy `'r'` collides with them, and
|
|
638
|
+
* awaiting that rival from inside the writer's own commit is a livelock.
|
|
639
|
+
*/
|
|
640
|
+
export class TailLandsButReportsStale extends DelegatingTransactor {
|
|
641
|
+
/** Remaining number of commits to tear. */
|
|
642
|
+
private injections: number;
|
|
643
|
+
/** Commits torn so far: the tail landed, every other block of the action was abandoned. */
|
|
644
|
+
tears = 0;
|
|
645
|
+
/** Commits delegated whole that succeeded on the inner transactor. */
|
|
646
|
+
landedCommits = 0;
|
|
647
|
+
private rivalDue = false;
|
|
648
|
+
|
|
649
|
+
constructor(inner: TestTransactor, injections = 1, private readonly afterCancel?: RivalWrite) {
|
|
650
|
+
super(inner);
|
|
651
|
+
this.injections = injections;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
override async commit(request: CommitRequest): Promise<CommitResult> {
|
|
655
|
+
if (this.injections > 0 && request.blockIds.some(blockId => blockId !== request.tailId)) {
|
|
656
|
+
const tail = await this.inner.commit({ ...request, blockIds: [request.tailId] });
|
|
657
|
+
// A tail that itself lost is an ordinary loss — nothing is torn, so report it verbatim
|
|
658
|
+
// and keep the injection for a commit that can actually tear.
|
|
659
|
+
if (!tail.success) return tail;
|
|
660
|
+
this.injections--;
|
|
661
|
+
this.tears++;
|
|
662
|
+
this.rivalDue = this.afterCancel !== undefined;
|
|
663
|
+
return { success: false, conflict: true, reason: 'commit-not-durable: injected tail-only landing' };
|
|
664
|
+
}
|
|
665
|
+
const result = await this.inner.commit(request);
|
|
666
|
+
if (result.success) this.landedCommits++;
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
override async cancel(actionRef: ActionBlocks): Promise<void> {
|
|
671
|
+
await this.inner.cancel(actionRef);
|
|
672
|
+
if (this.rivalDue) {
|
|
673
|
+
this.rivalDue = false;
|
|
674
|
+
await this.afterCancel!(this.inner);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
558
679
|
/** A competing writer: a real write driven against the UNWRAPPED transactor, so its own
|
|
559
680
|
* pend/commit calls are invisible to {@link CompetingWriterTransactor}'s counters and cannot
|
|
560
681
|
* re-trigger the interception. See {@link commitRivalTreeWrite} for the usual implementation. */
|
|
@@ -793,6 +914,13 @@ function latestMaterializedAt(blockState: BlockState, maxRev: number): { block:
|
|
|
793
914
|
/**
|
|
794
915
|
* `applyTransform` over cloned inputs, so a returned block never aliases stored state.
|
|
795
916
|
*
|
|
917
|
+
* The `insert` clone here is not redundant with `transformForBlockId`'s: callers pass `Transform`
|
|
918
|
+
* values pulled straight out of `blockState.pendingActions` (a `Map<ActionId, Transform>` written
|
|
919
|
+
* once by `pend()`), which can be `get()`'d more than once and read again at commit time before
|
|
920
|
+
* being superseded. `applyTransform` mutates `transform.insert` in place when `updates` ride
|
|
921
|
+
* along, so cloning immediately before each call is what keeps that stored entry pristine across
|
|
922
|
+
* repeated reads.
|
|
923
|
+
*
|
|
796
924
|
* An absent base is NOT a short circuit: an insert needs no base — `applyTransform` adopts it as
|
|
797
925
|
* the block. Bailing out on `!block` made this double silently drop a pending-only insert read
|
|
798
926
|
* through the pending overlay, the one shape `StorageRepo.get` serves with content but no
|