@adobe/data 0.9.61 → 0.9.63
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/ecs/archetype/archetype.d.ts +9 -2
- package/dist/ecs/archetype/archetype.js.map +1 -1
- package/dist/ecs/archetype/create-archetype.js +5 -2
- package/dist/ecs/archetype/create-archetype.js.map +1 -1
- package/dist/ecs/database/concurrency/concurrency-strategy.d.ts +86 -0
- package/dist/ecs/database/concurrency/concurrency-strategy.js +3 -0
- package/dist/ecs/database/concurrency/concurrency-strategy.js.map +1 -0
- package/dist/ecs/database/concurrency/immediate-concurrency.d.ts +22 -0
- package/dist/ecs/database/concurrency/immediate-concurrency.js +74 -0
- package/dist/ecs/database/concurrency/immediate-concurrency.js.map +1 -0
- package/dist/ecs/database/concurrency/index.d.ts +4 -0
- package/dist/ecs/database/concurrency/index.js +5 -0
- package/dist/ecs/database/concurrency/index.js.map +1 -0
- package/dist/ecs/database/concurrency/rebase-replay-concurrency.d.ts +20 -0
- package/dist/ecs/database/concurrency/rebase-replay-concurrency.js +34 -0
- package/dist/ecs/database/concurrency/rebase-replay-concurrency.js.map +1 -0
- package/dist/ecs/database/concurrency/roll-forward-concurrency.d.ts +33 -0
- package/dist/ecs/database/concurrency/roll-forward-concurrency.js +115 -0
- package/dist/ecs/database/concurrency/roll-forward-concurrency.js.map +1 -0
- package/dist/ecs/database/concurrency/roll-forward-concurrency.test.d.ts +1 -0
- package/dist/ecs/database/concurrency/roll-forward-concurrency.test.js +136 -0
- package/dist/ecs/database/concurrency/roll-forward-concurrency.test.js.map +1 -0
- package/dist/ecs/database/create-plugin.d.ts +1 -1
- package/dist/ecs/database/database.d.ts +29 -7
- package/dist/ecs/database/database.index.performance.test.js +5 -5
- package/dist/ecs/database/database.index.performance.test.js.map +1 -1
- package/dist/ecs/database/database.index.test.js +556 -51
- package/dist/ecs/database/database.index.test.js.map +1 -1
- package/dist/ecs/database/database.index.type-test.js +34 -10
- package/dist/ecs/database/database.index.type-test.js.map +1 -1
- package/dist/ecs/database/database.js.map +1 -1
- package/dist/ecs/database/database.reset.test.js +2 -1
- package/dist/ecs/database/database.reset.test.js.map +1 -1
- package/dist/ecs/database/index-registry/create-index-registry.d.ts +38 -9
- package/dist/ecs/database/index-registry/create-index-registry.js +67 -20
- package/dist/ecs/database/index-registry/create-index-registry.js.map +1 -1
- package/dist/ecs/database/index-registry/create-index.d.ts +10 -2
- package/dist/ecs/database/index-registry/create-index.js +44 -47
- package/dist/ecs/database/index-registry/create-index.js.map +1 -1
- package/dist/ecs/database/index.d.ts +1 -0
- package/dist/ecs/database/index.js +1 -0
- package/dist/ecs/database/index.js.map +1 -1
- package/dist/ecs/database/observe-index-entities.d.ts +26 -0
- package/dist/ecs/database/observe-index-entities.js +66 -0
- package/dist/ecs/database/observe-index-entities.js.map +1 -0
- package/dist/ecs/database/observed/create-observed-database.js +1 -1
- package/dist/ecs/database/observed/create-observed-database.js.map +1 -1
- package/dist/ecs/database/observed/observed-database.d.ts +1 -1
- package/dist/ecs/database/public/create-database.d.ts +12 -27
- package/dist/ecs/database/public/create-database.js +131 -37
- package/dist/ecs/database/public/create-database.js.map +1 -1
- package/dist/ecs/database/public/create-database.test.js +8 -7
- package/dist/ecs/database/public/create-database.test.js.map +1 -1
- package/dist/ecs/database/public/create-transaction-dispatcher.d.ts +4 -2
- package/dist/ecs/database/public/create-transaction-dispatcher.js +2 -8
- package/dist/ecs/database/public/create-transaction-dispatcher.js.map +1 -1
- package/dist/ecs/database/reconciling/create-rebase-replay-applier.d.ts +26 -0
- package/dist/ecs/database/reconciling/create-rebase-replay-applier.js +121 -0
- package/dist/ecs/database/reconciling/create-rebase-replay-applier.js.map +1 -0
- package/dist/ecs/database/reconciling/create-reconciling-database.d.ts +11 -0
- package/dist/ecs/database/reconciling/create-reconciling-database.js +28 -137
- package/dist/ecs/database/reconciling/create-reconciling-database.js.map +1 -1
- package/dist/ecs/database/transactional-store/create-transactional-store.js.map +1 -1
- package/dist/ecs/database/transactional-store/transactional-store.d.ts +1 -1
- package/dist/ecs/entity-location-table/create-entity-location-table.js +2 -2
- package/dist/ecs/entity-location-table/create-entity-location-table.js.map +1 -1
- package/dist/ecs/entity-location-table/entity-location-table.d.ts +7 -1
- package/dist/ecs/store/core/core.d.ts +7 -1
- package/dist/ecs/store/core/create-core.js +3 -3
- package/dist/ecs/store/core/create-core.js.map +1 -1
- package/dist/ecs/store/core/select-entities.js +6 -3
- package/dist/ecs/store/core/select-entities.js.map +1 -1
- package/dist/ecs/store/index-types.d.ts +53 -17
- package/dist/ecs/store/public/create-store.js +32 -6
- package/dist/ecs/store/public/create-store.js.map +1 -1
- package/dist/ecs/store/public/create-store.test.js +25 -0
- package/dist/ecs/store/public/create-store.test.js.map +1 -1
- package/dist/ecs/store/store.d.ts +10 -3
- package/dist/ecs/store/store.js.map +1 -1
- package/dist/functions/compare.d.ts +13 -0
- package/dist/functions/compare.js +15 -0
- package/dist/functions/compare.js.map +1 -0
- package/dist/functions/compare.test.d.ts +1 -0
- package/dist/functions/compare.test.js +24 -0
- package/dist/functions/compare.test.js.map +1 -0
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.js +1 -0
- package/dist/functions/index.js.map +1 -1
- package/dist/test-setup.js +2 -2
- package/dist/test-setup.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/references/data-lit/package.json +1 -1
- package/references/data-lit-tictactoe/package.json +1 -1
- package/references/data-react/package.json +1 -1
- package/references/data-react-hello/package.json +1 -1
- package/references/data-react-pixie/package.json +1 -1
|
@@ -20,12 +20,19 @@ interface BaseArchetype {
|
|
|
20
20
|
}
|
|
21
21
|
export interface ReadonlyArchetype<C extends RequiredComponents> extends BaseArchetype, ReadonlyTable<C> {
|
|
22
22
|
readonly components: ComponentSet<StringKeyof<C>>;
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Serialize the archetype. When `copy` is true each column buffer is
|
|
25
|
+
* detached (`.copy()`) so the snapshot survives later mutation of the live
|
|
26
|
+
* archetype; otherwise the snapshot references the live column buffers
|
|
27
|
+
* (faster, but only valid until the next mutation).
|
|
28
|
+
*/
|
|
29
|
+
toData: (copy?: boolean) => unknown;
|
|
24
30
|
}
|
|
25
31
|
export interface Archetype<C extends RequiredComponents = RequiredComponents> extends BaseArchetype, Table<C> {
|
|
26
32
|
readonly components: ComponentSet<StringKeyof<C>>;
|
|
27
33
|
insert: <T extends EntityInsertValues<C>>(rowData: Exact<EntityInsertValues<C>, T>) => Entity;
|
|
28
|
-
|
|
34
|
+
/** See {@link ReadonlyArchetype.toData}. */
|
|
35
|
+
toData: (copy?: boolean) => unknown;
|
|
29
36
|
fromData: (data: unknown) => void;
|
|
30
37
|
}
|
|
31
38
|
export type FromArchetype<T> = T extends ReadonlyArchetype<infer C> ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archetype.js","sourceRoot":"","sources":["../../../src/ecs/archetype/archetype.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archetype.js","sourceRoot":"","sources":["../../../src/ecs/archetype/archetype.ts"],"names":[],"mappings":"AAsDA,gDAAgD;AAChD,CAAC;IAKG,2BAA2B;IAC3B,MAAM,eAAe,GAAG,CAAC,IAAmB,EAAE,EAAE;QAC5C,MAAM,SAAS,GAAoB,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB;IAC7C,CAAC,CAAC;IAEF,2CAA2C;IAC3C,MAAM,iBAAiB,GAAG,CAAC,IAAmB,EAAE,EAAE;QAC9C,MAAM,WAAW,GAAoB,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAChG,oDAAoD;QACpD,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -134,8 +134,11 @@ export const createArchetype = (components, id, entityLocationTable) => {
|
|
|
134
134
|
...table,
|
|
135
135
|
components: componentSet,
|
|
136
136
|
insert: createEntity,
|
|
137
|
-
toData: () => ({
|
|
138
|
-
columns:
|
|
137
|
+
toData: (copy = false) => ({
|
|
138
|
+
columns: copy
|
|
139
|
+
? Object.fromEntries(Object.entries(archetype.columns)
|
|
140
|
+
.map(([name, column]) => [name, column.copy()]))
|
|
141
|
+
: archetype.columns,
|
|
139
142
|
rowCount: archetype.rowCount,
|
|
140
143
|
rowCapacity: archetype.rowCapacity,
|
|
141
144
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-archetype.js","sourceRoot":"","sources":["../../../src/ecs/archetype/create-archetype.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"create-archetype.js","sourceRoot":"","sources":["../../../src/ecs/archetype/create-archetype.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Entity } from "../../entity/entity.js";
|
|
2
|
+
import type { TransactionContext, TransactionResult } from "../transactional-store/index.js";
|
|
3
|
+
import type { TransactionEnvelope } from "../reconciling/reconciling-database.js";
|
|
4
|
+
/**
|
|
5
|
+
* The interface every concurrency strategy must satisfy.
|
|
6
|
+
*
|
|
7
|
+
* A strategy encapsulates all decisions about how locally-initiated
|
|
8
|
+
* transactions are applied and how inbound envelopes (from a sync service
|
|
9
|
+
* or another local source) are reconciled against the current store state.
|
|
10
|
+
*
|
|
11
|
+
* Two built-in strategies are provided:
|
|
12
|
+
*
|
|
13
|
+
* - `createImmediateConcurrency()` — commits apply immediately, no
|
|
14
|
+
* rollback queue. Use this as the base database for external wrappers
|
|
15
|
+
* (e.g. a collaborative-editing layer that manages its own replay buffer).
|
|
16
|
+
*
|
|
17
|
+
* - `createRebaseReplayConcurrency(userId)` — commits apply locally as
|
|
18
|
+
* transients and wait for the sync server's echoed committed envelope to
|
|
19
|
+
* promote them. Full rollback-and-replay queue, cross-peer id-determinism.
|
|
20
|
+
*/
|
|
21
|
+
export interface ConcurrencyStrategy {
|
|
22
|
+
/**
|
|
23
|
+
* When true, the dispatcher applies "commit" intents as transients
|
|
24
|
+
* (negative time) and waits for a server echo to promote them.
|
|
25
|
+
* When false, commits apply immediately with positive time.
|
|
26
|
+
*/
|
|
27
|
+
readonly deferredCommit: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Peer/session identifier stamped on every outbound envelope.
|
|
30
|
+
* Required for strategies that participate in a multi-peer sync protocol.
|
|
31
|
+
*/
|
|
32
|
+
readonly userId?: number | string;
|
|
33
|
+
/**
|
|
34
|
+
* Process a transaction envelope. Called by the dispatcher for each
|
|
35
|
+
* locally-initiated transaction and by sync services for inbound commits.
|
|
36
|
+
* Does NOT fire `observe.envelopes` (that is the dispatcher's job).
|
|
37
|
+
*/
|
|
38
|
+
readonly apply: (envelope: TransactionEnvelope) => TransactionResult<unknown> | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Cancel a pending transient by compound (userId, id) key.
|
|
41
|
+
* No-op for strategies that have no transient queue.
|
|
42
|
+
*/
|
|
43
|
+
readonly cancel: (id: number, userId?: number | string) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called by `db.reset()` before the store is cleared.
|
|
46
|
+
* Strategies with a transient queue should clear it here.
|
|
47
|
+
*/
|
|
48
|
+
readonly onReset: () => void;
|
|
49
|
+
/**
|
|
50
|
+
* Called immediately before `db.toData()` serializes the store.
|
|
51
|
+
* Strategies with a transient queue should roll back transients here so
|
|
52
|
+
* the snapshot contains only committed state.
|
|
53
|
+
*
|
|
54
|
+
* When this hook (together with `onAfterToData`) is present, `db.toData()`
|
|
55
|
+
* serializes a detached copy of the store between the two hooks, so the
|
|
56
|
+
* `onAfterToData` replay cannot corrupt the returned snapshot. See
|
|
57
|
+
* `Database.toData()`.
|
|
58
|
+
*/
|
|
59
|
+
readonly onBeforeToData?: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Called immediately after `db.toData()` serializes the store.
|
|
62
|
+
* Strategies that roll back in `onBeforeToData` should replay here to
|
|
63
|
+
* restore the in-progress transient state.
|
|
64
|
+
*/
|
|
65
|
+
readonly onAfterToData?: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* Called immediately after `db.fromData()` loads a snapshot into the
|
|
68
|
+
* store. Strategies with a transient queue should replay pending
|
|
69
|
+
* transients here so in-progress work is visible after a data load.
|
|
70
|
+
*/
|
|
71
|
+
readonly onAfterFromData?: () => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Factory that creates a `ConcurrencyStrategy` bound to the given execute
|
|
75
|
+
* and transaction-lookup functions. Called once per database at construction.
|
|
76
|
+
*
|
|
77
|
+
* `execute` is the observed database's execute — it applies a transaction,
|
|
78
|
+
* fires observers, and returns the mutation record.
|
|
79
|
+
*
|
|
80
|
+
* `getTransaction` looks up a registered transaction function by name,
|
|
81
|
+
* returning undefined when the name has not yet been registered via extend.
|
|
82
|
+
*/
|
|
83
|
+
export type ConcurrencyStrategyFactory = (execute: (fn: (ctx: TransactionContext<any, any, any>) => void | Entity, options?: {
|
|
84
|
+
transient?: boolean;
|
|
85
|
+
userId?: number | string;
|
|
86
|
+
}) => TransactionResult<unknown>, getTransaction: (name: string) => ((ctx: TransactionContext<any, any, any>, args: unknown) => void | Entity) | undefined) => ConcurrencyStrategy;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency-strategy.js","sourceRoot":"","sources":["../../../../src/ecs/database/concurrency/concurrency-strategy.ts"],"names":[],"mappings":"AAAA,uDAAuD"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ConcurrencyStrategyFactory } from "./concurrency-strategy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Lighter-weight concurrency strategy that applies commits and transients
|
|
4
|
+
* immediately with no global rollback-and-replay cycle.
|
|
5
|
+
*
|
|
6
|
+
* Key differences from {@link createRebaseReplayConcurrency}:
|
|
7
|
+
*
|
|
8
|
+
* - No deferred-commit mode — `db.transactions.X(args)` commits
|
|
9
|
+
* synchronously with positive time.
|
|
10
|
+
* - No global rebase: when an inbound committed envelope arrives, it is
|
|
11
|
+
* applied directly without rolling back other pending transients. This
|
|
12
|
+
* means entity-id allocation is NOT deterministic across peers under
|
|
13
|
+
* concurrent edits — use this only when there is a single writer or
|
|
14
|
+
* when an external layer manages replay (e.g. a collaborative-editing
|
|
15
|
+
* wrapper).
|
|
16
|
+
* - Async generator transients are still supported: each yield applies
|
|
17
|
+
* the intermediate state immediately and rolls it back when the next
|
|
18
|
+
* yield or the final commit arrives. Cancel rolls back the last
|
|
19
|
+
* applied transient for that transaction. Transients from different
|
|
20
|
+
* concurrent transactions do NOT interact (no cross-transaction rebase).
|
|
21
|
+
*/
|
|
22
|
+
export declare const createImmediateConcurrency: () => ConcurrencyStrategyFactory;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { applyOperations } from "../transactional-store/apply-operations.js";
|
|
3
|
+
/**
|
|
4
|
+
* Lighter-weight concurrency strategy that applies commits and transients
|
|
5
|
+
* immediately with no global rollback-and-replay cycle.
|
|
6
|
+
*
|
|
7
|
+
* Key differences from {@link createRebaseReplayConcurrency}:
|
|
8
|
+
*
|
|
9
|
+
* - No deferred-commit mode — `db.transactions.X(args)` commits
|
|
10
|
+
* synchronously with positive time.
|
|
11
|
+
* - No global rebase: when an inbound committed envelope arrives, it is
|
|
12
|
+
* applied directly without rolling back other pending transients. This
|
|
13
|
+
* means entity-id allocation is NOT deterministic across peers under
|
|
14
|
+
* concurrent edits — use this only when there is a single writer or
|
|
15
|
+
* when an external layer manages replay (e.g. a collaborative-editing
|
|
16
|
+
* wrapper).
|
|
17
|
+
* - Async generator transients are still supported: each yield applies
|
|
18
|
+
* the intermediate state immediately and rolls it back when the next
|
|
19
|
+
* yield or the final commit arrives. Cancel rolls back the last
|
|
20
|
+
* applied transient for that transaction. Transients from different
|
|
21
|
+
* concurrent transactions do NOT interact (no cross-transaction rebase).
|
|
22
|
+
*/
|
|
23
|
+
export const createImmediateConcurrency = () => (execute, getTransaction) => {
|
|
24
|
+
// Per-transaction store of the last-applied transient result, keyed
|
|
25
|
+
// by compound `"userId:id"`. Used to roll back a transient when a
|
|
26
|
+
// newer transient, commit, or cancel arrives for the same transaction.
|
|
27
|
+
const pending = new Map();
|
|
28
|
+
const key = (id, userId) => `${userId}:${id}`;
|
|
29
|
+
const rollbackPending = (id, userId) => {
|
|
30
|
+
const k = key(id, userId);
|
|
31
|
+
const prior = pending.get(k);
|
|
32
|
+
if (prior) {
|
|
33
|
+
execute(t => applyOperations(t, prior.undo), { transient: true, userId: undefined });
|
|
34
|
+
pending.delete(k);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
deferredCommit: false,
|
|
39
|
+
apply(envelope) {
|
|
40
|
+
const { id, userId, time, args } = envelope;
|
|
41
|
+
if (time === 0) {
|
|
42
|
+
rollbackPending(id, userId);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (time < 0) {
|
|
46
|
+
// Roll back the previous transient for this transaction (if any),
|
|
47
|
+
// then apply the new one. No interaction with other transactions.
|
|
48
|
+
rollbackPending(id, userId);
|
|
49
|
+
const fn = getTransaction(envelope.name);
|
|
50
|
+
if (!fn)
|
|
51
|
+
throw new Error(`Unknown transaction: ${envelope.name}`);
|
|
52
|
+
const result = execute(t => fn(t, args), { transient: true, userId });
|
|
53
|
+
const isNoOp = result.redo.length === 0 && result.undo.length === 0;
|
|
54
|
+
if (!isNoOp)
|
|
55
|
+
pending.set(key(id, userId), result);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
// Positive time: commit. Roll back any pending transient for this
|
|
59
|
+
// transaction, then apply the committed state.
|
|
60
|
+
rollbackPending(id, userId);
|
|
61
|
+
const fn = getTransaction(envelope.name);
|
|
62
|
+
if (!fn)
|
|
63
|
+
throw new Error(`Unknown transaction: ${envelope.name}`);
|
|
64
|
+
return execute(t => fn(t, args), { transient: false, userId });
|
|
65
|
+
},
|
|
66
|
+
cancel(id, userId) {
|
|
67
|
+
rollbackPending(id, userId);
|
|
68
|
+
},
|
|
69
|
+
onReset() {
|
|
70
|
+
pending.clear();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=immediate-concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"immediate-concurrency.js","sourceRoot":"","sources":["../../../../src/ecs/database/concurrency/immediate-concurrency.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { ConcurrencyStrategy, ConcurrencyStrategyFactory } from "./concurrency-strategy.js";
|
|
2
|
+
export { createImmediateConcurrency } from "./immediate-concurrency.js";
|
|
3
|
+
export { createRebaseReplayConcurrency } from "./rebase-replay-concurrency.js";
|
|
4
|
+
export { createRollForwardConcurrency } from "./roll-forward-concurrency.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
export { createImmediateConcurrency } from "./immediate-concurrency.js";
|
|
3
|
+
export { createRebaseReplayConcurrency } from "./rebase-replay-concurrency.js";
|
|
4
|
+
export { createRollForwardConcurrency } from "./roll-forward-concurrency.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/ecs/database/concurrency/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAGvD,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAC/E,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ConcurrencyStrategyFactory } from "./concurrency-strategy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Concurrency strategy that implements the optimistic rebase-replay protocol
|
|
4
|
+
* required for multi-peer synchronisation.
|
|
5
|
+
*
|
|
6
|
+
* Behaviour:
|
|
7
|
+
* - Local "commit" intents are applied as transients (negative time) and
|
|
8
|
+
* wait for the sync server's echoed committed envelope to promote them
|
|
9
|
+
* via rollback-and-replay. This is required for cross-peer entity-id
|
|
10
|
+
* determinism under concurrent edits.
|
|
11
|
+
* - Every outbound envelope is stamped with `userId` so the reconciler's
|
|
12
|
+
* compound `(userId, id)` key keeps concurrent peers' id counters from
|
|
13
|
+
* colliding.
|
|
14
|
+
* - `onBeforeToData` / `onAfterToData` roll back and replay the transient
|
|
15
|
+
* queue around serialisation so snapshots always contain only committed state.
|
|
16
|
+
*
|
|
17
|
+
* @param userId Stable peer/session identifier unique across all peers
|
|
18
|
+
* sharing the same sync server.
|
|
19
|
+
*/
|
|
20
|
+
export declare const createRebaseReplayConcurrency: (userId: number | string) => ConcurrencyStrategyFactory;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { createRebaseReplayApplier } from "../reconciling/create-rebase-replay-applier.js";
|
|
3
|
+
/**
|
|
4
|
+
* Concurrency strategy that implements the optimistic rebase-replay protocol
|
|
5
|
+
* required for multi-peer synchronisation.
|
|
6
|
+
*
|
|
7
|
+
* Behaviour:
|
|
8
|
+
* - Local "commit" intents are applied as transients (negative time) and
|
|
9
|
+
* wait for the sync server's echoed committed envelope to promote them
|
|
10
|
+
* via rollback-and-replay. This is required for cross-peer entity-id
|
|
11
|
+
* determinism under concurrent edits.
|
|
12
|
+
* - Every outbound envelope is stamped with `userId` so the reconciler's
|
|
13
|
+
* compound `(userId, id)` key keeps concurrent peers' id counters from
|
|
14
|
+
* colliding.
|
|
15
|
+
* - `onBeforeToData` / `onAfterToData` roll back and replay the transient
|
|
16
|
+
* queue around serialisation so snapshots always contain only committed state.
|
|
17
|
+
*
|
|
18
|
+
* @param userId Stable peer/session identifier unique across all peers
|
|
19
|
+
* sharing the same sync server.
|
|
20
|
+
*/
|
|
21
|
+
export const createRebaseReplayConcurrency = (userId) => (...args) => {
|
|
22
|
+
const applier = createRebaseReplayApplier(...args);
|
|
23
|
+
return {
|
|
24
|
+
deferredCommit: true,
|
|
25
|
+
userId,
|
|
26
|
+
apply: applier.apply,
|
|
27
|
+
cancel: applier.cancel,
|
|
28
|
+
onReset: applier.onReset,
|
|
29
|
+
onBeforeToData: applier.rollbackAllTransients,
|
|
30
|
+
onAfterToData: applier.replayAllTransients,
|
|
31
|
+
onAfterFromData: applier.replayAllTransients,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=rebase-replay-concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rebase-replay-concurrency.js","sourceRoot":"","sources":["../../../../src/ecs/database/concurrency/rebase-replay-concurrency.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,gDAAgD,CAAC;AAG3F;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,MAAuB,EAA8B,EAAE,CACjG,CAAC,GAAG,IAAI,EAAuB,EAAE;IAC7B,MAAM,OAAO,GAAG,yBAAyB,CAAC,GAAG,IAAI,CAAC,CAAC;IACnD,OAAO;QACH,cAAc,EAAE,IAAI;QACpB,MAAM;QACN,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,cAAc,EAAE,OAAO,CAAC,qBAAqB;QAC7C,aAAa,EAAE,OAAO,CAAC,mBAAmB;QAC1C,eAAe,EAAE,OAAO,CAAC,mBAAmB;KAC/C,CAAC;AACN,CAAC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ConcurrencyStrategyFactory } from "./concurrency-strategy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Roll-forward reconciliation: a client-side optimistic concurrency model for
|
|
4
|
+
* collaborative / multi-peer editing. Provided to prove the
|
|
5
|
+
* {@link ConcurrencyStrategy} seam can host a fundamentally different
|
|
6
|
+
* reconciliation engine than {@link createRebaseReplayConcurrency} — it is
|
|
7
|
+
* not currently wired into a real transport.
|
|
8
|
+
*
|
|
9
|
+
* The defining difference from {@link createRebaseReplayConcurrency}:
|
|
10
|
+
*
|
|
11
|
+
* - **Roll-forward, not re-execute.** On rebase, each pending local edit is
|
|
12
|
+
* replayed by re-applying its captured post-image tuples, *not* by
|
|
13
|
+
* re-running its transaction function. A transaction that reads current
|
|
14
|
+
* state and derives a new value (e.g. "add 10 to whatever is there")
|
|
15
|
+
* therefore preserves the value it originally produced, regardless of how
|
|
16
|
+
* the confirmed base shifted underneath it. Re-execution would instead
|
|
17
|
+
* recompute against the new base.
|
|
18
|
+
*
|
|
19
|
+
* The rebase sequence on a confirmed (positive-time) delta:
|
|
20
|
+
* 1. roll back all pending entries newest-to-oldest (apply their undo),
|
|
21
|
+
* 2. apply the confirmed delta,
|
|
22
|
+
* 3. replay the remaining pending entries oldest-to-newest by re-applying
|
|
23
|
+
* their captured redo tuples, recapturing fresh undo against the new base.
|
|
24
|
+
*
|
|
25
|
+
* Local transients carry monotonically increasing |time| (they are appended
|
|
26
|
+
* as the user edits), so a newly-captured entry always sorts last; the buffer
|
|
27
|
+
* is kept in arrival order and a new entry is materialized on top of the
|
|
28
|
+
* replayed survivors.
|
|
29
|
+
*
|
|
30
|
+
* @param userId Stable peer/session identifier stamped on every outbound
|
|
31
|
+
* envelope, unique across all peers sharing the same sync server.
|
|
32
|
+
*/
|
|
33
|
+
export declare const createRollForwardConcurrency: (userId: number | string) => ConcurrencyStrategyFactory;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { applyOperations } from "../transactional-store/apply-operations.js";
|
|
3
|
+
/**
|
|
4
|
+
* Roll-forward reconciliation: a client-side optimistic concurrency model for
|
|
5
|
+
* collaborative / multi-peer editing. Provided to prove the
|
|
6
|
+
* {@link ConcurrencyStrategy} seam can host a fundamentally different
|
|
7
|
+
* reconciliation engine than {@link createRebaseReplayConcurrency} — it is
|
|
8
|
+
* not currently wired into a real transport.
|
|
9
|
+
*
|
|
10
|
+
* The defining difference from {@link createRebaseReplayConcurrency}:
|
|
11
|
+
*
|
|
12
|
+
* - **Roll-forward, not re-execute.** On rebase, each pending local edit is
|
|
13
|
+
* replayed by re-applying its captured post-image tuples, *not* by
|
|
14
|
+
* re-running its transaction function. A transaction that reads current
|
|
15
|
+
* state and derives a new value (e.g. "add 10 to whatever is there")
|
|
16
|
+
* therefore preserves the value it originally produced, regardless of how
|
|
17
|
+
* the confirmed base shifted underneath it. Re-execution would instead
|
|
18
|
+
* recompute against the new base.
|
|
19
|
+
*
|
|
20
|
+
* The rebase sequence on a confirmed (positive-time) delta:
|
|
21
|
+
* 1. roll back all pending entries newest-to-oldest (apply their undo),
|
|
22
|
+
* 2. apply the confirmed delta,
|
|
23
|
+
* 3. replay the remaining pending entries oldest-to-newest by re-applying
|
|
24
|
+
* their captured redo tuples, recapturing fresh undo against the new base.
|
|
25
|
+
*
|
|
26
|
+
* Local transients carry monotonically increasing |time| (they are appended
|
|
27
|
+
* as the user edits), so a newly-captured entry always sorts last; the buffer
|
|
28
|
+
* is kept in arrival order and a new entry is materialized on top of the
|
|
29
|
+
* replayed survivors.
|
|
30
|
+
*
|
|
31
|
+
* @param userId Stable peer/session identifier stamped on every outbound
|
|
32
|
+
* envelope, unique across all peers sharing the same sync server.
|
|
33
|
+
*/
|
|
34
|
+
export const createRollForwardConcurrency = (userId) => (execute, getTransaction) => {
|
|
35
|
+
const pending = [];
|
|
36
|
+
const indexOfEntry = (id, entryUserId) => pending.findIndex(e => e.id === id && e.userId === entryUserId);
|
|
37
|
+
const rollbackAll = () => {
|
|
38
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
39
|
+
execute(t => applyOperations(t, pending[i].undo), { transient: true, userId: undefined });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
// Roll-forward: re-apply each pending entry's captured post-image
|
|
43
|
+
// tuples and recapture fresh undo against the (possibly changed) base.
|
|
44
|
+
const replayAll = () => {
|
|
45
|
+
for (const entry of pending) {
|
|
46
|
+
const result = execute(t => applyOperations(t, entry.redo), { transient: true, userId: entry.userId });
|
|
47
|
+
entry.undo = result.undo;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// Run a transaction function once to materialize its effect and capture
|
|
51
|
+
// the post-image. This is the only place a transaction function runs;
|
|
52
|
+
// every subsequent rebase rolls the captured tuples forward instead.
|
|
53
|
+
const runTransaction = (envelope) => {
|
|
54
|
+
const fn = getTransaction(envelope.name);
|
|
55
|
+
if (!fn)
|
|
56
|
+
throw new Error(`Unknown transaction: ${envelope.name}`);
|
|
57
|
+
return execute(t => fn(t, envelope.args), { transient: envelope.time < 0, userId: envelope.userId });
|
|
58
|
+
};
|
|
59
|
+
const removeAndRebuild = (id, entryUserId) => {
|
|
60
|
+
const idx = indexOfEntry(id, entryUserId);
|
|
61
|
+
if (idx === -1)
|
|
62
|
+
return;
|
|
63
|
+
rollbackAll();
|
|
64
|
+
pending.splice(idx, 1);
|
|
65
|
+
replayAll();
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
deferredCommit: true,
|
|
69
|
+
userId,
|
|
70
|
+
apply(envelope) {
|
|
71
|
+
const { id, userId: envelopeUserId, time } = envelope;
|
|
72
|
+
// Cancel: drop the matching pending entry and rebuild.
|
|
73
|
+
if (time === 0) {
|
|
74
|
+
removeAndRebuild(id, envelopeUserId);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
// Optimistic local transient: roll back, drop any prior version
|
|
78
|
+
// of this transaction, replay the survivors, then run the fn on
|
|
79
|
+
// top and capture its post-image tuples.
|
|
80
|
+
if (time < 0) {
|
|
81
|
+
rollbackAll();
|
|
82
|
+
const existing = indexOfEntry(id, envelopeUserId);
|
|
83
|
+
if (existing !== -1)
|
|
84
|
+
pending.splice(existing, 1);
|
|
85
|
+
replayAll();
|
|
86
|
+
const result = runTransaction(envelope);
|
|
87
|
+
const isNoOp = result.redo.length === 0 && result.undo.length === 0;
|
|
88
|
+
if (!isNoOp) {
|
|
89
|
+
pending.push({ id, userId: envelopeUserId, time, redo: result.redo, undo: result.undo });
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
// Confirmed delta (positive time): roll back all pending, drop
|
|
94
|
+
// our own echo (now authoritative), apply the confirmed delta,
|
|
95
|
+
// then roll the survivors forward.
|
|
96
|
+
rollbackAll();
|
|
97
|
+
const echo = indexOfEntry(id, envelopeUserId);
|
|
98
|
+
if (echo !== -1)
|
|
99
|
+
pending.splice(echo, 1);
|
|
100
|
+
const result = runTransaction(envelope);
|
|
101
|
+
replayAll();
|
|
102
|
+
return result;
|
|
103
|
+
},
|
|
104
|
+
cancel(id, cancelUserId) {
|
|
105
|
+
removeAndRebuild(id, cancelUserId);
|
|
106
|
+
},
|
|
107
|
+
onReset() {
|
|
108
|
+
pending.length = 0;
|
|
109
|
+
},
|
|
110
|
+
onBeforeToData: rollbackAll,
|
|
111
|
+
onAfterToData: replayAll,
|
|
112
|
+
onAfterFromData: replayAll,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=roll-forward-concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roll-forward-concurrency.js","sourceRoot":"","sources":["../../../../src/ecs/database/concurrency/roll-forward-concurrency.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
//
|
|
3
|
+
// Verifies the pluggable-concurrency seam by running a client-side
|
|
4
|
+
// roll-forward replay strategy through the public `createDatabase` API, and
|
|
5
|
+
// contrasting it with the built-in rebase-replay strategy on an identical
|
|
6
|
+
// scenario.
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
import { Database } from "../database.js";
|
|
9
|
+
import { createRollForwardConcurrency } from "./roll-forward-concurrency.js";
|
|
10
|
+
import { createRebaseReplayConcurrency } from "./rebase-replay-concurrency.js";
|
|
11
|
+
// A single numeric component is enough to expose the roll-forward vs
|
|
12
|
+
// re-execute difference: one transaction derives its value from current state.
|
|
13
|
+
const plugin = Database.Plugin.create({
|
|
14
|
+
components: {
|
|
15
|
+
counter: { type: "number" },
|
|
16
|
+
},
|
|
17
|
+
resources: {},
|
|
18
|
+
archetypes: {
|
|
19
|
+
Counter: ["counter"],
|
|
20
|
+
},
|
|
21
|
+
transactions: {
|
|
22
|
+
createCounter(t, args) {
|
|
23
|
+
return t.archetypes.Counter.insert({ counter: args.counter });
|
|
24
|
+
},
|
|
25
|
+
// Reads current value and adds 10 — its result depends on the base
|
|
26
|
+
// state it runs against. This is the transaction whose replay semantics
|
|
27
|
+
// distinguish roll-forward from re-execute.
|
|
28
|
+
bumpFromCurrent(t, args) {
|
|
29
|
+
const current = t.read(args.entity)?.counter ?? 0;
|
|
30
|
+
t.update(args.entity, { counter: current + 10 });
|
|
31
|
+
},
|
|
32
|
+
setAbsolute(t, args) {
|
|
33
|
+
t.update(args.entity, { counter: args.value });
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const makeDb = (concurrency) => Database.create(plugin, { concurrency });
|
|
38
|
+
// Seed a server-confirmed entity (positive time) so the entity exists in the
|
|
39
|
+
// committed base before any optimistic local edit. Returns its id.
|
|
40
|
+
const seedCommittedCounter = (db, value) => {
|
|
41
|
+
const result = db.apply({ id: 1, userId: "seed", name: "createCounter", args: { counter: value }, time: Date.now() });
|
|
42
|
+
return result.value;
|
|
43
|
+
};
|
|
44
|
+
describe("createRollForwardConcurrency — pluggable concurrency viability", () => {
|
|
45
|
+
it("plugs into createDatabase and exposes itself as the database concurrency strategy", () => {
|
|
46
|
+
const db = makeDb(createRollForwardConcurrency("A"));
|
|
47
|
+
expect(db.concurrency.deferredCommit).toBe(true);
|
|
48
|
+
expect(db.concurrency.userId).toBe("A");
|
|
49
|
+
});
|
|
50
|
+
it("applies a server-confirmed delta directly", () => {
|
|
51
|
+
const db = makeDb(createRollForwardConcurrency("A"));
|
|
52
|
+
const entity = seedCommittedCounter(db, 42);
|
|
53
|
+
expect(db.read(entity)?.counter).toBe(42);
|
|
54
|
+
});
|
|
55
|
+
it("makes a local optimistic transient visible immediately", () => {
|
|
56
|
+
const db = makeDb(createRollForwardConcurrency("A"));
|
|
57
|
+
const entity = seedCommittedCounter(db, 0);
|
|
58
|
+
db.transactions.bumpFromCurrent({ entity });
|
|
59
|
+
// Deferred-commit: applied locally as a transient before any echo.
|
|
60
|
+
expect(db.read(entity)?.counter).toBe(10);
|
|
61
|
+
});
|
|
62
|
+
it("replays pending edits by rolling captured post-images forward, not by re-executing", () => {
|
|
63
|
+
const db = makeDb(createRollForwardConcurrency("A"));
|
|
64
|
+
const entity = seedCommittedCounter(db, 0);
|
|
65
|
+
// Local optimistic edit: 0 -> 10, captured post-image = "set counter 10".
|
|
66
|
+
db.transactions.bumpFromCurrent({ entity });
|
|
67
|
+
expect(db.read(entity)?.counter).toBe(10);
|
|
68
|
+
// A peer's confirmed delta moves the base out from under the pending edit.
|
|
69
|
+
db.apply({ id: 99, userId: "B", name: "setAbsolute", args: { entity, value: 100 }, time: Date.now() + 1 });
|
|
70
|
+
// Roll-forward replays the *captured tuple* (set counter 10), so the
|
|
71
|
+
// pending edit's original post-image wins — NOT 110 (which is what
|
|
72
|
+
// re-executing bumpFromCurrent against the new base of 100 would give).
|
|
73
|
+
expect(db.read(entity)?.counter).toBe(10);
|
|
74
|
+
});
|
|
75
|
+
it("cancel discards a pending transient and restores committed state", () => {
|
|
76
|
+
const db = makeDb(createRollForwardConcurrency("A"));
|
|
77
|
+
const entity = seedCommittedCounter(db, 5);
|
|
78
|
+
// Drive a cancellable transient through the dispatcher via an async
|
|
79
|
+
// generator that yields once then throws (cleanly tears down).
|
|
80
|
+
const observer = vi.fn();
|
|
81
|
+
const unobserve = db.observe.entity(entity)(observer);
|
|
82
|
+
db.transactions.bumpFromCurrent({ entity });
|
|
83
|
+
expect(db.read(entity)?.counter).toBe(15);
|
|
84
|
+
// Cancel the pending entry by its dispatcher-assigned id. The first
|
|
85
|
+
// (and only) wrapped transaction gets id 1 from the dispatcher counter.
|
|
86
|
+
db.cancel(1, "A");
|
|
87
|
+
expect(db.read(entity)?.counter).toBe(5);
|
|
88
|
+
unobserve();
|
|
89
|
+
});
|
|
90
|
+
it("reset clears the pending buffer", () => {
|
|
91
|
+
const db = makeDb(createRollForwardConcurrency("A"));
|
|
92
|
+
const entity = seedCommittedCounter(db, 0);
|
|
93
|
+
db.transactions.bumpFromCurrent({ entity });
|
|
94
|
+
expect(db.read(entity)?.counter).toBe(10);
|
|
95
|
+
db.reset();
|
|
96
|
+
// Store is empty and the pending buffer is gone — re-seeding starts clean.
|
|
97
|
+
const reseeded = seedCommittedCounter(db, 7);
|
|
98
|
+
expect(db.read(reseeded)?.counter).toBe(7);
|
|
99
|
+
});
|
|
100
|
+
it("toData serializes only committed state and leaves the live transient intact", () => {
|
|
101
|
+
const db = makeDb(createRollForwardConcurrency("A"));
|
|
102
|
+
const entity = seedCommittedCounter(db, 0);
|
|
103
|
+
db.transactions.bumpFromCurrent({ entity });
|
|
104
|
+
expect(db.read(entity)?.counter).toBe(10);
|
|
105
|
+
const snapshot = db.toData();
|
|
106
|
+
// The live optimistic transient survives the rollback→serialize→replay.
|
|
107
|
+
expect(db.read(entity)?.counter).toBe(10);
|
|
108
|
+
// The snapshot is a detached copy of committed state only: loading it
|
|
109
|
+
// into a fresh database yields the committed base (0), not the pending
|
|
110
|
+
// optimistic value (10). This must hold even though `onAfterToData`
|
|
111
|
+
// replayed the transient back onto the live store after serialization.
|
|
112
|
+
const loaded = Database.create(plugin);
|
|
113
|
+
loaded.fromData(snapshot);
|
|
114
|
+
const loadedValues = loaded.select(["counter"]).map(e => loaded.read(e)?.counter);
|
|
115
|
+
expect(loadedValues).toEqual([0]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("roll-forward vs rebase-replay — same seam, different replay semantics", () => {
|
|
119
|
+
// Identical scenario, swapping only the concurrency strategy factory. The
|
|
120
|
+
// divergent result proves the two engines are genuinely different and that
|
|
121
|
+
// the pluggable seam selects between them with no other changes.
|
|
122
|
+
const runRebaseScenario = (concurrency) => {
|
|
123
|
+
const db = makeDb(concurrency);
|
|
124
|
+
const entity = seedCommittedCounter(db, 0);
|
|
125
|
+
db.transactions.bumpFromCurrent({ entity });
|
|
126
|
+
db.apply({ id: 99, userId: "B", name: "setAbsolute", args: { entity, value: 100 }, time: Date.now() + 1 });
|
|
127
|
+
return db.read(entity)?.counter ?? NaN;
|
|
128
|
+
};
|
|
129
|
+
it("roll-forward preserves the captured post-image (10)", () => {
|
|
130
|
+
expect(runRebaseScenario(createRollForwardConcurrency("A"))).toBe(10);
|
|
131
|
+
});
|
|
132
|
+
it("rebase-replay re-executes against the new base (110)", () => {
|
|
133
|
+
expect(runRebaseScenario(createRebaseReplayConcurrency("A"))).toBe(110);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=roll-forward-concurrency.test.js.map
|