@optimystic/db-p2p 1.0.0-beta.3 → 1.1.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 (234) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +73 -1
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +70 -3
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/block-transfer.d.ts +16 -1
  6. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  7. package/dist/src/cluster/block-transfer.js +73 -72
  8. package/dist/src/cluster/block-transfer.js.map +1 -1
  9. package/dist/src/cluster/client.d.ts +4 -6
  10. package/dist/src/cluster/client.d.ts.map +1 -1
  11. package/dist/src/cluster/client.js +8 -13
  12. package/dist/src/cluster/client.js.map +1 -1
  13. package/dist/src/cluster/cluster-policy.d.ts +2 -1
  14. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  15. package/dist/src/cluster/cluster-policy.js.map +1 -1
  16. package/dist/src/cluster/cluster-repo.d.ts +155 -9
  17. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  18. package/dist/src/cluster/cluster-repo.js +446 -58
  19. package/dist/src/cluster/cluster-repo.js.map +1 -1
  20. package/dist/src/cluster/commit-proof.js +2 -2
  21. package/dist/src/cluster/race-resolution.d.ts +51 -7
  22. package/dist/src/cluster/race-resolution.d.ts.map +1 -1
  23. package/dist/src/cluster/race-resolution.js +71 -7
  24. package/dist/src/cluster/race-resolution.js.map +1 -1
  25. package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
  26. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  27. package/dist/src/cluster/rebalance-monitor.js +120 -26
  28. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  29. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  30. package/dist/src/cluster/spread-on-churn.js +34 -47
  31. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  32. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  33. package/dist/src/cohort-topic/host.js +8 -6
  34. package/dist/src/cohort-topic/host.js.map +1 -1
  35. package/dist/src/cohort-topic/membership-source.js +1 -1
  36. package/dist/src/cohort-topic/membership-source.js.map +1 -1
  37. package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
  38. package/dist/src/cohort-topic/peer-codec.js +5 -2
  39. package/dist/src/cohort-topic/peer-codec.js.map +1 -1
  40. package/dist/src/cohort-topic/stream-util.d.ts +24 -3
  41. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  42. package/dist/src/cohort-topic/stream-util.js +36 -3
  43. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  44. package/dist/src/cohort-topic/topic-router.d.ts +9 -0
  45. package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
  46. package/dist/src/cohort-topic/topic-router.js +12 -2
  47. package/dist/src/cohort-topic/topic-router.js.map +1 -1
  48. package/dist/src/dispute/client.d.ts.map +1 -1
  49. package/dist/src/dispute/client.js +15 -3
  50. package/dist/src/dispute/client.js.map +1 -1
  51. package/dist/src/index.d.ts +5 -0
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +5 -0
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/libp2p-key-network.d.ts +78 -17
  56. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  57. package/dist/src/libp2p-key-network.js +234 -147
  58. package/dist/src/libp2p-key-network.js.map +1 -1
  59. package/dist/src/libp2p-node-base.d.ts +35 -1
  60. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  61. package/dist/src/libp2p-node-base.js +151 -26
  62. package/dist/src/libp2p-node-base.js.map +1 -1
  63. package/dist/src/libp2p-node-rn.d.ts +2 -2
  64. package/dist/src/libp2p-node-rn.d.ts.map +1 -1
  65. package/dist/src/libp2p-node-rn.js.map +1 -1
  66. package/dist/src/libp2p-node.d.ts +2 -2
  67. package/dist/src/libp2p-node.d.ts.map +1 -1
  68. package/dist/src/libp2p-node.js.map +1 -1
  69. package/dist/src/logger.d.ts.map +1 -1
  70. package/dist/src/logger.js +6 -1
  71. package/dist/src/logger.js.map +1 -1
  72. package/dist/src/matchmaking/query-transport.d.ts +3 -4
  73. package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
  74. package/dist/src/matchmaking/query-transport.js +9 -9
  75. package/dist/src/matchmaking/query-transport.js.map +1 -1
  76. package/dist/src/network/network-manager-service.d.ts +10 -5
  77. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  78. package/dist/src/network/network-manager-service.js +11 -2
  79. package/dist/src/network/network-manager-service.js.map +1 -1
  80. package/dist/src/network/relay-reservation.d.ts +170 -0
  81. package/dist/src/network/relay-reservation.d.ts.map +1 -0
  82. package/dist/src/network/relay-reservation.js +527 -0
  83. package/dist/src/network/relay-reservation.js.map +1 -0
  84. package/dist/src/pend-validation.d.ts +3 -2
  85. package/dist/src/pend-validation.d.ts.map +1 -1
  86. package/dist/src/pend-validation.js +3 -2
  87. package/dist/src/pend-validation.js.map +1 -1
  88. package/dist/src/protocol-client.d.ts +8 -1
  89. package/dist/src/protocol-client.d.ts.map +1 -1
  90. package/dist/src/protocol-client.js +11 -0
  91. package/dist/src/protocol-client.js.map +1 -1
  92. package/dist/src/reactivity/forwarder-host.d.ts +2 -1
  93. package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
  94. package/dist/src/reactivity/forwarder-host.js +2 -1
  95. package/dist/src/reactivity/forwarder-host.js.map +1 -1
  96. package/dist/src/reactivity/origination-manager.d.ts +3 -3
  97. package/dist/src/reactivity/origination-manager.js +3 -3
  98. package/dist/src/reactivity/recover-transport.d.ts +22 -14
  99. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  100. package/dist/src/reactivity/recover-transport.js +32 -19
  101. package/dist/src/reactivity/recover-transport.js.map +1 -1
  102. package/dist/src/reactivity/subscription-manager.d.ts +2 -2
  103. package/dist/src/reactivity/topic-bytes.d.ts +6 -7
  104. package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
  105. package/dist/src/reactivity/topic-bytes.js +8 -9
  106. package/dist/src/reactivity/topic-bytes.js.map +1 -1
  107. package/dist/src/repo/client.d.ts +5 -2
  108. package/dist/src/repo/client.d.ts.map +1 -1
  109. package/dist/src/repo/client.js +49 -40
  110. package/dist/src/repo/client.js.map +1 -1
  111. package/dist/src/repo/cluster-coordinator.d.ts +96 -11
  112. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  113. package/dist/src/repo/cluster-coordinator.js +164 -25
  114. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  115. package/dist/src/repo/coordinator-repo.d.ts +240 -122
  116. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  117. package/dist/src/repo/coordinator-repo.js +627 -473
  118. package/dist/src/repo/coordinator-repo.js.map +1 -1
  119. package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
  120. package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
  121. package/dist/src/repo/i-under-replication-ledger.js +2 -0
  122. package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
  123. package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
  124. package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
  125. package/dist/src/repo/kv-under-replication-ledger.js +287 -0
  126. package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
  127. package/dist/src/repo/responsibility.d.ts +37 -0
  128. package/dist/src/repo/responsibility.d.ts.map +1 -0
  129. package/dist/src/repo/responsibility.js +37 -0
  130. package/dist/src/repo/responsibility.js.map +1 -0
  131. package/dist/src/repo/service.d.ts +44 -15
  132. package/dist/src/repo/service.d.ts.map +1 -1
  133. package/dist/src/repo/service.js +64 -27
  134. package/dist/src/repo/service.js.map +1 -1
  135. package/dist/src/repo/stuck-reservation.d.ts +127 -0
  136. package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
  137. package/dist/src/repo/stuck-reservation.js +179 -0
  138. package/dist/src/repo/stuck-reservation.js.map +1 -0
  139. package/dist/src/repo/under-replication-drain.d.ts +208 -0
  140. package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
  141. package/dist/src/repo/under-replication-drain.js +461 -0
  142. package/dist/src/repo/under-replication-drain.js.map +1 -0
  143. package/dist/src/rn.d.ts +5 -0
  144. package/dist/src/rn.d.ts.map +1 -1
  145. package/dist/src/rn.js +5 -0
  146. package/dist/src/rn.js.map +1 -1
  147. package/dist/src/storage/block-latch.d.ts +3 -2
  148. package/dist/src/storage/block-latch.d.ts.map +1 -1
  149. package/dist/src/storage/block-latch.js +7 -4
  150. package/dist/src/storage/block-latch.js.map +1 -1
  151. package/dist/src/storage/block-storage.d.ts +45 -3
  152. package/dist/src/storage/block-storage.d.ts.map +1 -1
  153. package/dist/src/storage/block-storage.js +156 -9
  154. package/dist/src/storage/block-storage.js.map +1 -1
  155. package/dist/src/storage/i-block-storage.d.ts +57 -5
  156. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  157. package/dist/src/storage/i-block-storage.js.map +1 -1
  158. package/dist/src/storage/pending-claim.d.ts +167 -0
  159. package/dist/src/storage/pending-claim.d.ts.map +1 -0
  160. package/dist/src/storage/pending-claim.js +156 -0
  161. package/dist/src/storage/pending-claim.js.map +1 -0
  162. package/dist/src/storage/raw-store-codec.d.ts +13 -0
  163. package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
  164. package/dist/src/storage/raw-store-codec.js +24 -3
  165. package/dist/src/storage/raw-store-codec.js.map +1 -1
  166. package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
  167. package/dist/src/storage/restoration-coordinator.js +2 -1
  168. package/dist/src/storage/restoration-coordinator.js.map +1 -1
  169. package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
  170. package/dist/src/storage/ring-shift-coordinator.js +3 -3
  171. package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
  172. package/dist/src/storage/storage-repo.d.ts +122 -5
  173. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  174. package/dist/src/storage/storage-repo.js +245 -68
  175. package/dist/src/storage/storage-repo.js.map +1 -1
  176. package/dist/src/storage/struct.d.ts +58 -0
  177. package/dist/src/storage/struct.d.ts.map +1 -1
  178. package/dist/src/testing/mesh-harness.d.ts +77 -11
  179. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  180. package/dist/src/testing/mesh-harness.js +226 -100
  181. package/dist/src/testing/mesh-harness.js.map +1 -1
  182. package/package.json +2 -2
  183. package/readme.md +27 -3
  184. package/src/cluster/block-transfer-service.ts +132 -4
  185. package/src/cluster/block-transfer.ts +79 -84
  186. package/src/cluster/client.ts +115 -119
  187. package/src/cluster/cluster-policy.ts +2 -1
  188. package/src/cluster/cluster-repo.ts +493 -65
  189. package/src/cluster/commit-proof.ts +2 -2
  190. package/src/cluster/race-resolution.ts +76 -7
  191. package/src/cluster/rebalance-monitor.ts +677 -539
  192. package/src/cluster/spread-on-churn.ts +34 -54
  193. package/src/cohort-topic/host.ts +8 -6
  194. package/src/cohort-topic/membership-source.ts +1 -1
  195. package/src/cohort-topic/peer-codec.ts +34 -31
  196. package/src/cohort-topic/stream-util.ts +39 -4
  197. package/src/cohort-topic/topic-router.ts +12 -2
  198. package/src/dispute/client.ts +17 -7
  199. package/src/index.ts +64 -59
  200. package/src/libp2p-key-network.ts +1377 -1260
  201. package/src/libp2p-node-base.ts +186 -27
  202. package/src/libp2p-node-rn.ts +31 -30
  203. package/src/libp2p-node.ts +37 -36
  204. package/src/logger.ts +6 -1
  205. package/src/matchmaking/query-transport.ts +9 -9
  206. package/src/network/network-manager-service.ts +16 -5
  207. package/src/network/relay-reservation.ts +636 -0
  208. package/src/pend-validation.ts +3 -2
  209. package/src/protocol-client.ts +13 -1
  210. package/src/reactivity/forwarder-host.ts +2 -1
  211. package/src/reactivity/origination-manager.ts +3 -3
  212. package/src/reactivity/recover-transport.ts +39 -22
  213. package/src/reactivity/subscription-manager.ts +2 -2
  214. package/src/reactivity/topic-bytes.ts +35 -37
  215. package/src/repo/client.ts +172 -173
  216. package/src/repo/cluster-coordinator.ts +183 -25
  217. package/src/repo/coordinator-repo.ts +656 -506
  218. package/src/repo/i-under-replication-ledger.ts +89 -0
  219. package/src/repo/kv-under-replication-ledger.ts +302 -0
  220. package/src/repo/responsibility.ts +48 -0
  221. package/src/repo/service.ts +76 -39
  222. package/src/repo/stuck-reservation.ts +224 -0
  223. package/src/repo/under-replication-drain.ts +582 -0
  224. package/src/rn.ts +74 -69
  225. package/src/storage/block-latch.ts +7 -4
  226. package/src/storage/block-storage.ts +161 -10
  227. package/src/storage/i-block-storage.ts +60 -5
  228. package/src/storage/pending-claim.ts +200 -0
  229. package/src/storage/raw-store-codec.ts +26 -3
  230. package/src/storage/restoration-coordinator.ts +212 -211
  231. package/src/storage/ring-shift-coordinator.ts +271 -271
  232. package/src/storage/storage-repo.ts +280 -72
  233. package/src/storage/struct.ts +58 -0
  234. package/src/testing/mesh-harness.ts +293 -112
@@ -0,0 +1,200 @@
1
+ import type { ActionId, BlockId, PendRequest, Transform } from "@optimystic/db-core";
2
+
3
+ /**
4
+ * One pending record on a block, with the revision its pend asked for.
5
+ *
6
+ * `rev` is the `PendRequest.rev` the record was written under (see
7
+ * `IBlockStorage.savePendingTransaction`), kept in the block's metadata beside `latest`
8
+ * (`BlockMetadata.pendingRevs`). Absent for a record that named no revision — an insert-only pend,
9
+ * which no production caller sends — and for a record written before the revision was kept at all.
10
+ * Both read as "unknown", and an unknown claim is treated as the strongest one (see
11
+ * {@link isReservationAgainst}), so an old record can only refuse more than it should, never less.
12
+ */
13
+ export interface PendingClaim {
14
+ actionId: ActionId;
15
+ rev?: number;
16
+ /**
17
+ * The committed revision the record's update operations were computed against — the pend's
18
+ * `baseRevs[blockId]`, kept in `BlockMetadata.pendingBases`. Absent for a record whose pend
19
+ * carried no base for this block (inserted, deleted, or unknown to the author) and for a record
20
+ * written before the field existed; both read as "base unknown". Unlike an unknown `rev`, an
21
+ * unknown base is NOT the strongest kind of anything: it is simply nothing to compare, and each
22
+ * apply site decides what that means for it (`StorageRepo.internalCommit` falls back to the
23
+ * commit's own declaration; the read-driven promotion in `StorageRepo.get` declines; the rival
24
+ * rule, {@link isReservationAgainst}, keeps the revision rule for it).
25
+ */
26
+ baseRev?: number;
27
+ }
28
+
29
+ /**
30
+ * What {@link isReservationAgainst} reads of an incoming pend, for one block: the revision it
31
+ * requests, and the committed base the block's update operations were computed against — built from
32
+ * the pend by {@link reservationRequestFor}, so the rule reads exactly the base storage would keep.
33
+ */
34
+ export interface ReservationRequest {
35
+ rev?: number;
36
+ baseRev?: number;
37
+ }
38
+
39
+ /**
40
+ * Whether a rival's pending record is a RESERVATION against an incoming pend for the same block —
41
+ * the one question both rival scans ask (`StorageRepo.pend` at apply,
42
+ * `ClusterMember.validatePendOperations` at the promise vote).
43
+ *
44
+ * A pending record reserves the block for the revision it claims: it says "I am about to commit
45
+ * this block at revision `rev`", and it holds that slot from pend-apply until its commit or cancel.
46
+ * It stops reserving once the incoming writer has BUILT ON it — read the block with that change in
47
+ * it — because then admitting the newcomer cannot lose the change, whatever became of the record's
48
+ * action. The rule, first match wins:
49
+ *
50
+ * - Either side's revision unknown → reserves. A record predating revision recording keeps the
51
+ * old behaviour rather than silently admitting a rival, and a rev-less request cannot be placed
52
+ * past anything.
53
+ * - The record carries a base AND the request carries a usable one ({@link usableBase}: a number
54
+ * below the requested revision) → reserves iff `claim.rev > baseRev`. A base at or past the
55
+ * claimed slot means the newcomer's operations were computed against a version of the block that
56
+ * already holds the record's change (it committed there, and this member merely missed the
57
+ * commit — the newcomer's own commit brings it current), or against a version past a slot the
58
+ * record lost. A base below it means the newcomer read the block without that change, and
59
+ * admitting it would let its commit apply over the stale base and sweep the record — the rival's
60
+ * change lost while the log names it.
61
+ * - Otherwise → reserves iff `claim.rev >= rev` (the revision rule). Revisions are allocated per
62
+ * COLLECTION and a writer pends at one past the collection revision it read, so a claim below the
63
+ * requested revision is one the collection has moved past, which is taken as having been built
64
+ * on. That is sound when the newcomer read the block from a member that held the record
65
+ * (`StorageRepo.get` promotes a record for any action the reader's context names, with the block
66
+ * floor, `BlockGets.floors`, as the second line), and false in exactly one shape: a member that
67
+ * never received the rival's pend — possible only where the promise quorum can leave a member
68
+ * out, see {@link cohortCanMissAPend} — serving a floor-less handle while the rival's data-block
69
+ * commit is still in flight. It stays the rule for a request with no usable base (an inserted or
70
+ * deleted block, a sender that names none, or a malformed base), and for a record with no stored
71
+ * base (an inserted or deleted block, or a record written by a release that did not keep bases).
72
+ * A base-less record keeps clearing the moment the collection moves past its slot, as it always
73
+ * did: judged by the newcomer's base, one whose commit ran nowhere could never clear, since no
74
+ * member can promote it (the read-driven promotion declines a record with no base), and a node
75
+ * upgraded over its own leftovers would wedge on them. The price: a rival that DELETES the block
76
+ * keeps no base either, so the missed-pend shape below stays open for a rival delete, exactly as
77
+ * it was before the base arm existed.
78
+ *
79
+ * The base arm is never more permissive than the revision arm: an honest base is at most `rev - 1`,
80
+ * so a claim at or below the base is also below the requested revision. It can only hold more,
81
+ * which is why the safety argument of Theorem 1 does not weaken. It closes the missed-pend shape
82
+ * wherever the members still holding the rival's record are enough to deny the newcomer a
83
+ * super-majority — one member missing the pend leaves every other one holding it — and a member
84
+ * that has meanwhile taken the rival's change refuses the newcomer's commit at the fork guard
85
+ * (`StorageRepo.internalCommit`) instead. Liveness for the missed-commit
86
+ * shape (`a-member-that-missed-a-commit-refuses-every-later-write`) is preserved: a writer that read
87
+ * the block from a member that applied the rival's change declares at least that revision as its
88
+ * base, and one served by the missed-commit member itself is served after that member promotes the
89
+ * record, which it does whenever the record's stored base equals its latest.
90
+ *
91
+ * WHERE the base is read is the callers' decision, and deliberately narrow: only the promise vote
92
+ * reads it, and only for a cohort whose quorum can leave a member out ({@link cohortCanMissAPend});
93
+ * the apply-time scan in `StorageRepo.pend`, and the vote in every smaller cohort, pass no base. So
94
+ * the apply never refuses what the vote approved. Holding more costs liveness in two shapes, and the
95
+ * narrowing keeps both off cohorts that need unanimity (two and three members at the default
96
+ * threshold), where one member's hold sinks the pend: (1) a newcomer that read the block one change
97
+ * short is held until the rival commits, and its retries do not re-read the block today, so it then
98
+ * re-pends on the stale base and the fork guard refuses its commit — refused, never acknowledged over
99
+ * the rival (backlog `bug-a-writer-held-by-a-change-it-never-saw-retries-on-its-stale-copy`); (2) a
100
+ * record whose change no member will ever take — an abandoned action whose data-block pend landed,
101
+ * whose log-tail pend did not, and whose cancel never arrived — used to be superseded as soon as the
102
+ * collection moved past its slot, and under the base arm holds every newcomer instead.
103
+ *
104
+ * NOTE: from four members up, residual (2) wedges the block when the stray record stands on enough
105
+ * members to deny a super-majority (a writer that crashed after its data-block pend, before any
106
+ * cancel); a stray record on one member alone is outvoted, and that member's own apply (revision
107
+ * rule) then admits the newcomer, whose commit sweeps the record. Locally the member cannot tell the
108
+ * crashed writer's record from the missed-pend shape: both are a record claiming a slot past the
109
+ * block's latest, met by a newcomer whose base is that latest. Backlog
110
+ * `debt-unpromotable-pending-records-need-a-sweep` owns abandoned records, and both stuck-reservation
111
+ * lines name the block; if it shows up in the field, the cure is that sweep, not a looser rule here.
112
+ */
113
+ export function isReservationAgainst(claim: PendingClaim, request: ReservationRequest): boolean {
114
+ if (claim.rev === undefined || request.rev === undefined) {
115
+ return true;
116
+ }
117
+ const base = claim.baseRev === undefined ? undefined : usableBase(request);
118
+ return base === undefined ? claim.rev >= request.rev : claim.rev > base;
119
+ }
120
+
121
+ /**
122
+ * Whether a cohort of `peerCount` members, promising at `superMajorityThreshold`, can reach its
123
+ * promise super-majority without one of its members — so a member can miss a pend the rest of the
124
+ * cohort stored, and later serve the block one change short. That is the only shape the base arm of
125
+ * {@link isReservationAgainst} closes, and it is also exactly where one member's hold cannot sink a
126
+ * pend by itself. Below it (two and three members at the default 0.75) every member's approval is
127
+ * needed, so the base arm could only add holds that no lost update justifies. The promise vote
128
+ * reads the incoming base only when this is true.
129
+ */
130
+ export function cohortCanMissAPend(peerCount: number, superMajorityThreshold: number): boolean {
131
+ return Math.ceil(peerCount * superMajorityThreshold) < peerCount;
132
+ }
133
+
134
+ /**
135
+ * `request.baseRev` when {@link isReservationAgainst} can read it: a number strictly below the
136
+ * requested revision. Anything else — absent, not a number (untrusted wire data), or at or past
137
+ * `rev`, which no honest writer sends since its base is a revision it read and it pends past what it
138
+ * read — is `undefined`, and the rule falls back to the requested revision alone.
139
+ */
140
+ export function usableBase(request: ReservationRequest): number | undefined {
141
+ const { rev, baseRev } = request;
142
+ return typeof baseRev === 'number' && rev !== undefined && baseRev < rev ? baseRev : undefined;
143
+ }
144
+
145
+ /**
146
+ * The {@link ReservationRequest} `pend` makes of `blockId`, whose transform within the pend is
147
+ * `transform`: the pend's revision, and the base {@link declaredBaseFor} keeps for the block — so a
148
+ * base on an inserted or deleted block, or a malformed one, is read by the rule exactly as storage
149
+ * keeps it: not at all. `ignoredBase` carries a base the pend named for the block that the rule does
150
+ * not read (malformed, not below the revision, or on a base-independent transform), for the caller's
151
+ * log line; no producer sends one.
152
+ */
153
+ export function reservationRequestFor(
154
+ pend: Pick<PendRequest, 'rev' | 'baseRevs'>, blockId: BlockId, transform: Transform
155
+ ): { request: ReservationRequest; ignoredBase?: unknown } {
156
+ const request: ReservationRequest = { rev: pend.rev };
157
+ const baseRev = declaredBaseFor(pend.baseRevs, blockId, transform);
158
+ if (baseRev !== undefined) {
159
+ request.baseRev = baseRev;
160
+ }
161
+ const named: unknown = pend.baseRevs?.[blockId];
162
+ return named !== undefined && usableBase(request) === undefined ? { request, ignoredBase: named } : { request };
163
+ }
164
+
165
+ /**
166
+ * Whether `transform`'s result does not depend on the block's prior content: an insert replaces the
167
+ * block wholesale and a delete materializes to nothing (delete-last-wins in `applyTransform`), so
168
+ * neither can fork on a different base, and no base is ever named for them (`Tracker.stagedBaseRevs`
169
+ * names update-only blocks alone). Every base check keys on this, on the member's OWN pended
170
+ * transform (or the incoming pend's own transform, at the rival scans), never on what a request
171
+ * declares.
172
+ */
173
+ export function isBaseIndependent(transform: Transform): boolean {
174
+ return Boolean(transform.insert) || Boolean(transform.delete);
175
+ }
176
+
177
+ /**
178
+ * The base a pend declares for `blockId`, as storage will keep it: `baseRevs[blockId]` when it is a
179
+ * number and the block's transform is update-only, else nothing. Untrusted wire data with no ingress
180
+ * schema (the posture `validateCommitOperations` takes toward `blockDigests`): a malformed or surplus
181
+ * entry is ignored for that id, never thrown on; an entry for an inserted or deleted block — which no
182
+ * producer sends — is dropped, so a base-independent record can never read as base-dependent later.
183
+ *
184
+ * NOTE: the base is stored AS TOLD, and the pend never refuses on it — not when it is behind this
185
+ * member's latest, and not when it is ahead. A member has no grounds to second-guess the author's
186
+ * claim about the author's own computation (the client is what makes an honest claim correct:
187
+ * `Tracker` pins the base at the first staged update and re-stages over a moved one). Refusing here
188
+ * would also be the wrong tier: a member holding a torn or abandoned HIGHER revision would cast a
189
+ * reject that, at three members, fails every honest retry — the writer re-reads the majority's
190
+ * revision and declares it again — whereas the same mismatch at commit time is one member's refusal
191
+ * (`StorageRepo.internalCommit`), heals by reconcile, and the cohort still commits on majority
192
+ * durability.
193
+ */
194
+ export function declaredBaseFor(baseRevs: PendRequest['baseRevs'], blockId: BlockId, transform: Transform): number | undefined {
195
+ if (isBaseIndependent(transform)) {
196
+ return undefined;
197
+ }
198
+ const declared = baseRevs?.[blockId];
199
+ return typeof declared === 'number' ? declared : undefined;
200
+ }
@@ -18,8 +18,15 @@ import type { ActionId } from "@optimystic/db-core";
18
18
  * Do NOT normalize ranges here.
19
19
  */
20
20
 
21
+ // The encoder stays module-scope: Hermes provides `TextEncoder` natively (React Native 0.74+).
21
22
  const encoder = new TextEncoder();
22
- const decoder = new TextDecoder();
23
+
24
+ // The decoder is built on first use, NOT at module load. Hermes has no native `TextDecoder`, and
25
+ // this module is loaded by the React Native entry (`rn.ts`, via `kv-raw-storage.ts`), so a
26
+ // module-scope `new TextDecoder()` made the import itself throw whenever it ran ahead of the host's
27
+ // polyfill (readme.md § React Native). Memoized, so every call shares one instance as before.
28
+ let decoderInstance: TextDecoder | undefined;
29
+ const decoder = (): TextDecoder => decoderInstance ??= new TextDecoder();
23
30
 
24
31
  /** JSON-encode a value (`BlockMetadata` / `Transform` / `IBlock`) to UTF-8 bytes. */
25
32
  export function encodeJson<T>(value: T): Uint8Array {
@@ -28,7 +35,7 @@ export function encodeJson<T>(value: T): Uint8Array {
28
35
 
29
36
  /** Decode UTF-8 JSON bytes back into a value. Callers guard the miss (`undefined`) first. */
30
37
  export function decodeJson<T>(bytes: Uint8Array): T {
31
- return JSON.parse(decoder.decode(bytes)) as T;
38
+ return JSON.parse(decoder().decode(bytes)) as T;
32
39
  }
33
40
 
34
41
  /** Encode an `ActionId` string to UTF-8 bytes (stored as the revisions-store value). */
@@ -38,5 +45,21 @@ export function encodeActionId(actionId: ActionId): Uint8Array {
38
45
 
39
46
  /** Decode UTF-8 bytes back into an `ActionId` string. */
40
47
  export function decodeActionId(bytes: Uint8Array): ActionId {
41
- return decoder.decode(bytes) as ActionId;
48
+ return decoder().decode(bytes) as ActionId;
49
+ }
50
+
51
+ /**
52
+ * Deep-copy a value this codec decoded (`IBlock` / `Transform`), by the same JSON round-trip the
53
+ * store applies. Used where a caller must mutate a stored value without touching the original, in
54
+ * place of `structuredClone` — which Hermes (React Native) does not provide.
55
+ *
56
+ * Lossless for exactly the values this module hands out: anything `decodeJson` returned is already
57
+ * JSON-shaped, so it carries none of what a JSON round-trip drops or rewrites (an `undefined` array
58
+ * slot, an `undefined`-valued property, a `Date`, a `Uint8Array`) — those were already rewritten when
59
+ * the value was stored. It is NOT a general-purpose `structuredClone` replacement: a live object
60
+ * that never crossed this codec can come back different. `undefined` passes through, since
61
+ * `JSON.parse(JSON.stringify(undefined))` throws.
62
+ */
63
+ export function cloneDecoded<T>(value: T): T {
64
+ return value === undefined ? value : JSON.parse(JSON.stringify(value)) as T;
42
65
  }