@optimystic/db-core 0.25.1 → 0.27.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.
Files changed (104) hide show
  1. package/dist/src/cohort-topic/membership/verifier.d.ts +8 -0
  2. package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
  3. package/dist/src/cohort-topic/membership/verifier.js +17 -0
  4. package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
  5. package/dist/src/cohort-topic/promotion.d.ts +25 -0
  6. package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
  7. package/dist/src/cohort-topic/promotion.js +58 -11
  8. package/dist/src/cohort-topic/promotion.js.map +1 -1
  9. package/dist/src/cohort-topic/wire/primitives.d.ts +6 -0
  10. package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -1
  11. package/dist/src/cohort-topic/wire/primitives.js +6 -0
  12. package/dist/src/cohort-topic/wire/primitives.js.map +1 -1
  13. package/dist/src/cohort-topic/wire/types.d.ts +5 -0
  14. package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
  15. package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
  16. package/dist/src/cohort-topic/wire/validate.js +10 -19
  17. package/dist/src/cohort-topic/wire/validate.js.map +1 -1
  18. package/dist/src/collection/collection.d.ts +8 -1
  19. package/dist/src/collection/collection.d.ts.map +1 -1
  20. package/dist/src/collection/collection.js +19 -3
  21. package/dist/src/collection/collection.js.map +1 -1
  22. package/dist/src/matchmaking/wire.d.ts +2 -2
  23. package/dist/src/matchmaking/wire.d.ts.map +1 -1
  24. package/dist/src/matchmaking/wire.js +3 -3
  25. package/dist/src/matchmaking/wire.js.map +1 -1
  26. package/dist/src/network/struct.d.ts +5 -5
  27. package/dist/src/network/struct.d.ts.map +1 -1
  28. package/dist/src/testing/index.d.ts +1 -0
  29. package/dist/src/testing/index.d.ts.map +1 -1
  30. package/dist/src/testing/index.js +1 -0
  31. package/dist/src/testing/index.js.map +1 -1
  32. package/dist/src/testing/refresh-probe.d.ts +31 -0
  33. package/dist/src/testing/refresh-probe.d.ts.map +1 -0
  34. package/dist/src/testing/refresh-probe.js +39 -0
  35. package/dist/src/testing/refresh-probe.js.map +1 -0
  36. package/dist/src/testing/test-transactor.d.ts +58 -5
  37. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  38. package/dist/src/testing/test-transactor.js +87 -4
  39. package/dist/src/testing/test-transactor.js.map +1 -1
  40. package/dist/src/transaction/coordinator.d.ts +90 -6
  41. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  42. package/dist/src/transaction/coordinator.js +311 -90
  43. package/dist/src/transaction/coordinator.js.map +1 -1
  44. package/dist/src/transaction/errors.d.ts +42 -0
  45. package/dist/src/transaction/errors.d.ts.map +1 -1
  46. package/dist/src/transaction/errors.js +50 -0
  47. package/dist/src/transaction/errors.js.map +1 -1
  48. package/dist/src/transaction/index.d.ts +1 -1
  49. package/dist/src/transaction/index.d.ts.map +1 -1
  50. package/dist/src/transaction/index.js +1 -1
  51. package/dist/src/transaction/index.js.map +1 -1
  52. package/dist/src/transaction/session.d.ts +7 -3
  53. package/dist/src/transaction/session.d.ts.map +1 -1
  54. package/dist/src/transaction/session.js +7 -3
  55. package/dist/src/transaction/session.js.map +1 -1
  56. package/dist/src/transaction/transaction.d.ts +12 -2
  57. package/dist/src/transaction/transaction.d.ts.map +1 -1
  58. package/dist/src/transaction/transaction.js.map +1 -1
  59. package/dist/src/transactor/network-transactor.d.ts +1 -1
  60. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  61. package/dist/src/transactor/network-transactor.js +6 -26
  62. package/dist/src/transactor/network-transactor.js.map +1 -1
  63. package/dist/src/transactor/transactor-source.d.ts +4 -6
  64. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  65. package/dist/src/transactor/transactor-source.js +4 -6
  66. package/dist/src/transactor/transactor-source.js.map +1 -1
  67. package/dist/src/transform/atomic.d.ts.map +1 -1
  68. package/dist/src/transform/atomic.js +7 -0
  69. package/dist/src/transform/atomic.js.map +1 -1
  70. package/dist/src/transform/base-pins.d.ts +48 -0
  71. package/dist/src/transform/base-pins.d.ts.map +1 -0
  72. package/dist/src/transform/base-pins.js +65 -0
  73. package/dist/src/transform/base-pins.js.map +1 -0
  74. package/dist/src/transform/digest.d.ts +4 -2
  75. package/dist/src/transform/digest.d.ts.map +1 -1
  76. package/dist/src/transform/digest.js +14 -19
  77. package/dist/src/transform/digest.js.map +1 -1
  78. package/dist/src/transform/tracker.d.ts +31 -3
  79. package/dist/src/transform/tracker.d.ts.map +1 -1
  80. package/dist/src/transform/tracker.js +105 -13
  81. package/dist/src/transform/tracker.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/cohort-topic/membership/verifier.ts +487 -461
  84. package/src/cohort-topic/promotion.ts +86 -14
  85. package/src/cohort-topic/wire/primitives.ts +195 -188
  86. package/src/cohort-topic/wire/types.ts +480 -475
  87. package/src/cohort-topic/wire/validate.ts +504 -512
  88. package/src/collection/collection.ts +19 -3
  89. package/src/matchmaking/wire.ts +605 -603
  90. package/src/network/struct.ts +5 -5
  91. package/src/testing/index.ts +1 -0
  92. package/src/testing/refresh-probe.ts +52 -0
  93. package/src/testing/test-transactor.ts +100 -4
  94. package/src/transaction/coordinator.ts +312 -99
  95. package/src/transaction/errors.ts +141 -91
  96. package/src/transaction/index.ts +1 -1
  97. package/src/transaction/session.ts +7 -3
  98. package/src/transaction/transaction.ts +12 -2
  99. package/src/transactor/network-transactor.ts +1025 -1045
  100. package/src/transactor/transactor-source.ts +170 -172
  101. package/src/transform/atomic.ts +6 -0
  102. package/src/transform/base-pins.ts +83 -0
  103. package/src/transform/digest.ts +14 -19
  104. package/src/transform/tracker.ts +108 -15
@@ -1,91 +1,141 @@
1
- import type { CollectionId } from "../collection/index.js";
2
-
3
- /**
4
- * Thrown by {@link TransactionCoordinator.commit} when a multi-collection commit
5
- * fails AFTER at least one collection has already DURABLY committed through the
6
- * distributed consensus path (GATHER/PEND/COMMIT).
7
- *
8
- * ## Why this exists (and why we can't just "roll back")
9
- *
10
- * The COMMIT phase commits each collection's pended blocks independently (see
11
- * `commitPhase`). A per-collection commit can *permanently* fail — e.g. a racing
12
- * transaction advanced that collection's log tail between PEND and COMMIT (a stale
13
- * loss) — while the other collections commit successfully. Those durable commits
14
- * are per-collection and there is no cross-collection undo, so a failure on one
15
- * collection cannot un-commit the ones that already landed.
16
- *
17
- * Uniformly restoring every collection's pre-commit local state (as a clean
18
- * rollback would) is exactly wrong here: for a collection that DID durably commit
19
- * it would re-stage its already-durable actions as still-pending, making local
20
- * tracker memory disagree with cluster storage. Instead the coordinator gives the
21
- * committed collections the success-path local treatment (fold to cache + reset)
22
- * and only reverts the failed/never-committed collections, then surfaces THIS error
23
- * naming both sets so the caller knows reconciliation is required and does NOT
24
- * falsely report a clean rollback.
25
- *
26
- * This is the session-mode / distributed-consensus analog of the plugin's legacy
27
- * `PartialCommitError` (single-node, per-tree `sync()`).
28
- *
29
- * ## The design decision is settled (not "still open")
30
- *
31
- * The default multi-collection guarantee is formally **atomicity of intent + eventual,
32
- * reported visibility**, NOT all-or-nothing — see `docs/correctness.md` **Theorem 3** and
33
- * `docs/transactions.md` (§ "Session-mode (distributed) commit is not atomic across
34
- * collections"). This error IS that guarantee's reporting surface, not a placeholder for a
35
- * stronger one. Genuine cross-collection all-or-nothing is a future opt-in strong mode
36
- * (backlog `feat-cross-collection-atomic-commit`).
37
- *
38
- * ## Reconcile contract for the catcher
39
- *
40
- * A caller receiving this error MUST NOT blindly retry the whole transaction and MUST NOT
41
- * treat it as a clean abort: `committedCollections` are durable and cannot be rolled back,
42
- * so a whole-transaction retry would double-apply them. Reconcile the named committed set
43
- * against `failedCollections` (re-drive only the failed collections, or repair the split).
44
- */
45
- export class CoordinatorPartialCommitError extends Error {
46
- constructor(
47
- /** Collections durably committed via consensus before the failure (NOT rolled back). */
48
- public readonly committedCollections: readonly CollectionId[],
49
- /** Collections that never committed this attempt (local state reverted for retry). */
50
- public readonly failedCollections: readonly CollectionId[],
51
- /** The underlying commit-phase failure that aborted the commit. */
52
- public readonly reason?: unknown,
53
- ) {
54
- super(
55
- `Multi-collection commit was not atomic: ${committedCollections.length} collection(s) ` +
56
- `durably committed via distributed consensus before the commit failed and CANNOT be ` +
57
- `rolled back — reconciliation is required. ` +
58
- `Committed (durable, now out of sync with the failed collections): [${committedCollections.join(', ')}]. ` +
59
- `Failed (never committed; local state reverted for retry): [${failedCollections.join(', ')}]. ` +
60
- `Underlying failure: ${reason instanceof Error ? reason.message : String(reason)}`
61
- );
62
- this.name = 'CoordinatorPartialCommitError';
63
- }
64
- }
65
-
66
- /**
67
- * Thrown by {@link TransactionCoordinator.commit} when a multi-collection commit failed as a
68
- * CLEAN stale loss — an optimistic-concurrency conflict (a racing transaction advanced a log tail)
69
- * in which NOTHING durably committed, so every participating collection's local tracker was
70
- * restored to its pre-append state and the transaction is safe to re-drive.
71
- *
72
- * This is the retryable counterpart to {@link CoordinatorPartialCommitError}: a partial landing
73
- * cannot be blindly retried (it would double-apply the durable half), but a clean loss can. The
74
- * coordinator's built-in backoff+jitter retry catches this internally and re-drives after re-reading
75
- * fresh revisions; it only escapes to the caller once the retry budget (`maxAttempts` / `deadlineMs`)
76
- * is exhausted, at which point it signals "gave up after a clean loss" rather than a partial split.
77
- */
78
- export class CoordinatorStaleLossError extends Error {
79
- constructor(
80
- /** Collections that lost the race this attempt (all had their local state reverted for retry). */
81
- public readonly failedCollections: readonly CollectionId[],
82
- /** The underlying stale/conflict reason surfaced by the failed pend/commit phase. */
83
- public readonly reason?: string,
84
- ) {
85
- super(
86
- `Multi-collection commit failed on a clean stale loss (no collection durably committed) ` +
87
- `for [${failedCollections.join(', ')}]` + (reason ? ` — ${reason}` : '')
88
- );
89
- this.name = 'CoordinatorStaleLossError';
90
- }
91
- }
1
+ import type { CollectionId } from "../collection/index.js";
2
+
3
+ /**
4
+ * Thrown by {@link TransactionCoordinator.commit} when a multi-collection commit
5
+ * fails AFTER at least one collection has already DURABLY committed through the
6
+ * distributed consensus path (GATHER/PEND/COMMIT).
7
+ *
8
+ * ## Why this exists (and why we can't just "roll back")
9
+ *
10
+ * The COMMIT phase commits each collection's pended blocks independently (see
11
+ * `commitPhase`). A per-collection commit can *permanently* fail — e.g. a racing
12
+ * transaction advanced that collection's log tail between PEND and COMMIT (a stale
13
+ * loss) — while the other collections commit successfully. Those durable commits
14
+ * are per-collection and there is no cross-collection undo, so a failure on one
15
+ * collection cannot un-commit the ones that already landed.
16
+ *
17
+ * Uniformly restoring every collection's pre-commit local state (as a clean
18
+ * rollback would) is exactly wrong here: for a collection that DID durably commit
19
+ * it would re-stage its already-durable actions as still-pending, making local
20
+ * tracker memory disagree with cluster storage. Instead the coordinator gives the
21
+ * committed collections the success-path local treatment (fold to cache + reset)
22
+ * and only reverts the failed/never-committed collections, then surfaces THIS error
23
+ * naming both sets so the caller knows reconciliation is required and does NOT
24
+ * falsely report a clean rollback.
25
+ *
26
+ * This is the session-mode / distributed-consensus analog of the plugin's legacy
27
+ * `PartialCommitError` (single-node, per-tree `sync()`).
28
+ *
29
+ * ## The design decision is settled (not "still open")
30
+ *
31
+ * The default multi-collection guarantee is formally **atomicity of intent + eventual,
32
+ * reported visibility**, NOT all-or-nothing — see `docs/correctness.md` **Theorem 3** and
33
+ * `docs/transactions.md` (§ "Session-mode (distributed) commit is not atomic across
34
+ * collections"). This error IS that guarantee's reporting surface, not a placeholder for a
35
+ * stronger one. Genuine cross-collection all-or-nothing is a future opt-in strong mode
36
+ * (backlog `feat-cross-collection-atomic-commit`).
37
+ *
38
+ * ## Reconcile contract for the catcher
39
+ *
40
+ * A caller receiving this error MUST NOT blindly retry the whole transaction and MUST NOT
41
+ * treat it as a clean abort: `committedCollections` are durable and cannot be rolled back,
42
+ * so a whole-transaction retry would double-apply them. Reconcile the named committed set
43
+ * against `failedCollections` (re-drive only the failed collections, or repair the split).
44
+ */
45
+ export class CoordinatorPartialCommitError extends Error {
46
+ constructor(
47
+ /** Collections durably committed via consensus before the failure (NOT rolled back). */
48
+ public readonly committedCollections: readonly CollectionId[],
49
+ /** Collections that never committed this attempt (local state reverted for retry). */
50
+ public readonly failedCollections: readonly CollectionId[],
51
+ /** The underlying commit-phase failure that aborted the commit. */
52
+ public readonly reason?: unknown,
53
+ ) {
54
+ super(
55
+ `Multi-collection commit was not atomic: ${committedCollections.length} collection(s) ` +
56
+ `durably committed via distributed consensus before the commit failed and CANNOT be ` +
57
+ `rolled back — reconciliation is required. ` +
58
+ `Committed (durable, now out of sync with the failed collections): [${committedCollections.join(', ')}]. ` +
59
+ `Failed (never committed; local state reverted for retry): [${failedCollections.join(', ')}]. ` +
60
+ `Underlying failure: ${reason instanceof Error ? reason.message : String(reason)}`
61
+ );
62
+ this.name = 'CoordinatorPartialCommitError';
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Thrown by {@link TransactionCoordinator.commit} when a multi-collection commit failed as a
68
+ * CLEAN stale loss — an optimistic-concurrency conflict (a racing transaction advanced a log tail)
69
+ * in which NOTHING durably committed, so every participating collection's local tracker was
70
+ * restored to its pre-append state and the transaction is safe to re-drive.
71
+ *
72
+ * This is the retryable counterpart to {@link CoordinatorPartialCommitError}: a partial landing
73
+ * cannot be blindly retried (it would double-apply the durable half), but a clean loss can. The
74
+ * coordinator's built-in backoff+jitter retry catches this internally and re-drives after re-reading
75
+ * fresh revisions; it only escapes to the caller once the retry budget (`maxAttempts` / `deadlineMs`)
76
+ * is exhausted, at which point it signals "gave up after a clean loss" rather than a partial split.
77
+ */
78
+ export class CoordinatorStaleLossError extends Error {
79
+ constructor(
80
+ /** Collections that lost the race this attempt (all had their local state reverted for retry). */
81
+ public readonly failedCollections: readonly CollectionId[],
82
+ /** The underlying stale/conflict reason surfaced by the failed pend/commit phase. */
83
+ public readonly reason?: string,
84
+ ) {
85
+ super(
86
+ `Multi-collection commit failed on a clean stale loss (no collection durably committed) ` +
87
+ `for [${failedCollections.join(', ')}]` + (reason ? ` — ${reason}` : '')
88
+ );
89
+ this.name = 'CoordinatorStaleLossError';
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Thrown by {@link TransactionCoordinator.applyActions} and {@link TransactionCoordinator.commit}
95
+ * (and returned as a failure result by {@link TransactionCoordinator.execute}) when a second
96
+ * transaction stamp is opened while the coordinator already tracks an open stamp.
97
+ *
98
+ * ## Why the coordinator refuses instead of isolating
99
+ *
100
+ * The coordinator's registered collections hold exactly ONE staged state — one set of tracker
101
+ * transforms and one pending action queue per collection, shared by every open stamp. Two stamps
102
+ * staging through one coordinator therefore read each other's uncommitted rows, and committing
103
+ * either stamp writes the OTHER's staged actions into its own durable log entry (a commit builds
104
+ * each log entry from the collection's whole pending queue, and the pended transforms carry no
105
+ * stamp tag to filter by). There is no correct commit for that configuration, so the coordinator
106
+ * enforces at most one open stamp at a time.
107
+ *
108
+ * ## Recovery
109
+ *
110
+ * Commit or roll back the open stamp first. A CLEAN commit failure deliberately keeps its stamp
111
+ * open so `rollback(stampId)` remains a complete recovery — which means an abandoned failed
112
+ * commit holds the coordinator against new stamps until someone rolls it back.
113
+ *
114
+ * Callers that genuinely need concurrent writers must give each writer its own `Collection`
115
+ * instances — its own bridge and coordinator (see docs/transactions.md, "One writer at a time on
116
+ * the shared TransactionBridge"). Two configurations stay the caller's contract to avoid because
117
+ * this guard cannot see them: two coordinators sharing the same collection instances, and two
118
+ * writers that stage only via `Tree.stage` / `Collection.act` (which open no stamp at all).
119
+ *
120
+ * A stamp opens at its first {@link TransactionCoordinator.applyActions} call. On the Quereus
121
+ * path that is the pre-stage-barrier call the bridge makes at the first STATEMENT — so a second
122
+ * bridge sharing this coordinator fails at its first statement, not at BEGIN.
123
+ */
124
+ export class CoordinatorConcurrentStampError extends Error {
125
+ constructor(
126
+ /** The stamp already open on this coordinator. */
127
+ public readonly openStampId: string,
128
+ /** The stamp that was refused. */
129
+ public readonly rejectedStampId: string,
130
+ ) {
131
+ super(
132
+ `Transaction stamp ${rejectedStampId} refused: this coordinator already tracks open stamp ` +
133
+ `${openStampId}, and its collections hold only one staged state, so two concurrent stamps ` +
134
+ `cannot be kept apart. Commit or roll back stamp ${openStampId} first — a failed commit ` +
135
+ `keeps its stamp open until it is rolled back. Concurrent writers each need their own ` +
136
+ `Collection instances (own bridge/coordinator per writer). A stamp opens at its first ` +
137
+ `applied statement (the pre-stage applyActions barrier), not at BEGIN.`
138
+ );
139
+ this.name = 'CoordinatorConcurrentStampError';
140
+ }
141
+ }
@@ -34,7 +34,7 @@ export {
34
34
 
35
35
  export { ReadDependencyCollector } from './read-dependency-collector.js';
36
36
  export { TransactionCoordinator } from './coordinator.js';
37
- export { CoordinatorPartialCommitError, CoordinatorStaleLossError } from './errors.js';
37
+ export { CoordinatorConcurrentStampError, CoordinatorPartialCommitError, CoordinatorStaleLossError } from './errors.js';
38
38
  export { TransactionSession } from './session.js';
39
39
  export { TransactionValidator } from './validator.js';
40
40
  export type { EngineRegistration, ValidationCoordinatorFactory, BlockStateProvider } from './validator.js';
@@ -177,9 +177,13 @@ export class TransactionSession {
177
177
  /**
178
178
  * Rollback the transaction (undo this session's applied actions).
179
179
  *
180
- * Delegates to coordinator.rollback(stampId) which restores collection
181
- * trackers to the pre-session snapshot and replays any later sessions'
182
- * actions to preserve their transforms.
180
+ * Delegates to coordinator.rollback(stampId), which restores each collection's staged
181
+ * state — tracker transforms AND the pending action queue — to the pre-session snapshot.
182
+ * That restore is coordinator-wide rather than session-scoped, which is safe because the
183
+ * coordinator permits at most one open stamp: there is no concurrent session whose staged
184
+ * state it could discard. Restoring both halves is what keeps this session's actions out
185
+ * of the next transaction's durable log entry; see the snapshot contract on
186
+ * TransactionCoordinator.stampData.
183
187
  */
184
188
  async rollback(): Promise<void> {
185
189
  if (this.committed) {
@@ -342,8 +342,18 @@ export type ExecutionResult = {
342
342
  * On a PARTIAL multi-collection commit failure, the collections that DID durably
343
343
  * commit through consensus before the failure (and thus CANNOT be rolled back —
344
344
  * reconciliation is required). Absent/empty means nothing durably committed, so
345
- * the caller may treat the failure as a clean abort. See
346
- * {@link CoordinatorPartialCommitError} for the session-mode (commit) analog.
345
+ * the caller may treat the failure as a clean abort — including calling
346
+ * `coordinator.rollback(stampId)`, which is still a valid and complete recovery there.
347
+ *
348
+ * When this IS present, the coordinator has already given the committed collections
349
+ * the success-path local treatment and restored the failed ones to their pre-staging
350
+ * state, and has DROPPED the transaction's rollback tracking — so
351
+ * `coordinator.rollback(stampId)` is a deliberate no-op rather than a rewind that
352
+ * would corrupt the collections that did commit. Reconciliation means driving a NEW
353
+ * transaction naming only the failed collections; re-driving THIS transaction through
354
+ * `execute()` would re-stage and re-apply the committed half a second time.
355
+ *
356
+ * See {@link CoordinatorPartialCommitError} for the session-mode (commit) analog.
347
357
  */
348
358
  committedCollections?: CollectionId[];
349
359
  /** On a partial multi-collection commit failure, the collections that failed to commit. */