@optimystic/db-core 0.14.1 → 0.16.3
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/blocks/block-store.d.ts +18 -1
- package/dist/src/blocks/block-store.d.ts.map +1 -1
- package/dist/src/blocks/helpers.d.ts +2 -2
- package/dist/src/blocks/helpers.d.ts.map +1 -1
- package/dist/src/blocks/helpers.js +2 -2
- package/dist/src/blocks/helpers.js.map +1 -1
- package/dist/src/btree/btree.d.ts +27 -6
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +104 -61
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +11 -1
- package/dist/src/chain/chain.js.map +1 -1
- package/dist/src/cluster/index.d.ts +1 -0
- package/dist/src/cluster/index.d.ts.map +1 -1
- package/dist/src/cluster/index.js +1 -0
- package/dist/src/cluster/index.js.map +1 -1
- package/dist/src/cluster/membership.d.ts +49 -0
- package/dist/src/cluster/membership.d.ts.map +1 -0
- package/dist/src/cluster/membership.js +75 -0
- package/dist/src/cluster/membership.js.map +1 -0
- package/dist/src/cluster/structs.d.ts +50 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +9 -1
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/cohort-topic/addressing.d.ts +8 -2
- package/dist/src/cohort-topic/addressing.d.ts.map +1 -1
- package/dist/src/cohort-topic/addressing.js +11 -3
- package/dist/src/cohort-topic/addressing.js.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +13 -7
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +17 -6
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -1
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts +17 -0
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/replay-guard.js +33 -0
- package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -1
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts +7 -0
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/topic-budget.js +8 -2
- package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -1
- package/dist/src/cohort-topic/coldstart.d.ts +34 -14
- package/dist/src/cohort-topic/coldstart.d.ts.map +1 -1
- package/dist/src/cohort-topic/coldstart.js +25 -11
- package/dist/src/cohort-topic/coldstart.js.map +1 -1
- package/dist/src/cohort-topic/gossip/bus.d.ts +12 -1
- package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -1
- package/dist/src/cohort-topic/gossip/bus.js +27 -1
- package/dist/src/cohort-topic/gossip/bus.js.map +1 -1
- package/dist/src/cohort-topic/gossip/view.d.ts +8 -0
- package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -1
- package/dist/src/cohort-topic/gossip/view.js +17 -1
- package/dist/src/cohort-topic/gossip/view.js.map +1 -1
- package/dist/src/cohort-topic/member-engine.d.ts +10 -3
- package/dist/src/cohort-topic/member-engine.d.ts.map +1 -1
- package/dist/src/cohort-topic/member-engine.js +29 -14
- package/dist/src/cohort-topic/member-engine.js.map +1 -1
- package/dist/src/cohort-topic/membership/publisher.d.ts +14 -7
- package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -1
- package/dist/src/cohort-topic/membership/publisher.js +15 -27
- package/dist/src/cohort-topic/membership/publisher.js.map +1 -1
- package/dist/src/cohort-topic/membership/verifier.d.ts +35 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
- package/dist/src/cohort-topic/membership/verifier.js +88 -3
- package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
- package/dist/src/cohort-topic/promotion.d.ts +7 -0
- package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
- package/dist/src/cohort-topic/promotion.js +16 -2
- package/dist/src/cohort-topic/promotion.js.map +1 -1
- package/dist/src/cohort-topic/registration/renewal.d.ts +18 -0
- package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -1
- package/dist/src/cohort-topic/registration/renewal.js +54 -0
- package/dist/src/cohort-topic/registration/renewal.js.map +1 -1
- package/dist/src/cohort-topic/registration/types.d.ts +12 -0
- package/dist/src/cohort-topic/registration/types.d.ts.map +1 -1
- package/dist/src/cohort-topic/registration/types.js +14 -0
- package/dist/src/cohort-topic/registration/types.js.map +1 -1
- package/dist/src/cohort-topic/service.d.ts +4 -3
- package/dist/src/cohort-topic/service.d.ts.map +1 -1
- package/dist/src/cohort-topic/service.js +38 -12
- package/dist/src/cohort-topic/service.js.map +1 -1
- package/dist/src/cohort-topic/sig/payloads.d.ts +23 -7
- package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -1
- package/dist/src/cohort-topic/sig/payloads.js +22 -6
- package/dist/src/cohort-topic/sig/payloads.js.map +1 -1
- package/dist/src/cohort-topic/traffic.d.ts +6 -0
- package/dist/src/cohort-topic/traffic.d.ts.map +1 -1
- package/dist/src/cohort-topic/traffic.js +16 -1
- package/dist/src/cohort-topic/traffic.js.map +1 -1
- package/dist/src/cohort-topic/walk.d.ts +22 -7
- package/dist/src/cohort-topic/walk.d.ts.map +1 -1
- package/dist/src/cohort-topic/walk.js +78 -18
- package/dist/src/cohort-topic/walk.js.map +1 -1
- package/dist/src/cohort-topic/wire/codec.d.ts +3 -1
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/codec.js +7 -1
- package/dist/src/cohort-topic/wire/codec.js.map +1 -1
- package/dist/src/cohort-topic/wire/index.d.ts +1 -1
- package/dist/src/cohort-topic/wire/index.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/index.js +1 -1
- package/dist/src/cohort-topic/wire/index.js.map +1 -1
- package/dist/src/cohort-topic/wire/payloads.d.ts +5 -4
- package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/payloads.js +14 -5
- package/dist/src/cohort-topic/wire/payloads.js.map +1 -1
- package/dist/src/cohort-topic/wire/primitives.d.ts +65 -0
- package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/primitives.js +173 -0
- package/dist/src/cohort-topic/wire/primitives.js.map +1 -0
- package/dist/src/cohort-topic/wire/types.d.ts +132 -5
- package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.d.ts +15 -5
- package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.js +146 -123
- package/dist/src/cohort-topic/wire/validate.js.map +1 -1
- package/dist/src/collection/collection.d.ts +23 -12
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +111 -35
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collection/struct.d.ts +37 -2
- package/dist/src/collection/struct.d.ts.map +1 -1
- package/dist/src/collection/struct.js +16 -1
- package/dist/src/collection/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +18 -1
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +36 -6
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.d.ts +26 -3
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +34 -7
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/log/struct.d.ts +5 -4
- package/dist/src/log/struct.d.ts.map +1 -1
- package/dist/src/log/struct.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts +2 -1
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +13 -92
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/i-key-network.d.ts +9 -0
- package/dist/src/network/i-key-network.d.ts.map +1 -1
- package/dist/src/network/i-repo.d.ts +8 -0
- package/dist/src/network/i-repo.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +10 -0
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/reactivity/push-state.d.ts.map +1 -1
- package/dist/src/reactivity/push-state.js +4 -33
- package/dist/src/reactivity/push-state.js.map +1 -1
- package/dist/src/reactivity/wire-validate.d.ts +7 -22
- package/dist/src/reactivity/wire-validate.d.ts.map +1 -1
- package/dist/src/reactivity/wire-validate.js +7 -73
- package/dist/src/reactivity/wire-validate.js.map +1 -1
- package/dist/src/reactivity/wire.js +3 -3
- package/dist/src/reactivity/wire.js.map +1 -1
- package/dist/src/testing/async-wait.d.ts +23 -0
- package/dist/src/testing/async-wait.d.ts.map +1 -0
- package/dist/src/testing/async-wait.js +42 -0
- package/dist/src/testing/async-wait.js.map +1 -0
- package/dist/src/testing/index.d.ts +3 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +3 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/test-transactor.d.ts +44 -0
- package/dist/src/testing/test-transactor.d.ts.map +1 -0
- package/dist/src/testing/test-transactor.js +423 -0
- package/dist/src/testing/test-transactor.js.map +1 -0
- package/dist/src/transaction/actions-engine.d.ts +6 -3
- package/dist/src/transaction/actions-engine.d.ts.map +1 -1
- package/dist/src/transaction/actions-engine.js +8 -7
- package/dist/src/transaction/actions-engine.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +43 -22
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +443 -178
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +82 -0
- package/dist/src/transaction/errors.d.ts.map +1 -0
- package/dist/src/transaction/errors.js +93 -0
- package/dist/src/transaction/errors.js.map +1 -0
- package/dist/src/transaction/index.d.ts +6 -3
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +4 -2
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transaction/operations-hash.d.ts +119 -0
- package/dist/src/transaction/operations-hash.d.ts.map +1 -0
- package/dist/src/transaction/operations-hash.js +180 -0
- package/dist/src/transaction/operations-hash.js.map +1 -0
- package/dist/src/transaction/read-dependency-collector.d.ts +43 -0
- package/dist/src/transaction/read-dependency-collector.d.ts.map +1 -0
- package/dist/src/transaction/read-dependency-collector.js +69 -0
- package/dist/src/transaction/read-dependency-collector.js.map +1 -0
- package/dist/src/transaction/session.d.ts +12 -3
- package/dist/src/transaction/session.d.ts.map +1 -1
- package/dist/src/transaction/session.js +35 -9
- package/dist/src/transaction/session.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +162 -1
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +85 -3
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transaction/validator.d.ts +3 -11
- package/dist/src/transaction/validator.d.ts.map +1 -1
- package/dist/src/transaction/validator.js +69 -28
- package/dist/src/transaction/validator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +35 -0
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +146 -40
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +19 -5
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +43 -11
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/atomic-proxy.d.ts +35 -5
- package/dist/src/transform/atomic-proxy.d.ts.map +1 -1
- package/dist/src/transform/atomic-proxy.js +39 -7
- package/dist/src/transform/atomic-proxy.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +36 -5
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +84 -8
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +8 -2
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +17 -2
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +66 -11
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/backoff.d.ts +48 -0
- package/dist/src/utility/backoff.d.ts.map +1 -0
- package/dist/src/utility/backoff.js +60 -0
- package/dist/src/utility/backoff.js.map +1 -0
- package/dist/src/utility/pending.d.ts +1 -0
- package/dist/src/utility/pending.d.ts.map +1 -1
- package/dist/src/utility/pending.js +3 -1
- package/dist/src/utility/pending.js.map +1 -1
- package/package.json +4 -4
- package/src/blocks/block-store.ts +19 -1
- package/src/blocks/helpers.ts +3 -3
- package/src/btree/btree.ts +108 -63
- package/src/chain/chain.ts +11 -1
- package/src/cluster/index.ts +1 -0
- package/src/cluster/membership.ts +85 -0
- package/src/cluster/structs.ts +51 -1
- package/src/cohort-topic/addressing.ts +12 -4
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +21 -9
- package/src/cohort-topic/antidos/replay-guard.ts +37 -0
- package/src/cohort-topic/antidos/topic-budget.ts +17 -2
- package/src/cohort-topic/coldstart.ts +48 -15
- package/src/cohort-topic/gossip/bus.ts +39 -2
- package/src/cohort-topic/gossip/view.ts +26 -1
- package/src/cohort-topic/member-engine.ts +36 -15
- package/src/cohort-topic/membership/publisher.ts +25 -33
- package/src/cohort-topic/membership/verifier.ts +119 -3
- package/src/cohort-topic/promotion.ts +23 -2
- package/src/cohort-topic/registration/renewal.ts +73 -1
- package/src/cohort-topic/registration/types.ts +15 -0
- package/src/cohort-topic/service.ts +41 -15
- package/src/cohort-topic/sig/payloads.ts +29 -9
- package/src/cohort-topic/traffic.ts +23 -1
- package/src/cohort-topic/walk.ts +95 -24
- package/src/cohort-topic/wire/codec.ts +12 -0
- package/src/cohort-topic/wire/index.ts +2 -0
- package/src/cohort-topic/wire/payloads.ts +14 -5
- package/src/cohort-topic/wire/primitives.ts +188 -0
- package/src/cohort-topic/wire/types.ts +136 -4
- package/src/cohort-topic/wire/validate.ts +168 -134
- package/src/collection/collection.ts +123 -40
- package/src/collection/struct.ts +44 -2
- package/src/collections/tree/tree.ts +42 -5
- package/src/index.ts +1 -0
- package/src/log/log.ts +37 -9
- package/src/log/struct.ts +5 -4
- package/src/matchmaking/wire.ts +25 -102
- package/src/network/i-key-network.ts +10 -0
- package/src/network/i-repo.ts +8 -0
- package/src/network/struct.ts +10 -0
- package/src/reactivity/push-state.ts +4 -37
- package/src/reactivity/wire-validate.ts +7 -81
- package/src/reactivity/wire.ts +3 -3
- package/src/testing/async-wait.ts +65 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/test-transactor.ts +468 -0
- package/src/transaction/actions-engine.ts +8 -6
- package/src/transaction/coordinator.ts +498 -229
- package/src/transaction/errors.ts +91 -0
- package/src/transaction/index.ts +23 -3
- package/src/transaction/operations-hash.ts +196 -0
- package/src/transaction/read-dependency-collector.ts +78 -0
- package/src/transaction/session.ts +37 -9
- package/src/transaction/transaction.ts +211 -4
- package/src/transaction/validator.ts +73 -48
- package/src/transactor/network-transactor.ts +164 -49
- package/src/transactor/transactor-source.ts +47 -12
- package/src/transform/atomic-proxy.ts +92 -49
- package/src/transform/cache-source.ts +87 -9
- package/src/transform/helpers.ts +8 -2
- package/src/transform/tracker.ts +69 -11
- package/src/utility/backoff.ts +95 -0
- package/src/utility/pending.ts +3 -1
- package/src/transaction/context.ts +0 -103
|
@@ -51,6 +51,26 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
51
51
|
private readonly getRepo: (peerId: PeerId) => IRepo;
|
|
52
52
|
private readonly localChangeNotifier: IBlockChangeNotifier | undefined;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Per-transaction coordinator cache: `actionId → (blockId → resolved coordinator)`.
|
|
56
|
+
* {@link pend} populates it from its final (retry-adjusted) batch assignment; commit
|
|
57
|
+
* reads it via {@link resolveCoordinator} before falling back to a live
|
|
58
|
+
* `findCoordinator`, so a block's coordinator is resolved once per transaction across
|
|
59
|
+
* the pend→commit window instead of once at pend and again at commit.
|
|
60
|
+
*
|
|
61
|
+
* Keyed by `actionId`, which is unique per transaction, so an entry is only ever read
|
|
62
|
+
* by commits of the SAME transaction — the ones that immediately follow its pend. Once
|
|
63
|
+
* those finish, nothing reads the entry again (a later transaction has a fresh
|
|
64
|
+
* actionId), so it carries no cross-transaction staleness even if it lingers. The TTL
|
|
65
|
+
* and size cap in {@link txnCoordinatorsFor} are therefore only a memory backstop that
|
|
66
|
+
* reclaims entries from transactions that pend but never commit — NOT a staleness
|
|
67
|
+
* bound. This is why keying by actionId gives the same "thrown away when the
|
|
68
|
+
* transaction ends" safety as threading a Map through the call, without touching the
|
|
69
|
+
* ITransactor contract.
|
|
70
|
+
*/
|
|
71
|
+
private readonly txnCoordinatorCache = new Map<ActionId, { coordinators: Map<BlockId, PeerId>; expires: number }>();
|
|
72
|
+
private static readonly MAX_TXN_COORDINATOR_CACHE_ENTRIES = 1000;
|
|
73
|
+
|
|
54
74
|
constructor(
|
|
55
75
|
init: NetworkTransactorInit,
|
|
56
76
|
) {
|
|
@@ -109,56 +129,69 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
109
129
|
error = e as Error;
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
// Second-chance retry:
|
|
113
|
-
//
|
|
132
|
+
// Second-chance retry: ONLY for a genuine no-response — a batch with no valid
|
|
133
|
+
// response, or a response missing an entry for a requested block id. An
|
|
134
|
+
// authoritative "absent" answer (a valid response that carries an entry for
|
|
135
|
+
// every requested block id, even one whose entry has only `state` and no
|
|
136
|
+
// materialized `block`) is FINAL and must not retry. A block that genuinely
|
|
137
|
+
// does not exist yet surfaces as `{ state: {} }` (an entry that is present) —
|
|
138
|
+
// retrying it doubles the round-trips on the common createOrOpen "does this
|
|
139
|
+
// block exist?" probe. Cross-member reconciliation for a missing block has
|
|
140
|
+
// already happened one layer down: CoordinatorRepo.get detects `isMissing` and
|
|
141
|
+
// consults cluster peers before it responds, so by the time an authoritative
|
|
142
|
+
// absent reaches here there is nothing left for a transactor-level retry to
|
|
143
|
+
// discover. See ticket txn-perf-authoritative-notfound.
|
|
114
144
|
const hasValidResponse = (b: CoordinatorBatch<BlockId[], GetBlockResults>) => {
|
|
115
145
|
return b.request?.isResponse === true && b.request.response != null;
|
|
116
146
|
};
|
|
117
147
|
|
|
118
|
-
|
|
148
|
+
// A batch is answered when its response carries an entry for EVERY requested
|
|
149
|
+
// block id. An entry present with only `state` (no `block`) is an authoritative
|
|
150
|
+
// "absent", which counts as answered — not a gap.
|
|
151
|
+
const isAuthoritative = (b: CoordinatorBatch<BlockId[], GetBlockResults>) => {
|
|
119
152
|
if (!hasValidResponse(b)) return false;
|
|
120
153
|
const resp = b.request!.response! as GetBlockResults;
|
|
121
|
-
return b.payload.
|
|
122
|
-
const entry = resp[bid];
|
|
123
|
-
return entry && typeof entry === 'object' && 'block' in entry && entry.block != null;
|
|
124
|
-
});
|
|
154
|
+
return b.payload.every(bid => resp[bid] !== undefined);
|
|
125
155
|
};
|
|
126
156
|
|
|
127
|
-
// Retry
|
|
128
|
-
//
|
|
157
|
+
// Retry only genuine no-response / partial-response batches. An authoritative
|
|
158
|
+
// absent answer is not retried.
|
|
129
159
|
const retryable = Array.from(allBatches(batches)).filter(b =>
|
|
130
|
-
!
|
|
160
|
+
!isAuthoritative(b as any)
|
|
131
161
|
) as CoordinatorBatch<BlockId[], GetBlockResults>[];
|
|
132
162
|
|
|
133
163
|
if (retryable.length > 0 && Date.now() < expiration) {
|
|
134
164
|
log('get:retry retryable=%d', retryable.length);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
165
|
+
// Fan out the per-batch retries concurrently. Each root batch builds its own
|
|
166
|
+
// excluded-peer set and attaches its own `subsumedBy`, so the retry rounds are
|
|
167
|
+
// independent per root and safe to run in parallel.
|
|
168
|
+
const retryOutcomes = await Promise.allSettled(retryable.map(async b => {
|
|
169
|
+
const excluded = new Set<PeerId>([b.peerId, ...((b.excludedPeers ?? []) as PeerId[])]);
|
|
170
|
+
const retries = await createBatchesForPayload<BlockId[], GetBlockResults>(
|
|
171
|
+
b.payload,
|
|
172
|
+
b.payload,
|
|
173
|
+
(gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)],
|
|
174
|
+
Array.from(excluded),
|
|
175
|
+
async (blockId, options) => this.keyNetwork.findCoordinator(await blockIdToBytes(blockId), options)
|
|
176
|
+
);
|
|
177
|
+
if (retries.length > 0) {
|
|
178
|
+
b.subsumedBy = [...(b.subsumedBy ?? []), ...retries];
|
|
179
|
+
await processBatches(
|
|
180
|
+
retries,
|
|
181
|
+
(batch) => this.getRepo(batch.peerId).get({ blockIds: batch.payload, context: blockGets.context }, { expiration, dialTimeoutMs: this.dialTimeoutMs }),
|
|
182
|
+
batch => batch.payload,
|
|
143
183
|
(gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)],
|
|
144
|
-
|
|
184
|
+
expiration,
|
|
145
185
|
async (blockId, options) => this.keyNetwork.findCoordinator(await blockIdToBytes(blockId), options)
|
|
146
186
|
);
|
|
147
|
-
if (retries.length > 0) {
|
|
148
|
-
b.subsumedBy = [...(b.subsumedBy ?? []), ...retries];
|
|
149
|
-
await processBatches(
|
|
150
|
-
retries,
|
|
151
|
-
(batch) => this.getRepo(batch.peerId).get({ blockIds: batch.payload, context: blockGets.context }, { expiration, dialTimeoutMs: this.dialTimeoutMs }),
|
|
152
|
-
batch => batch.payload,
|
|
153
|
-
(gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)],
|
|
154
|
-
expiration,
|
|
155
|
-
async (blockId, options) => this.keyNetwork.findCoordinator(await blockIdToBytes(blockId), options)
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
187
|
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
}));
|
|
189
|
+
// First-error-wins: keep any pre-existing error, otherwise adopt the first
|
|
190
|
+
// rejection across the concurrent retries (retryable order is preserved).
|
|
191
|
+
for (const outcome of retryOutcomes) {
|
|
192
|
+
if (outcome.status === 'rejected' && !error) {
|
|
193
|
+
error = outcome.reason instanceof Error ? outcome.reason : new Error(String(outcome.reason));
|
|
194
|
+
}
|
|
162
195
|
}
|
|
163
196
|
}
|
|
164
197
|
|
|
@@ -437,7 +470,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
437
470
|
expiration,
|
|
438
471
|
dialTimeoutMs: this.dialTimeoutMs,
|
|
439
472
|
coordinatingBlockIds: batch.coordinatingBlockIds
|
|
440
|
-
}
|
|
473
|
+
}
|
|
441
474
|
),
|
|
442
475
|
batch => blockIdsForTransforms(batch.payload),
|
|
443
476
|
transformForBlock,
|
|
@@ -446,13 +479,10 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
446
479
|
);
|
|
447
480
|
// Cache resolved coordinators for follow-up commit to hit the same peers
|
|
448
481
|
try {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
for (const b of Array.from(allBatches(batches))) {
|
|
452
|
-
pn.recordCoordinator(await blockIdToBytes(b.blockId), b.peerId);
|
|
453
|
-
}
|
|
482
|
+
for (const b of Array.from(allBatches(batches))) {
|
|
483
|
+
this.keyNetwork.recordCoordinator?.(await blockIdToBytes(b.blockId), b.peerId);
|
|
454
484
|
}
|
|
455
|
-
} catch (e) {
|
|
485
|
+
} catch (e) { log('WARN: Failed to record coordinator hint %o', e); }
|
|
456
486
|
} catch (e) {
|
|
457
487
|
error = e as Error;
|
|
458
488
|
}
|
|
@@ -477,7 +507,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
477
507
|
|
|
478
508
|
if (error) { // If any failures, cancel all pending actions as background microtask
|
|
479
509
|
log('pend:cancel actionId=%s', blockAction.actionId);
|
|
480
|
-
void Promise.resolve().then(() => this.cancelBatch(batches, { blockIds, actionId: blockAction.actionId }));
|
|
510
|
+
void Promise.resolve().then(() => this.cancelBatch(batches, { blockIds, actionId: blockAction.actionId })).catch(e => log('WARN: cancel after pend failure rejected: %o', e));
|
|
481
511
|
const stale = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && !b.request!.response!.success));
|
|
482
512
|
if (stale.length > 0) { // Any active stale failures should preempt reporting connection or other potential transient errors (we have information)
|
|
483
513
|
log('pend:stale actionId=%s staleCount=%d', blockAction.actionId, stale.length);
|
|
@@ -491,6 +521,26 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
491
521
|
|
|
492
522
|
// Collect replies back into result structure
|
|
493
523
|
const completed = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && b.request!.response!.success));
|
|
524
|
+
|
|
525
|
+
// Seed the per-transaction coordinator cache from the final (retry-adjusted) batch
|
|
526
|
+
// assignment so the follow-up commit reuses pend's resolution without a fresh
|
|
527
|
+
// findCoordinator round or a hop through the optional recordCoordinator hint. We read
|
|
528
|
+
// blockIdsForTransforms(b.payload) rather than the anchor b.blockId so EVERY block a
|
|
529
|
+
// consolidated batch coordinates is recorded — and against the peer that actually
|
|
530
|
+
// pended it, since a block re-homed by a retry lands in the retry batch's payload.
|
|
531
|
+
// NOTE: this cache assumes cluster membership is stable for the transaction's
|
|
532
|
+
// lifetime — the coordinator resolved here is reused verbatim at commit. Transactions
|
|
533
|
+
// are short, so that holds today. If a future change lets clusters churn *within* a
|
|
534
|
+
// single transaction (e.g. very long-running commits), a cached coordinator could
|
|
535
|
+
// point at a peer no longer in the cohort; commit self-heals (a failed cached peer is
|
|
536
|
+
// excluded and re-resolved live by processBatches), at the cost of one wasted round-trip.
|
|
537
|
+
const txnCoordinators = this.txnCoordinatorsFor(blockAction.actionId);
|
|
538
|
+
for (const b of completed) {
|
|
539
|
+
for (const bid of blockIdsForTransforms(b.payload)) {
|
|
540
|
+
txnCoordinators.set(bid, b.peerId);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
494
544
|
log('pend:done actionId=%s ms=%d batches=%d', blockAction.actionId, Date.now() - t0, batches.length);
|
|
495
545
|
return {
|
|
496
546
|
success: true,
|
|
@@ -558,7 +608,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
558
608
|
if (error) {
|
|
559
609
|
// Non-tail block commit failures should not fail the overall action once the tail has committed.
|
|
560
610
|
// Proceed and rely on reconciliation paths (e.g. reads with context) to finalize state on lagging peers.
|
|
561
|
-
try {
|
|
611
|
+
try { log('WARN: non-tail commit had errors; proceeding after tail commit: %s', error.message); } catch { /* ignore */ }
|
|
562
612
|
}
|
|
563
613
|
}
|
|
564
614
|
|
|
@@ -569,12 +619,18 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
569
619
|
private async commitBlock(blockId: BlockId, blockIds: BlockId[], actionId: ActionId, rev: number, tailId?: BlockId): Promise<CommitResult> {
|
|
570
620
|
const { batches: tailBatches, error: tailError } = await this.commitBlocks({ blockIds: [blockId], actionId, rev, tailId });
|
|
571
621
|
if (tailError) {
|
|
572
|
-
//
|
|
573
|
-
|
|
622
|
+
// commit is a pure attempt: stale → { success:false }, transient → throw. Cancellation
|
|
623
|
+
// is the CALLER's responsibility (coordinator cancelPhase; TransactorSource.transact),
|
|
624
|
+
// which owns the retry budget and the committed-vs-pending picture — self-cancelling here
|
|
625
|
+
// would tear down a pend a caller's retry loop is still working against, and double-cancel.
|
|
574
626
|
// Collect and return any active stale failures
|
|
575
627
|
const stale = Array.from(allBatches(tailBatches, b => b.request?.isResponse as boolean && !b.request!.response!.success));
|
|
576
628
|
if (stale.length > 0) {
|
|
577
|
-
|
|
629
|
+
// NOTE: a reason-only StaleFailure (success:false, no `missing`) lands here too and
|
|
630
|
+
// returns { missing: [], success:false } — the `reason` string is dropped rather than
|
|
631
|
+
// surfaced via `throw tailError`. Fine today (failure still propagates); if reason-only
|
|
632
|
+
// commit rejections ever need their diagnostic reason, gate this branch on non-empty missing.
|
|
633
|
+
return { missing: distinctBlockActionTransforms(stale.flatMap(b => (b.request!.response! as StaleFailure).missing).filter((x): x is ActionTransforms => x !== undefined)), success: false as const };
|
|
578
634
|
}
|
|
579
635
|
throw tailError;
|
|
580
636
|
}
|
|
@@ -584,7 +640,9 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
584
640
|
/** Attempts to commit a set of blocks, and handles failures and errors */
|
|
585
641
|
private async commitBlocks({ blockIds, actionId, rev, tailId }: RepoCommitRequest) {
|
|
586
642
|
const expiration = Date.now() + this.timeoutMs;
|
|
587
|
-
|
|
643
|
+
// Thread the transaction's actionId so both the initial batch assembly and any
|
|
644
|
+
// per-block retry re-resolution prefer the coordinator pend already resolved.
|
|
645
|
+
const batches = await this.batchesForPayload<BlockId[], CommitResult>(blockIds, blockIds, mergeBlocks, [], actionId);
|
|
588
646
|
log('commitBlocks actionId=%s rev=%d batches=%d', actionId, rev, batches.length);
|
|
589
647
|
let error: Error | undefined;
|
|
590
648
|
try {
|
|
@@ -594,7 +652,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
594
652
|
batch => batch.payload,
|
|
595
653
|
mergeBlocks,
|
|
596
654
|
expiration,
|
|
597
|
-
async (blockId, options) => this.
|
|
655
|
+
async (blockId, options) => this.resolveCoordinator(blockId, options, actionId)
|
|
598
656
|
);
|
|
599
657
|
} catch (e) {
|
|
600
658
|
error = e as Error;
|
|
@@ -623,17 +681,74 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
623
681
|
blockIds: BlockId[],
|
|
624
682
|
payload: TPayload,
|
|
625
683
|
getBlockPayload: (payload: TPayload, blockId: BlockId, mergeWithPayload: TPayload | undefined) => TPayload,
|
|
626
|
-
excludedPeers: PeerId[]
|
|
684
|
+
excludedPeers: PeerId[],
|
|
685
|
+
/** When set, prefer a coordinator this transaction already resolved at pend (see {@link resolveCoordinator}). */
|
|
686
|
+
actionId?: ActionId
|
|
627
687
|
): Promise<CoordinatorBatch<TPayload, TResponse>[]> {
|
|
628
688
|
return createBatchesForPayload<TPayload, TResponse>(
|
|
629
689
|
blockIds,
|
|
630
690
|
payload,
|
|
631
691
|
getBlockPayload,
|
|
632
692
|
excludedPeers,
|
|
633
|
-
async (blockId, options) => this.
|
|
693
|
+
async (blockId, options) => this.resolveCoordinator(blockId, options, actionId)
|
|
634
694
|
);
|
|
635
695
|
}
|
|
636
696
|
|
|
697
|
+
/**
|
|
698
|
+
* Resolve the coordinator for `blockId`, preferring one this transaction already
|
|
699
|
+
* resolved during pend (the per-transaction cache) before falling back to a live
|
|
700
|
+
* `findCoordinator`. A cached coordinator that is in `excludedPeers` (already tried and
|
|
701
|
+
* failed during a retry) is skipped so a retry can't loop on a dead coordinator. A
|
|
702
|
+
* cache miss — including every call with no `actionId` (get/cancel) — never fails; it
|
|
703
|
+
* always falls through to live resolution.
|
|
704
|
+
*/
|
|
705
|
+
private async resolveCoordinator(
|
|
706
|
+
blockId: BlockId,
|
|
707
|
+
options: { excludedPeers: PeerId[] },
|
|
708
|
+
actionId: ActionId | undefined
|
|
709
|
+
): Promise<PeerId> {
|
|
710
|
+
if (actionId !== undefined) {
|
|
711
|
+
const cached = this.txnCoordinatorCache.get(actionId)?.coordinators.get(blockId);
|
|
712
|
+
if (cached && !options.excludedPeers.some(p => p.toString() === cached.toString())) {
|
|
713
|
+
return cached;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return this.keyNetwork.findCoordinator(await blockIdToBytes(blockId), options);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Get (creating if absent) the per-transaction coordinator map for `actionId`,
|
|
721
|
+
* refreshing its expiry and lazily sweeping expired sibling entries. See
|
|
722
|
+
* {@link txnCoordinatorCache} for why the TTL/size cap here is a memory backstop and
|
|
723
|
+
* not a staleness bound.
|
|
724
|
+
*/
|
|
725
|
+
private txnCoordinatorsFor(actionId: ActionId): Map<BlockId, PeerId> {
|
|
726
|
+
const now = Date.now();
|
|
727
|
+
// Reclaim entries from transactions that pended but never committed. A live entry is
|
|
728
|
+
// never stale (unique actionId; read only by its own transaction's commit), so
|
|
729
|
+
// sweeping lazily on write is safe.
|
|
730
|
+
for (const [aid, entry] of this.txnCoordinatorCache) {
|
|
731
|
+
if (entry.expires <= now) this.txnCoordinatorCache.delete(aid);
|
|
732
|
+
}
|
|
733
|
+
// Comfortably covers a normal pend→commit gap (~2 op budgets) with a fixed floor;
|
|
734
|
+
// an entry outliving this only loses the optimization (commit re-resolves live),
|
|
735
|
+
// never correctness.
|
|
736
|
+
const ttlMs = Math.max(this.timeoutMs * 2, 60_000);
|
|
737
|
+
const existing = this.txnCoordinatorCache.get(actionId);
|
|
738
|
+
if (existing) {
|
|
739
|
+
existing.expires = now + ttlMs;
|
|
740
|
+
return existing.coordinators;
|
|
741
|
+
}
|
|
742
|
+
const created = { coordinators: new Map<BlockId, PeerId>(), expires: now + ttlMs };
|
|
743
|
+
this.txnCoordinatorCache.set(actionId, created);
|
|
744
|
+
while (this.txnCoordinatorCache.size > NetworkTransactor.MAX_TXN_COORDINATOR_CACHE_ENTRIES) {
|
|
745
|
+
const oldest = this.txnCoordinatorCache.keys().next().value as ActionId | undefined;
|
|
746
|
+
if (oldest == null || oldest === actionId) break;
|
|
747
|
+
this.txnCoordinatorCache.delete(oldest);
|
|
748
|
+
}
|
|
749
|
+
return created.coordinators;
|
|
750
|
+
}
|
|
751
|
+
|
|
637
752
|
/** Cancels a pending transaction by canceling all blocks associated with the transaction, including failed peers */
|
|
638
753
|
private async cancelBatch<TPayload, TResponse>(
|
|
639
754
|
batches: CoordinatorBatch<TPayload, TResponse>[],
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { randomBytes } from '@noble/hashes/utils.js'
|
|
2
2
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
|
-
import type { IBlock, BlockId, BlockHeader, ITransactor, ActionId, StaleFailure, ActionContext, BlockType, BlockSource, Transforms } from "../index.js";
|
|
3
|
+
import type { IBlock, BlockId, BlockHeader, ITransactor, ActionId, StaleFailure, ActionContext, BlockType, BlockSource, ReadPurpose, Transforms } from "../index.js";
|
|
4
4
|
import type { ReadDependency } from "../transaction/transaction.js";
|
|
5
|
+
import { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
|
|
5
6
|
|
|
6
7
|
export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlock> {
|
|
7
|
-
|
|
8
|
+
/** Shared with this collection's CacheSource so cache hits also record dependencies.
|
|
9
|
+
* Defaults to a private instance so internal log-walk sources (which never need a
|
|
10
|
+
* transaction read set) work standalone. */
|
|
11
|
+
private readonly collector: ReadDependencyCollector;
|
|
12
|
+
/** Last revision observed per id, so CacheSource can learn the revision on a miss-load
|
|
13
|
+
* (it calls {@link getReadRevision} right after this source serves the block). */
|
|
14
|
+
private readRevisions = new Map<BlockId, number>();
|
|
8
15
|
|
|
9
16
|
constructor(
|
|
10
17
|
private readonly collectionId: BlockId,
|
|
11
18
|
private readonly transactor: ITransactor,
|
|
12
19
|
public actionContext: ActionContext | undefined,
|
|
13
|
-
|
|
20
|
+
collector?: ReadDependencyCollector,
|
|
21
|
+
) {
|
|
22
|
+
this.collector = collector ?? new ReadDependencyCollector();
|
|
23
|
+
}
|
|
14
24
|
|
|
15
25
|
createBlockHeader(type: BlockType, newId?: BlockId): BlockHeader {
|
|
16
26
|
return {
|
|
@@ -25,24 +35,46 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
25
35
|
return uint8ArrayToString(randomBytes(32), 'base64url')
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
async tryGet(id: BlockId): Promise<TBlock | undefined> {
|
|
38
|
+
async tryGet(id: BlockId, purpose: ReadPurpose = 'value'): Promise<TBlock | undefined> {
|
|
29
39
|
const result = await this.transactor.get({ blockIds: [id], context: this.actionContext });
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
// Guard the per-key entry: some transactors return a sparse result that omits `id`
|
|
41
|
+
// entirely (e.g. block genuinely not found), so `result` is a truthy object but
|
|
42
|
+
// `result[id]` is undefined. Destructuring that would throw a TypeError.
|
|
43
|
+
const entry = result?.[id];
|
|
44
|
+
if (entry) {
|
|
45
|
+
const { block, state } = entry;
|
|
46
|
+
// Record a read dependency only for a block that actually exists. A transactor may return a
|
|
47
|
+
// populated entry with `block: undefined` for a genuinely-missing block (TestTransactor does;
|
|
48
|
+
// the Network transactor always populates the key); recording there would add a phantom
|
|
49
|
+
// dependency for a nonexistent block. This makes the "absent reads nothing" contract uniform
|
|
50
|
+
// with the sparse-result case (entry omitted) — see transactor-source.spec.ts sparse test.
|
|
51
|
+
if (block) {
|
|
52
|
+
// Record read dependency for optimistic concurrency control, carrying the caller's
|
|
53
|
+
// read purpose (default `value`) so a purely-structural navigation read can later be
|
|
54
|
+
// dropped from the conflict set (see ReadDependencyCollector / Theorem 5).
|
|
55
|
+
const rev = state.latest?.rev ?? 0;
|
|
56
|
+
this.collector.record(id, rev, purpose);
|
|
57
|
+
this.readRevisions.set(id, rev);
|
|
58
|
+
}
|
|
34
59
|
// TODO: if the state reports that there is a pending action, record this so that we are sure to update before syncing
|
|
35
60
|
//state.pendings
|
|
36
61
|
return block as TBlock;
|
|
37
62
|
}
|
|
38
63
|
}
|
|
39
64
|
|
|
65
|
+
/** The revision observed the last time this source served {@link id} (from its committed
|
|
66
|
+
* state), or undefined if this source has never served it. CacheSource reads this on a
|
|
67
|
+
* miss-load to learn the revision to record and store. */
|
|
68
|
+
getReadRevision(id: BlockId): number | undefined {
|
|
69
|
+
return this.readRevisions.get(id);
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
getReadDependencies(): ReadDependency[] {
|
|
41
|
-
return this.
|
|
73
|
+
return this.collector.getReadDependencies();
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
clearReadDependencies(): void {
|
|
45
|
-
this.
|
|
77
|
+
this.collector.clear();
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
/**
|
|
@@ -55,10 +87,13 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
55
87
|
* is transitioning to a new block. Ignored if the given headerId is not present in the transforms.
|
|
56
88
|
* @param tailId - The Id of the collection's log tail block. If specified, this block's transform is performed next
|
|
57
89
|
* (prior to the rest of the block operations), to resolve the "winner" of a race to commit to the collection.
|
|
90
|
+
* @param priority - Aged, advisory retry priority (default 0). Rides on the pend so a repeatedly-losing
|
|
91
|
+
* single-collection sync out-ranks fresh rivals in a concurrent race (`resolveRace`); fairness-only, never
|
|
92
|
+
* affects validity. Omitted from the pend when 0 so the common first-attempt pend serializes exactly as before.
|
|
58
93
|
* @returns A promise that resolves to undefined if the action is successful, or a StaleFailure if the action is stale.
|
|
59
94
|
*/
|
|
60
|
-
async transact(transform: Transforms, actionId: ActionId, rev: number, headerId: BlockId, tailId: BlockId): Promise<undefined | StaleFailure> {
|
|
61
|
-
const pendResult = await this.transactor.pend({ transforms: transform, actionId, rev, policy: 'r' });
|
|
95
|
+
async transact(transform: Transforms, actionId: ActionId, rev: number, headerId: BlockId, tailId: BlockId, priority = 0): Promise<undefined | StaleFailure> {
|
|
96
|
+
const pendResult = await this.transactor.pend({ transforms: transform, actionId, rev, policy: 'r', ...(priority > 0 ? { priority } : {}) });
|
|
62
97
|
if (!pendResult.success) {
|
|
63
98
|
return pendResult;
|
|
64
99
|
}
|
|
@@ -1,49 +1,92 @@
|
|
|
1
|
-
import { Atomic } from './atomic.js';
|
|
2
|
-
import type { IBlock, BlockId, BlockStore, BlockType, BlockHeader, BlockOperation } from '../index.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
1
|
+
import { Atomic } from './atomic.js';
|
|
2
|
+
import type { IBlock, BlockId, BlockStore, BlockType, BlockHeader, BlockOperation, ReadPurpose } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Opaque handle to an in-flight atomic scope.
|
|
6
|
+
*
|
|
7
|
+
* A method that opens a scope (via {@link AtomicProxy.atomic}) receives the scope as its
|
|
8
|
+
* callback argument. To make a *nested* call reuse the enclosing scope — rather than open
|
|
9
|
+
* (and separately commit) a second one — pass that handle back as the `parent` argument of
|
|
10
|
+
* the nested `atomic()` call. This is how genuine nesting is distinguished from a second,
|
|
11
|
+
* unrelated concurrent scope, portably (no `AsyncLocalStorage`, which is Node-only).
|
|
12
|
+
*/
|
|
13
|
+
export interface AtomicScope { readonly __atomicScope: unique symbol; }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A BlockStore proxy that enables scoped atomic operations.
|
|
17
|
+
* Operations normally delegate directly to the underlying store,
|
|
18
|
+
* but during an `atomic()` call, they route through an Atomic tracker
|
|
19
|
+
* that commits all-or-nothing on success, or rolls back on error.
|
|
20
|
+
*
|
|
21
|
+
* Both the BTree and its trunk should share the same AtomicProxy instance
|
|
22
|
+
* so that all mutations (including root pointer updates) are part of the
|
|
23
|
+
* same atomic batch.
|
|
24
|
+
*
|
|
25
|
+
* Concurrency model: top-level `atomic()` calls are *serialized* through a promise queue,
|
|
26
|
+
* so a second, un-awaited scope started while the first is still in flight waits for the
|
|
27
|
+
* first to commit/roll back before opening its own tracker — it never shares the first's
|
|
28
|
+
* tracker. A genuinely nested call (one that hands back its enclosing scope) bypasses the
|
|
29
|
+
* queue and reuses that scope, so it neither deadlocks on itself nor double-commits.
|
|
30
|
+
*/
|
|
31
|
+
export class AtomicProxy<T extends IBlock> implements BlockStore<T> {
|
|
32
|
+
private _base: BlockStore<T>;
|
|
33
|
+
private _active: BlockStore<T>;
|
|
34
|
+
/** Tail of the serialization queue; each new top-level scope awaits the prior one. */
|
|
35
|
+
private _tail: Promise<void> = Promise.resolve();
|
|
36
|
+
/** The scope currently executing, or undefined between scopes. Serialization guarantees
|
|
37
|
+
* at most one is active at a time, so identity against `parent` cleanly separates a
|
|
38
|
+
* nested call (parent === current) from a foreign concurrent one (parent absent/stale). */
|
|
39
|
+
private _current?: AtomicScope;
|
|
40
|
+
|
|
41
|
+
constructor(store: BlockStore<T>) {
|
|
42
|
+
this._base = store;
|
|
43
|
+
this._active = store;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async tryGet(id: BlockId, purpose?: ReadPurpose): Promise<T | undefined> { return this._active.tryGet(id, purpose); }
|
|
47
|
+
/** Forward a leaf-value upgrade to the active store (duck-typed; the Tracker/CacheSource chain
|
|
48
|
+
* implements it). Keeps navigation-read filtering working for a B-tree bound to this proxy. */
|
|
49
|
+
markReadValue(id: BlockId): void { (this._active as { markReadValue?: (id: BlockId) => void }).markReadValue?.(id); }
|
|
50
|
+
insert(block: T): void { this._active.insert(block); }
|
|
51
|
+
update(blockId: BlockId, op: BlockOperation): void { this._active.update(blockId, op); }
|
|
52
|
+
delete(blockId: BlockId): void { this._active.delete(blockId); }
|
|
53
|
+
generateId(): BlockId { return this._active.generateId(); }
|
|
54
|
+
createBlockHeader(type: BlockType, newId?: BlockId): BlockHeader { return this._active.createBlockHeader(type, newId); }
|
|
55
|
+
|
|
56
|
+
/** Execute fn within an atomic scope. All store mutations are collected and committed on
|
|
57
|
+
* success, or discarded on error. `fn` receives a scope handle; pass it as the `parent`
|
|
58
|
+
* of any nested `atomic()` call that should join this scope instead of opening its own.
|
|
59
|
+
* Re-entrant safe (nesting reuses the parent scope) and concurrency safe (unrelated
|
|
60
|
+
* overlapping scopes serialize rather than sharing a tracker). */
|
|
61
|
+
async atomic<R>(fn: (scope: AtomicScope) => Promise<R>, parent?: AtomicScope): Promise<R> {
|
|
62
|
+
// Genuine nesting: the caller handed back the scope it is already inside. Reuse it —
|
|
63
|
+
// no second tracker, no second commit — and skip the queue, since waiting on the very
|
|
64
|
+
// scope we are running inside would deadlock.
|
|
65
|
+
if (parent !== undefined && parent === this._current) {
|
|
66
|
+
return fn(parent);
|
|
67
|
+
}
|
|
68
|
+
// Top-level scope (first, or a foreign call that overlapped one in flight): take a
|
|
69
|
+
// place in the serialization queue so it runs against its own tracker only after the
|
|
70
|
+
// prior scope has committed/rolled back.
|
|
71
|
+
const prior = this._tail;
|
|
72
|
+
let release!: () => void;
|
|
73
|
+
this._tail = new Promise<void>(resolve => { release = resolve; });
|
|
74
|
+
await prior;
|
|
75
|
+
const atomic = new Atomic<T>(this._base);
|
|
76
|
+
const scope = atomic as unknown as AtomicScope;
|
|
77
|
+
this._current = scope;
|
|
78
|
+
this._active = atomic;
|
|
79
|
+
try {
|
|
80
|
+
const result = await fn(scope);
|
|
81
|
+
atomic.commit();
|
|
82
|
+
return result;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
atomic.reset();
|
|
85
|
+
throw e;
|
|
86
|
+
} finally {
|
|
87
|
+
this._active = this._base;
|
|
88
|
+
this._current = undefined;
|
|
89
|
+
release();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|