@optimystic/db-core 0.21.0 → 0.24.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 (185) hide show
  1. package/README.md +336 -336
  2. package/dist/src/btree/btree.d.ts +2 -1
  3. package/dist/src/btree/btree.d.ts.map +1 -1
  4. package/dist/src/btree/btree.js +1 -1
  5. package/dist/src/btree/btree.js.map +1 -1
  6. package/dist/src/chain/chain.d.ts +1 -1
  7. package/dist/src/chain/chain.d.ts.map +1 -1
  8. package/dist/src/chain/chain.js +1 -1
  9. package/dist/src/chain/chain.js.map +1 -1
  10. package/dist/src/cluster/structs.d.ts +39 -1
  11. package/dist/src/cluster/structs.d.ts.map +1 -1
  12. package/dist/src/cluster/structs.js +24 -0
  13. package/dist/src/cluster/structs.js.map +1 -1
  14. package/dist/src/collection/collection.d.ts +20 -1
  15. package/dist/src/collection/collection.d.ts.map +1 -1
  16. package/dist/src/collection/collection.js +31 -4
  17. package/dist/src/collection/collection.js.map +1 -1
  18. package/dist/src/collections/diary/diary.d.ts.map +1 -1
  19. package/dist/src/collections/diary/diary.js +2 -1
  20. package/dist/src/collections/diary/diary.js.map +1 -1
  21. package/dist/src/collections/diary/struct.js +1 -1
  22. package/dist/src/collections/diary/struct.js.map +1 -1
  23. package/dist/src/collections/tree/collection-trunk.js +1 -1
  24. package/dist/src/collections/tree/collection-trunk.js.map +1 -1
  25. package/dist/src/collections/tree/struct.d.ts +1 -1
  26. package/dist/src/collections/tree/struct.d.ts.map +1 -1
  27. package/dist/src/collections/tree/struct.js +2 -1
  28. package/dist/src/collections/tree/struct.js.map +1 -1
  29. package/dist/src/collections/tree/tree.d.ts +5 -0
  30. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  31. package/dist/src/collections/tree/tree.js +7 -0
  32. package/dist/src/collections/tree/tree.js.map +1 -1
  33. package/dist/src/log/log.d.ts +1 -1
  34. package/dist/src/log/log.d.ts.map +1 -1
  35. package/dist/src/log/log.js +2 -2
  36. package/dist/src/log/log.js.map +1 -1
  37. package/dist/src/network/i-peer-network.d.ts +16 -0
  38. package/dist/src/network/i-peer-network.d.ts.map +1 -1
  39. package/dist/src/network/struct.d.ts +39 -2
  40. package/dist/src/network/struct.d.ts.map +1 -1
  41. package/dist/src/network/struct.js +18 -0
  42. package/dist/src/network/struct.js.map +1 -1
  43. package/dist/src/testing/test-transactor.d.ts +95 -8
  44. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  45. package/dist/src/testing/test-transactor.js +133 -12
  46. package/dist/src/testing/test-transactor.js.map +1 -1
  47. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  48. package/dist/src/transaction/coordinator.js +2 -1
  49. package/dist/src/transaction/coordinator.js.map +1 -1
  50. package/dist/src/transaction/transaction.d.ts +1 -1
  51. package/dist/src/transaction/transaction.js +1 -1
  52. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  53. package/dist/src/transactor/network-transactor.js +57 -18
  54. package/dist/src/transactor/network-transactor.js.map +1 -1
  55. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  56. package/dist/src/transactor/transactor-source.js +25 -2
  57. package/dist/src/transactor/transactor-source.js.map +1 -1
  58. package/dist/src/transform/cache-source.js +1 -1
  59. package/dist/src/transform/cache-source.js.map +1 -1
  60. package/dist/src/transform/helpers.d.ts +6 -1
  61. package/dist/src/transform/helpers.d.ts.map +1 -1
  62. package/dist/src/transform/helpers.js +7 -6
  63. package/dist/src/transform/helpers.js.map +1 -1
  64. package/dist/src/transform/tracker.d.ts.map +1 -1
  65. package/dist/src/transform/tracker.js +2 -1
  66. package/dist/src/transform/tracker.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/btree/btree.ts +2 -1
  69. package/src/chain/chain.ts +2 -1
  70. package/src/cluster/membership.ts +85 -85
  71. package/src/cluster/structs.ts +43 -4
  72. package/src/cohort-topic/addressing.ts +120 -120
  73. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
  74. package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
  75. package/src/cohort-topic/antidos/index.ts +5 -5
  76. package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
  77. package/src/cohort-topic/antidos/replay-guard.ts +146 -146
  78. package/src/cohort-topic/antidos/topic-budget.ts +160 -160
  79. package/src/cohort-topic/antiflood/index.ts +2 -2
  80. package/src/cohort-topic/antiflood/invariants.ts +108 -108
  81. package/src/cohort-topic/antiflood/jitter.ts +117 -117
  82. package/src/cohort-topic/coldstart.ts +237 -237
  83. package/src/cohort-topic/dmax.ts +88 -88
  84. package/src/cohort-topic/gossip/bus.ts +254 -254
  85. package/src/cohort-topic/gossip/index.ts +3 -3
  86. package/src/cohort-topic/gossip/records.ts +45 -45
  87. package/src/cohort-topic/gossip/view.ts +91 -91
  88. package/src/cohort-topic/index.ts +20 -20
  89. package/src/cohort-topic/load/barometer.ts +134 -134
  90. package/src/cohort-topic/load/index.ts +1 -1
  91. package/src/cohort-topic/member-engine.ts +430 -430
  92. package/src/cohort-topic/membership/index.ts +3 -3
  93. package/src/cohort-topic/membership/publisher.ts +163 -163
  94. package/src/cohort-topic/membership/source.ts +41 -41
  95. package/src/cohort-topic/membership/verifier.ts +461 -461
  96. package/src/cohort-topic/ports.ts +157 -157
  97. package/src/cohort-topic/promotion.ts +405 -405
  98. package/src/cohort-topic/registration/bytes.ts +37 -37
  99. package/src/cohort-topic/registration/handoff.ts +154 -154
  100. package/src/cohort-topic/registration/index.ts +6 -6
  101. package/src/cohort-topic/registration/renewal.ts +495 -495
  102. package/src/cohort-topic/registration/sharding.ts +61 -61
  103. package/src/cohort-topic/registration/store.ts +81 -81
  104. package/src/cohort-topic/registration/types.ts +91 -91
  105. package/src/cohort-topic/ring-hash.ts +50 -50
  106. package/src/cohort-topic/service.ts +416 -416
  107. package/src/cohort-topic/sig/index.ts +2 -2
  108. package/src/cohort-topic/sig/payloads.ts +59 -59
  109. package/src/cohort-topic/sig/threshold.ts +64 -64
  110. package/src/cohort-topic/tiers.ts +74 -74
  111. package/src/cohort-topic/traffic.ts +233 -233
  112. package/src/cohort-topic/walk.ts +326 -326
  113. package/src/cohort-topic/willingness.ts +237 -237
  114. package/src/cohort-topic/wire/codec.ts +216 -216
  115. package/src/cohort-topic/wire/index.ts +18 -18
  116. package/src/cohort-topic/wire/payloads.ts +126 -126
  117. package/src/cohort-topic/wire/primitives.ts +188 -188
  118. package/src/cohort-topic/wire/types.ts +475 -475
  119. package/src/cohort-topic/wire/validate.ts +512 -512
  120. package/src/collection/collection-type-registry.ts +37 -37
  121. package/src/collection/collection.ts +32 -4
  122. package/src/collections/diary/diary.ts +68 -67
  123. package/src/collections/diary/struct.ts +1 -1
  124. package/src/collections/tree/collection-trunk.ts +1 -1
  125. package/src/collections/tree/readme.md +4 -0
  126. package/src/collections/tree/struct.ts +3 -1
  127. package/src/collections/tree/tree.ts +320 -312
  128. package/src/log/log.ts +2 -2
  129. package/src/matchmaking/capability-filter.ts +45 -45
  130. package/src/matchmaking/config.ts +98 -98
  131. package/src/matchmaking/index.ts +21 -21
  132. package/src/matchmaking/multi-cohort-seeker.ts +234 -234
  133. package/src/matchmaking/provider.ts +123 -123
  134. package/src/matchmaking/query-eval.ts +105 -105
  135. package/src/matchmaking/seeker-walk.ts +127 -127
  136. package/src/matchmaking/seeker.ts +86 -86
  137. package/src/matchmaking/topic-anchor.ts +90 -90
  138. package/src/matchmaking/voting-quorum.ts +394 -394
  139. package/src/matchmaking/wire.ts +603 -603
  140. package/src/network/i-peer-network.ts +17 -0
  141. package/src/network/stale-failure.ts +43 -43
  142. package/src/network/struct.ts +41 -2
  143. package/src/network/types.ts +37 -37
  144. package/src/reactivity/backfill.ts +220 -220
  145. package/src/reactivity/backpressure.ts +191 -191
  146. package/src/reactivity/checkpoint.ts +308 -308
  147. package/src/reactivity/config.ts +172 -172
  148. package/src/reactivity/dedupe.ts +132 -132
  149. package/src/reactivity/forwarder.ts +87 -87
  150. package/src/reactivity/index.ts +34 -34
  151. package/src/reactivity/notification.ts +123 -123
  152. package/src/reactivity/policy.ts +79 -79
  153. package/src/reactivity/push-state.ts +310 -310
  154. package/src/reactivity/recover.ts +153 -153
  155. package/src/reactivity/replay-buffer.ts +141 -141
  156. package/src/reactivity/resume.ts +549 -549
  157. package/src/reactivity/rotation.ts +415 -415
  158. package/src/reactivity/subscriber.ts +132 -132
  159. package/src/reactivity/subscription.ts +66 -66
  160. package/src/reactivity/topic-anchor.ts +71 -71
  161. package/src/reactivity/verify.ts +73 -73
  162. package/src/reactivity/wire-validate.ts +13 -13
  163. package/src/reactivity/wire.ts +224 -224
  164. package/src/testing/async-wait.ts +65 -65
  165. package/src/testing/index.ts +2 -2
  166. package/src/testing/test-transactor.ts +638 -489
  167. package/src/transaction/coordinator.ts +2 -1
  168. package/src/transaction/errors.ts +91 -91
  169. package/src/transaction/operations-hash.ts +196 -196
  170. package/src/transaction/read-dependency-collector.ts +78 -78
  171. package/src/transaction/transaction.ts +1 -1
  172. package/src/transactor/change-notifier.ts +80 -80
  173. package/src/transactor/index.ts +5 -5
  174. package/src/transactor/network-transactor.ts +58 -19
  175. package/src/transactor/transactor-source.ts +25 -2
  176. package/src/transform/atomic-proxy.ts +92 -92
  177. package/src/transform/cache-source.ts +1 -1
  178. package/src/transform/helpers.ts +159 -158
  179. package/src/transform/tracker.ts +2 -1
  180. package/src/utility/backoff.ts +95 -95
  181. package/src/utility/batch-coordinator.ts +191 -191
  182. package/dist/src/transaction/context.d.ts +0 -60
  183. package/dist/src/transaction/context.d.ts.map +0 -1
  184. package/dist/src/transaction/context.js +0 -91
  185. package/dist/src/transaction/context.js.map +0 -1
@@ -1,126 +1,126 @@
1
- /**
2
- * Cohort-topic substrate — canonical participant-signature payloads.
3
- *
4
- * A participant peer-key-signs its outbound `RegisterV1` / `RenewV1` bodies (db-p2p supplies the
5
- * libp2p peer key); a cohort member recomputes the identical byte image to verify that signature
6
- * before admitting a register (anti-DoS) or honoring a crash-failover `reattach` attestation (so a
7
- * stray/MITM'd ping cannot usurp a live primary — `docs/cohort-topic.md` §TTL and renewal).
8
- *
9
- * Signer and verifier must agree byte-for-byte. Exactly like {@link import("../sig/payloads.js")}
10
- * for the threshold-signed notices, determinism comes from encoding an explicitly-ordered JSON
11
- * array (array order is stable, unlike object key order) as UTF-8 — never the `signature` envelope.
12
- * Optional fields are normalized to a fixed placeholder (`bootstrap`→`false`, `probe`→`false`,
13
- * `followOn`→`false`, `appPayload`→`null`, `bootstrapEvidence`→`null` with an empty string treated as
14
- * absent, `reattach`→`false`, `withdraw`→`false`) so an absent optional and a present-but-default
15
- * optional can never disagree across the wire round-trip.
16
- */
17
-
18
- import type { CohortGossipV1, RegisterV1, RenewV1 } from "./types.js";
19
-
20
- const utf8 = new TextEncoder();
21
-
22
- /** A `RegisterV1` minus its `signature` envelope — the bytes the participant peer-key-signs. */
23
- export type RegisterSignable = Omit<RegisterV1, "signature">;
24
-
25
- /** A `RenewV1` minus its `signature` envelope — the bytes the participant peer-key-signs. */
26
- export type RenewSignable = Omit<RenewV1, "signature">;
27
-
28
- /** Normalize the optional bootstrap-evidence slot: absent and empty-string both map to `null`. */
29
- function normalizeEvidence(bootstrapEvidence: string | undefined): string | null {
30
- return bootstrapEvidence === undefined || bootstrapEvidence === "" ? null : bootstrapEvidence;
31
- }
32
-
33
- /** Canonical signed byte image of a `RegisterV1` body (every field except `signature`). */
34
- export function registerSigningPayload(body: RegisterSignable): Uint8Array {
35
- return utf8.encode(JSON.stringify([
36
- "RegisterV1",
37
- body.v,
38
- body.topicId,
39
- body.tier,
40
- body.treeTier,
41
- body.participantCoord,
42
- body.ttl,
43
- body.bootstrap ?? false,
44
- body.appPayload ?? null,
45
- normalizeEvidence(body.bootstrapEvidence),
46
- body.timestamp,
47
- body.correlationId,
48
- // A read-only probe signs `probe: true`; a normal register signs `false`. Both sides recompute
49
- // this image identically, so a probe and a register over the same fields produce distinct signatures.
50
- body.probe ?? false,
51
- // A follow-on cold-start re-issue signs `followOn: true`, everything else `false`. Strictly appended
52
- // (sibling to `probe`/`withdraw`) so a MITM cannot strip or flip the follow-on assertion and the
53
- // signer (participant) + verifier (db-p2p cohort) agree byte-for-byte. Safe to append: no register
54
- // signatures are persisted (each is recomputed per verify), so there is no cross-version image concern.
55
- body.followOn ?? false,
56
- ]));
57
- }
58
-
59
- /** Canonical signed byte image of a `RenewV1` body (every field except `signature`). */
60
- export function renewSigningPayload(body: RenewSignable): Uint8Array {
61
- return utf8.encode(JSON.stringify([
62
- "RenewV1",
63
- body.v,
64
- body.topicId,
65
- body.participantId,
66
- body.correlationId,
67
- body.timestamp,
68
- body.reattach ?? false,
69
- // A withdraw tombstone signs `withdraw: true`, a ping/reattach signs `false`. Strictly appended
70
- // (array length 7→8) so the reattach-vs-ping distinction is preserved and a third party cannot
71
- // evict someone else's registration. Signer (participant) + verifier (db-p2p) move together; no
72
- // signatures are persisted, so there is no cross-version concern.
73
- body.withdraw ?? false,
74
- ]));
75
- }
76
-
77
- /** A `CohortGossipV1` minus its `signature` envelope — the bytes the gossiping member peer-key-signs. */
78
- export type CohortGossipSignable = Omit<CohortGossipV1, "signature">;
79
-
80
- /**
81
- * Canonical signed byte image of a `CohortGossipV1` (every field except `signature`). Intra-cohort
82
- * gossip is peer-key-signed by the originating member so a receiver can drop a frame whose `fromMember`
83
- * signature does not verify or that comes from a non-cohort member (it can never spoof willingness/load
84
- * or replicate forged records). Like the other payload helpers, determinism comes from an
85
- * explicitly-ordered array — nested records/summaries/evictions are emitted as fixed ordered tuples so
86
- * the signer and the receiver (which re-derives this image from the validated frame) agree byte-for-byte.
87
- * Absent optionals (`records`/`evicted`/`childLinks`/`childUnlinks`) normalize to `[]`, and `appState` to
88
- * `null`. `childLinks`/`childUnlinks` are covered so a MITM cannot strip or inject a child link/unlink.
89
- */
90
- export function cohortGossipSigningPayload(g: CohortGossipSignable): Uint8Array {
91
- return utf8.encode(JSON.stringify([
92
- "CohortGossipV1",
93
- g.v,
94
- g.fromMember,
95
- g.coord,
96
- g.cohortEpoch,
97
- g.treeTier,
98
- g.willingnessBits,
99
- g.loadBuckets,
100
- g.windowSeconds,
101
- g.topicSummaries.map((s) => [
102
- s.topicId,
103
- s.tier,
104
- s.directParticipants,
105
- s.arrivalsPerMin,
106
- s.queriesPerMin,
107
- s.promoted,
108
- s.childCohortCount,
109
- ]),
110
- (g.records ?? []).map((r) => [
111
- r.topicId,
112
- r.participantId,
113
- r.tier,
114
- r.primary,
115
- r.backups,
116
- r.attachedAt,
117
- r.lastPing,
118
- r.ttl,
119
- r.appState ?? null,
120
- ]),
121
- (g.evicted ?? []).map((e) => [e.topicId, e.participantId, e.lastPing]),
122
- (g.childLinks ?? []).map((c) => [c.topicId, c.childCohortCoord, c.effectiveAt]),
123
- (g.childUnlinks ?? []).map((c) => [c.topicId, c.childCohortCoord, c.effectiveAt]),
124
- g.timestamp,
125
- ]));
126
- }
1
+ /**
2
+ * Cohort-topic substrate — canonical participant-signature payloads.
3
+ *
4
+ * A participant peer-key-signs its outbound `RegisterV1` / `RenewV1` bodies (db-p2p supplies the
5
+ * libp2p peer key); a cohort member recomputes the identical byte image to verify that signature
6
+ * before admitting a register (anti-DoS) or honoring a crash-failover `reattach` attestation (so a
7
+ * stray/MITM'd ping cannot usurp a live primary — `docs/cohort-topic.md` §TTL and renewal).
8
+ *
9
+ * Signer and verifier must agree byte-for-byte. Exactly like {@link import("../sig/payloads.js")}
10
+ * for the threshold-signed notices, determinism comes from encoding an explicitly-ordered JSON
11
+ * array (array order is stable, unlike object key order) as UTF-8 — never the `signature` envelope.
12
+ * Optional fields are normalized to a fixed placeholder (`bootstrap`→`false`, `probe`→`false`,
13
+ * `followOn`→`false`, `appPayload`→`null`, `bootstrapEvidence`→`null` with an empty string treated as
14
+ * absent, `reattach`→`false`, `withdraw`→`false`) so an absent optional and a present-but-default
15
+ * optional can never disagree across the wire round-trip.
16
+ */
17
+
18
+ import type { CohortGossipV1, RegisterV1, RenewV1 } from "./types.js";
19
+
20
+ const utf8 = new TextEncoder();
21
+
22
+ /** A `RegisterV1` minus its `signature` envelope — the bytes the participant peer-key-signs. */
23
+ export type RegisterSignable = Omit<RegisterV1, "signature">;
24
+
25
+ /** A `RenewV1` minus its `signature` envelope — the bytes the participant peer-key-signs. */
26
+ export type RenewSignable = Omit<RenewV1, "signature">;
27
+
28
+ /** Normalize the optional bootstrap-evidence slot: absent and empty-string both map to `null`. */
29
+ function normalizeEvidence(bootstrapEvidence: string | undefined): string | null {
30
+ return bootstrapEvidence === undefined || bootstrapEvidence === "" ? null : bootstrapEvidence;
31
+ }
32
+
33
+ /** Canonical signed byte image of a `RegisterV1` body (every field except `signature`). */
34
+ export function registerSigningPayload(body: RegisterSignable): Uint8Array {
35
+ return utf8.encode(JSON.stringify([
36
+ "RegisterV1",
37
+ body.v,
38
+ body.topicId,
39
+ body.tier,
40
+ body.treeTier,
41
+ body.participantCoord,
42
+ body.ttl,
43
+ body.bootstrap ?? false,
44
+ body.appPayload ?? null,
45
+ normalizeEvidence(body.bootstrapEvidence),
46
+ body.timestamp,
47
+ body.correlationId,
48
+ // A read-only probe signs `probe: true`; a normal register signs `false`. Both sides recompute
49
+ // this image identically, so a probe and a register over the same fields produce distinct signatures.
50
+ body.probe ?? false,
51
+ // A follow-on cold-start re-issue signs `followOn: true`, everything else `false`. Strictly appended
52
+ // (sibling to `probe`/`withdraw`) so a MITM cannot strip or flip the follow-on assertion and the
53
+ // signer (participant) + verifier (db-p2p cohort) agree byte-for-byte. Safe to append: no register
54
+ // signatures are persisted (each is recomputed per verify), so there is no cross-version image concern.
55
+ body.followOn ?? false,
56
+ ]));
57
+ }
58
+
59
+ /** Canonical signed byte image of a `RenewV1` body (every field except `signature`). */
60
+ export function renewSigningPayload(body: RenewSignable): Uint8Array {
61
+ return utf8.encode(JSON.stringify([
62
+ "RenewV1",
63
+ body.v,
64
+ body.topicId,
65
+ body.participantId,
66
+ body.correlationId,
67
+ body.timestamp,
68
+ body.reattach ?? false,
69
+ // A withdraw tombstone signs `withdraw: true`, a ping/reattach signs `false`. Strictly appended
70
+ // (array length 7→8) so the reattach-vs-ping distinction is preserved and a third party cannot
71
+ // evict someone else's registration. Signer (participant) + verifier (db-p2p) move together; no
72
+ // signatures are persisted, so there is no cross-version concern.
73
+ body.withdraw ?? false,
74
+ ]));
75
+ }
76
+
77
+ /** A `CohortGossipV1` minus its `signature` envelope — the bytes the gossiping member peer-key-signs. */
78
+ export type CohortGossipSignable = Omit<CohortGossipV1, "signature">;
79
+
80
+ /**
81
+ * Canonical signed byte image of a `CohortGossipV1` (every field except `signature`). Intra-cohort
82
+ * gossip is peer-key-signed by the originating member so a receiver can drop a frame whose `fromMember`
83
+ * signature does not verify or that comes from a non-cohort member (it can never spoof willingness/load
84
+ * or replicate forged records). Like the other payload helpers, determinism comes from an
85
+ * explicitly-ordered array — nested records/summaries/evictions are emitted as fixed ordered tuples so
86
+ * the signer and the receiver (which re-derives this image from the validated frame) agree byte-for-byte.
87
+ * Absent optionals (`records`/`evicted`/`childLinks`/`childUnlinks`) normalize to `[]`, and `appState` to
88
+ * `null`. `childLinks`/`childUnlinks` are covered so a MITM cannot strip or inject a child link/unlink.
89
+ */
90
+ export function cohortGossipSigningPayload(g: CohortGossipSignable): Uint8Array {
91
+ return utf8.encode(JSON.stringify([
92
+ "CohortGossipV1",
93
+ g.v,
94
+ g.fromMember,
95
+ g.coord,
96
+ g.cohortEpoch,
97
+ g.treeTier,
98
+ g.willingnessBits,
99
+ g.loadBuckets,
100
+ g.windowSeconds,
101
+ g.topicSummaries.map((s) => [
102
+ s.topicId,
103
+ s.tier,
104
+ s.directParticipants,
105
+ s.arrivalsPerMin,
106
+ s.queriesPerMin,
107
+ s.promoted,
108
+ s.childCohortCount,
109
+ ]),
110
+ (g.records ?? []).map((r) => [
111
+ r.topicId,
112
+ r.participantId,
113
+ r.tier,
114
+ r.primary,
115
+ r.backups,
116
+ r.attachedAt,
117
+ r.lastPing,
118
+ r.ttl,
119
+ r.appState ?? null,
120
+ ]),
121
+ (g.evicted ?? []).map((e) => [e.topicId, e.participantId, e.lastPing]),
122
+ (g.childLinks ?? []).map((c) => [c.topicId, c.childCohortCoord, c.effectiveAt]),
123
+ (g.childUnlinks ?? []).map((c) => [c.topicId, c.childCohortCoord, c.effectiveAt]),
124
+ g.timestamp,
125
+ ]));
126
+ }
@@ -1,188 +1,188 @@
1
- /**
2
- * Cohort-topic wire substrate — shared structural-validation primitives.
3
- *
4
- * The generic per-field checks every wire codec across the substrate (cohort-topic, matchmaking,
5
- * reactivity) uses to narrow an already-parsed `unknown` (the output of `JSON.parse` on a decoded
6
- * frame) into a validated V1 shape. Each helper throws {@link CohortWireError} on a structural defect:
7
- * missing required field, wrong `v`, out-of-enum discriminant, a byte field that does not decode as
8
- * base64url, or an out-of-range numeric.
9
- *
10
- * These are the *only* generic primitives — domain-specific narrowing (the `validate*V1` functions, the
11
- * cohort-topic `tier` / `treeTier` semantics, matchmaking utf8 coders, reactivity notification shapes)
12
- * lives in the modules that own those message shapes. Keeping the primitives in one place means a
13
- * hardening tweak to a check (e.g. the fixed-length byte-width guard in {@link b64urlFixedLen}) lands
14
- * once and every consumer inherits it.
15
- */
16
-
17
- import { b64urlToBytes } from "./codec.js";
18
-
19
- /** Thrown for any malformed, oversized, or structurally invalid cohort-topic frame. */
20
- export class CohortWireError extends Error {
21
- constructor(message: string) {
22
- super(message);
23
- this.name = "CohortWireError";
24
- }
25
- }
26
-
27
- /** Throw a {@link CohortWireError} with `message`. */
28
- export function failWire(message: string): never {
29
- throw new CohortWireError(message);
30
- }
31
-
32
- /** Set `obj[key]` only when `value` is defined — keeps absent optionals off the decoded object. */
33
- export function assignDefined<T extends object, K extends keyof T>(obj: T, key: K, value: T[K] | undefined): void {
34
- if (value !== undefined) {
35
- obj[key] = value;
36
- }
37
- }
38
-
39
- /** Narrow `value` to a plain object (not null, not an array), or throw. */
40
- export function asObject(value: unknown, what: string): Record<string, unknown> {
41
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
42
- failWire(`${what}: expected an object`);
43
- }
44
- return value as Record<string, unknown>;
45
- }
46
-
47
- /** Require `obj.v === 1`. */
48
- export function requireV1(obj: Record<string, unknown>, what: string): void {
49
- if (obj["v"] !== 1) {
50
- failWire(`${what}: expected v === 1, got ${JSON.stringify(obj["v"])}`);
51
- }
52
- }
53
-
54
- /** Require `obj[key]` to be a string; returns it. */
55
- export function reqString(obj: Record<string, unknown>, key: string, what: string): string {
56
- const value = obj[key];
57
- if (typeof value !== "string") {
58
- failWire(`${what}: field "${key}" must be a string`);
59
- }
60
- return value;
61
- }
62
-
63
- /** Return `obj[key]` when it is a string, `undefined` when absent; throw on any other type. */
64
- export function optString(obj: Record<string, unknown>, key: string, what: string): string | undefined {
65
- const value = obj[key];
66
- if (value === undefined) {
67
- return undefined;
68
- }
69
- if (typeof value !== "string") {
70
- failWire(`${what}: field "${key}" must be a string when present`);
71
- }
72
- return value;
73
- }
74
-
75
- /** Require `obj[key]` to be a finite number; returns it. */
76
- export function reqFiniteNumber(obj: Record<string, unknown>, key: string, what: string): number {
77
- const value = obj[key];
78
- if (typeof value !== "number" || !Number.isFinite(value)) {
79
- failWire(`${what}: field "${key}" must be a finite number`);
80
- }
81
- return value;
82
- }
83
-
84
- /** Return `obj[key]` when it is a finite number, `undefined` when absent; throw on any other type. */
85
- export function optFiniteNumber(obj: Record<string, unknown>, key: string, what: string): number | undefined {
86
- const value = obj[key];
87
- if (value === undefined) {
88
- return undefined;
89
- }
90
- if (typeof value !== "number" || !Number.isFinite(value)) {
91
- failWire(`${what}: field "${key}" must be a finite number when present`);
92
- }
93
- return value;
94
- }
95
-
96
- /** Require `obj[key]` to be a boolean; returns it. */
97
- export function reqBool(obj: Record<string, unknown>, key: string, what: string): boolean {
98
- const value = obj[key];
99
- if (typeof value !== "boolean") {
100
- failWire(`${what}: field "${key}" must be a boolean`);
101
- }
102
- return value;
103
- }
104
-
105
- /** Return `obj[key]` when it is a boolean, `undefined` when absent; throw on any other type. */
106
- export function optBool(obj: Record<string, unknown>, key: string, what: string): boolean | undefined {
107
- const value = obj[key];
108
- if (value === undefined) {
109
- return undefined;
110
- }
111
- if (typeof value !== "boolean") {
112
- failWire(`${what}: field "${key}" must be a boolean when present`);
113
- }
114
- return value;
115
- }
116
-
117
- /**
118
- * Require `obj[key]` to be an integer `>= min` (and `<= max` when supplied); returns it. Reads and
119
- * finite-checks the field itself (via {@link reqFiniteNumber}) so callers pass the object + key rather
120
- * than a pre-extracted value — this composes with the other `req*` helpers.
121
- */
122
- export function reqIntInRange(obj: Record<string, unknown>, key: string, what: string, min: number, max?: number): number {
123
- const value = reqFiniteNumber(obj, key, what);
124
- if (!Number.isInteger(value) || value < min || (max !== undefined && value > max)) {
125
- const bound = max === undefined ? `>= ${min}` : `in ${min}..${max}`;
126
- failWire(`${what}: field "${key}" must be an integer ${bound}, got ${value}`);
127
- }
128
- return value;
129
- }
130
-
131
- /** Require `obj[key]` to be an array of strings; returns it. */
132
- export function reqStringArray(obj: Record<string, unknown>, key: string, what: string): string[] {
133
- const value = obj[key];
134
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
135
- failWire(`${what}: field "${key}" must be an array of strings`);
136
- }
137
- return value as string[];
138
- }
139
-
140
- /** Return `obj[key]` when it is an array of strings, `undefined` when absent; throw on any other type. */
141
- export function optStringArray(obj: Record<string, unknown>, key: string, what: string): string[] | undefined {
142
- if (obj[key] === undefined) {
143
- return undefined;
144
- }
145
- return reqStringArray(obj, key, what);
146
- }
147
-
148
- /** Require `obj[key]` to be one of `allowed`; returns it narrowed to the enum type. */
149
- export function reqEnum<T extends string>(obj: Record<string, unknown>, key: string, allowed: readonly T[], what: string): T {
150
- const value = obj[key];
151
- if (typeof value !== "string" || !(allowed as readonly string[]).includes(value)) {
152
- failWire(`${what}: field "${key}" must be one of ${allowed.join(" | ")}`);
153
- }
154
- return value as T;
155
- }
156
-
157
- /**
158
- * Assert a base64url string decodes cleanly; returns it unchanged. Used for variable-width fields —
159
- * peer ids (multihash-encoded, not 32 raw bytes), signatures, and opaque application payloads.
160
- *
161
- * NOTE: no max-length bound here. A hostile peer can still bloat one of these variable-width fields
162
- * (e.g. `participantCoord`, a signature) into a large map key in the store / rate limiter / replay
163
- * guard. Their widths aren't pinned by the spec, so a ceiling would be a chosen policy value rather
164
- * than a decode of the format. If a bloated one is ever seen as a map key in practice, add a
165
- * `b64urlMaxLen` ceiling here. (Fixed-width hash-derived fields go through `b64urlFixedLen` instead.)
166
- */
167
- export function b64urlField(value: string, key: string, what: string): string {
168
- try {
169
- b64urlToBytes(value);
170
- } catch {
171
- failWire(`${what}: field "${key}" is not valid base64url`);
172
- }
173
- return value;
174
- }
175
-
176
- /** Assert a base64url string decodes cleanly to exactly `len` bytes; returns it unchanged. */
177
- export function b64urlFixedLen(value: string, key: string, len: number, what: string): string {
178
- let bytes: Uint8Array;
179
- try {
180
- bytes = b64urlToBytes(value);
181
- } catch {
182
- return failWire(`${what}: field "${key}" is not valid base64url`);
183
- }
184
- if (bytes.length !== len) {
185
- failWire(`${what}: field "${key}" must decode to ${len} bytes, got ${bytes.length}`);
186
- }
187
- return value;
188
- }
1
+ /**
2
+ * Cohort-topic wire substrate — shared structural-validation primitives.
3
+ *
4
+ * The generic per-field checks every wire codec across the substrate (cohort-topic, matchmaking,
5
+ * reactivity) uses to narrow an already-parsed `unknown` (the output of `JSON.parse` on a decoded
6
+ * frame) into a validated V1 shape. Each helper throws {@link CohortWireError} on a structural defect:
7
+ * missing required field, wrong `v`, out-of-enum discriminant, a byte field that does not decode as
8
+ * base64url, or an out-of-range numeric.
9
+ *
10
+ * These are the *only* generic primitives — domain-specific narrowing (the `validate*V1` functions, the
11
+ * cohort-topic `tier` / `treeTier` semantics, matchmaking utf8 coders, reactivity notification shapes)
12
+ * lives in the modules that own those message shapes. Keeping the primitives in one place means a
13
+ * hardening tweak to a check (e.g. the fixed-length byte-width guard in {@link b64urlFixedLen}) lands
14
+ * once and every consumer inherits it.
15
+ */
16
+
17
+ import { b64urlToBytes } from "./codec.js";
18
+
19
+ /** Thrown for any malformed, oversized, or structurally invalid cohort-topic frame. */
20
+ export class CohortWireError extends Error {
21
+ constructor(message: string) {
22
+ super(message);
23
+ this.name = "CohortWireError";
24
+ }
25
+ }
26
+
27
+ /** Throw a {@link CohortWireError} with `message`. */
28
+ export function failWire(message: string): never {
29
+ throw new CohortWireError(message);
30
+ }
31
+
32
+ /** Set `obj[key]` only when `value` is defined — keeps absent optionals off the decoded object. */
33
+ export function assignDefined<T extends object, K extends keyof T>(obj: T, key: K, value: T[K] | undefined): void {
34
+ if (value !== undefined) {
35
+ obj[key] = value;
36
+ }
37
+ }
38
+
39
+ /** Narrow `value` to a plain object (not null, not an array), or throw. */
40
+ export function asObject(value: unknown, what: string): Record<string, unknown> {
41
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
42
+ failWire(`${what}: expected an object`);
43
+ }
44
+ return value as Record<string, unknown>;
45
+ }
46
+
47
+ /** Require `obj.v === 1`. */
48
+ export function requireV1(obj: Record<string, unknown>, what: string): void {
49
+ if (obj["v"] !== 1) {
50
+ failWire(`${what}: expected v === 1, got ${JSON.stringify(obj["v"])}`);
51
+ }
52
+ }
53
+
54
+ /** Require `obj[key]` to be a string; returns it. */
55
+ export function reqString(obj: Record<string, unknown>, key: string, what: string): string {
56
+ const value = obj[key];
57
+ if (typeof value !== "string") {
58
+ failWire(`${what}: field "${key}" must be a string`);
59
+ }
60
+ return value;
61
+ }
62
+
63
+ /** Return `obj[key]` when it is a string, `undefined` when absent; throw on any other type. */
64
+ export function optString(obj: Record<string, unknown>, key: string, what: string): string | undefined {
65
+ const value = obj[key];
66
+ if (value === undefined) {
67
+ return undefined;
68
+ }
69
+ if (typeof value !== "string") {
70
+ failWire(`${what}: field "${key}" must be a string when present`);
71
+ }
72
+ return value;
73
+ }
74
+
75
+ /** Require `obj[key]` to be a finite number; returns it. */
76
+ export function reqFiniteNumber(obj: Record<string, unknown>, key: string, what: string): number {
77
+ const value = obj[key];
78
+ if (typeof value !== "number" || !Number.isFinite(value)) {
79
+ failWire(`${what}: field "${key}" must be a finite number`);
80
+ }
81
+ return value;
82
+ }
83
+
84
+ /** Return `obj[key]` when it is a finite number, `undefined` when absent; throw on any other type. */
85
+ export function optFiniteNumber(obj: Record<string, unknown>, key: string, what: string): number | undefined {
86
+ const value = obj[key];
87
+ if (value === undefined) {
88
+ return undefined;
89
+ }
90
+ if (typeof value !== "number" || !Number.isFinite(value)) {
91
+ failWire(`${what}: field "${key}" must be a finite number when present`);
92
+ }
93
+ return value;
94
+ }
95
+
96
+ /** Require `obj[key]` to be a boolean; returns it. */
97
+ export function reqBool(obj: Record<string, unknown>, key: string, what: string): boolean {
98
+ const value = obj[key];
99
+ if (typeof value !== "boolean") {
100
+ failWire(`${what}: field "${key}" must be a boolean`);
101
+ }
102
+ return value;
103
+ }
104
+
105
+ /** Return `obj[key]` when it is a boolean, `undefined` when absent; throw on any other type. */
106
+ export function optBool(obj: Record<string, unknown>, key: string, what: string): boolean | undefined {
107
+ const value = obj[key];
108
+ if (value === undefined) {
109
+ return undefined;
110
+ }
111
+ if (typeof value !== "boolean") {
112
+ failWire(`${what}: field "${key}" must be a boolean when present`);
113
+ }
114
+ return value;
115
+ }
116
+
117
+ /**
118
+ * Require `obj[key]` to be an integer `>= min` (and `<= max` when supplied); returns it. Reads and
119
+ * finite-checks the field itself (via {@link reqFiniteNumber}) so callers pass the object + key rather
120
+ * than a pre-extracted value — this composes with the other `req*` helpers.
121
+ */
122
+ export function reqIntInRange(obj: Record<string, unknown>, key: string, what: string, min: number, max?: number): number {
123
+ const value = reqFiniteNumber(obj, key, what);
124
+ if (!Number.isInteger(value) || value < min || (max !== undefined && value > max)) {
125
+ const bound = max === undefined ? `>= ${min}` : `in ${min}..${max}`;
126
+ failWire(`${what}: field "${key}" must be an integer ${bound}, got ${value}`);
127
+ }
128
+ return value;
129
+ }
130
+
131
+ /** Require `obj[key]` to be an array of strings; returns it. */
132
+ export function reqStringArray(obj: Record<string, unknown>, key: string, what: string): string[] {
133
+ const value = obj[key];
134
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
135
+ failWire(`${what}: field "${key}" must be an array of strings`);
136
+ }
137
+ return value as string[];
138
+ }
139
+
140
+ /** Return `obj[key]` when it is an array of strings, `undefined` when absent; throw on any other type. */
141
+ export function optStringArray(obj: Record<string, unknown>, key: string, what: string): string[] | undefined {
142
+ if (obj[key] === undefined) {
143
+ return undefined;
144
+ }
145
+ return reqStringArray(obj, key, what);
146
+ }
147
+
148
+ /** Require `obj[key]` to be one of `allowed`; returns it narrowed to the enum type. */
149
+ export function reqEnum<T extends string>(obj: Record<string, unknown>, key: string, allowed: readonly T[], what: string): T {
150
+ const value = obj[key];
151
+ if (typeof value !== "string" || !(allowed as readonly string[]).includes(value)) {
152
+ failWire(`${what}: field "${key}" must be one of ${allowed.join(" | ")}`);
153
+ }
154
+ return value as T;
155
+ }
156
+
157
+ /**
158
+ * Assert a base64url string decodes cleanly; returns it unchanged. Used for variable-width fields —
159
+ * peer ids (multihash-encoded, not 32 raw bytes), signatures, and opaque application payloads.
160
+ *
161
+ * NOTE: no max-length bound here. A hostile peer can still bloat one of these variable-width fields
162
+ * (e.g. `participantCoord`, a signature) into a large map key in the store / rate limiter / replay
163
+ * guard. Their widths aren't pinned by the spec, so a ceiling would be a chosen policy value rather
164
+ * than a decode of the format. If a bloated one is ever seen as a map key in practice, add a
165
+ * `b64urlMaxLen` ceiling here. (Fixed-width hash-derived fields go through `b64urlFixedLen` instead.)
166
+ */
167
+ export function b64urlField(value: string, key: string, what: string): string {
168
+ try {
169
+ b64urlToBytes(value);
170
+ } catch {
171
+ failWire(`${what}: field "${key}" is not valid base64url`);
172
+ }
173
+ return value;
174
+ }
175
+
176
+ /** Assert a base64url string decodes cleanly to exactly `len` bytes; returns it unchanged. */
177
+ export function b64urlFixedLen(value: string, key: string, len: number, what: string): string {
178
+ let bytes: Uint8Array;
179
+ try {
180
+ bytes = b64urlToBytes(value);
181
+ } catch {
182
+ return failWire(`${what}: field "${key}" is not valid base64url`);
183
+ }
184
+ if (bytes.length !== len) {
185
+ failWire(`${what}: field "${key}" must decode to ${len} bytes, got ${bytes.length}`);
186
+ }
187
+ return value;
188
+ }