@optimystic/db-core 0.22.0 → 0.24.1

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 (141) hide show
  1. package/README.md +336 -336
  2. package/dist/src/cluster/structs.d.ts +39 -1
  3. package/dist/src/cluster/structs.d.ts.map +1 -1
  4. package/dist/src/cluster/structs.js +24 -0
  5. package/dist/src/cluster/structs.js.map +1 -1
  6. package/dist/src/collection/collection.d.ts +17 -0
  7. package/dist/src/collection/collection.d.ts.map +1 -1
  8. package/dist/src/collection/collection.js +24 -2
  9. package/dist/src/collection/collection.js.map +1 -1
  10. package/dist/src/collections/tree/tree.d.ts +5 -0
  11. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  12. package/dist/src/collections/tree/tree.js +7 -0
  13. package/dist/src/collections/tree/tree.js.map +1 -1
  14. package/dist/src/network/i-peer-network.d.ts +16 -0
  15. package/dist/src/network/i-peer-network.d.ts.map +1 -1
  16. package/dist/src/network/struct.d.ts +39 -2
  17. package/dist/src/network/struct.d.ts.map +1 -1
  18. package/dist/src/network/struct.js +18 -0
  19. package/dist/src/network/struct.js.map +1 -1
  20. package/dist/src/testing/test-transactor.d.ts +95 -8
  21. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  22. package/dist/src/testing/test-transactor.js +121 -8
  23. package/dist/src/testing/test-transactor.js.map +1 -1
  24. package/dist/src/transaction/transaction.d.ts +1 -1
  25. package/dist/src/transaction/transaction.js +1 -1
  26. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  27. package/dist/src/transactor/network-transactor.js +48 -13
  28. package/dist/src/transactor/network-transactor.js.map +1 -1
  29. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  30. package/dist/src/transactor/transactor-source.js +25 -2
  31. package/dist/src/transactor/transactor-source.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/cluster/membership.ts +85 -85
  34. package/src/cluster/structs.ts +43 -4
  35. package/src/cohort-topic/addressing.ts +120 -120
  36. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
  37. package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
  38. package/src/cohort-topic/antidos/index.ts +5 -5
  39. package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
  40. package/src/cohort-topic/antidos/replay-guard.ts +146 -146
  41. package/src/cohort-topic/antidos/topic-budget.ts +160 -160
  42. package/src/cohort-topic/antiflood/index.ts +2 -2
  43. package/src/cohort-topic/antiflood/invariants.ts +108 -108
  44. package/src/cohort-topic/antiflood/jitter.ts +117 -117
  45. package/src/cohort-topic/coldstart.ts +237 -237
  46. package/src/cohort-topic/dmax.ts +88 -88
  47. package/src/cohort-topic/gossip/bus.ts +254 -254
  48. package/src/cohort-topic/gossip/index.ts +3 -3
  49. package/src/cohort-topic/gossip/records.ts +45 -45
  50. package/src/cohort-topic/gossip/view.ts +91 -91
  51. package/src/cohort-topic/index.ts +20 -20
  52. package/src/cohort-topic/load/barometer.ts +134 -134
  53. package/src/cohort-topic/load/index.ts +1 -1
  54. package/src/cohort-topic/member-engine.ts +430 -430
  55. package/src/cohort-topic/membership/index.ts +3 -3
  56. package/src/cohort-topic/membership/publisher.ts +163 -163
  57. package/src/cohort-topic/membership/source.ts +41 -41
  58. package/src/cohort-topic/membership/verifier.ts +461 -461
  59. package/src/cohort-topic/ports.ts +157 -157
  60. package/src/cohort-topic/promotion.ts +405 -405
  61. package/src/cohort-topic/registration/bytes.ts +37 -37
  62. package/src/cohort-topic/registration/handoff.ts +154 -154
  63. package/src/cohort-topic/registration/index.ts +6 -6
  64. package/src/cohort-topic/registration/renewal.ts +495 -495
  65. package/src/cohort-topic/registration/sharding.ts +61 -61
  66. package/src/cohort-topic/registration/store.ts +81 -81
  67. package/src/cohort-topic/registration/types.ts +91 -91
  68. package/src/cohort-topic/ring-hash.ts +50 -50
  69. package/src/cohort-topic/service.ts +416 -416
  70. package/src/cohort-topic/sig/index.ts +2 -2
  71. package/src/cohort-topic/sig/payloads.ts +59 -59
  72. package/src/cohort-topic/sig/threshold.ts +64 -64
  73. package/src/cohort-topic/tiers.ts +74 -74
  74. package/src/cohort-topic/traffic.ts +233 -233
  75. package/src/cohort-topic/walk.ts +326 -326
  76. package/src/cohort-topic/willingness.ts +237 -237
  77. package/src/cohort-topic/wire/codec.ts +216 -216
  78. package/src/cohort-topic/wire/index.ts +18 -18
  79. package/src/cohort-topic/wire/payloads.ts +126 -126
  80. package/src/cohort-topic/wire/primitives.ts +188 -188
  81. package/src/cohort-topic/wire/types.ts +475 -475
  82. package/src/cohort-topic/wire/validate.ts +512 -512
  83. package/src/collection/collection-type-registry.ts +37 -37
  84. package/src/collection/collection.ts +25 -2
  85. package/src/collections/diary/diary.ts +68 -68
  86. package/src/collections/tree/readme.md +4 -0
  87. package/src/collections/tree/tree.ts +320 -312
  88. package/src/matchmaking/capability-filter.ts +45 -45
  89. package/src/matchmaking/config.ts +98 -98
  90. package/src/matchmaking/index.ts +21 -21
  91. package/src/matchmaking/multi-cohort-seeker.ts +234 -234
  92. package/src/matchmaking/provider.ts +123 -123
  93. package/src/matchmaking/query-eval.ts +105 -105
  94. package/src/matchmaking/seeker-walk.ts +127 -127
  95. package/src/matchmaking/seeker.ts +86 -86
  96. package/src/matchmaking/topic-anchor.ts +90 -90
  97. package/src/matchmaking/voting-quorum.ts +394 -394
  98. package/src/matchmaking/wire.ts +603 -603
  99. package/src/network/i-peer-network.ts +17 -0
  100. package/src/network/stale-failure.ts +43 -43
  101. package/src/network/struct.ts +41 -2
  102. package/src/network/types.ts +37 -37
  103. package/src/reactivity/backfill.ts +220 -220
  104. package/src/reactivity/backpressure.ts +191 -191
  105. package/src/reactivity/checkpoint.ts +308 -308
  106. package/src/reactivity/config.ts +172 -172
  107. package/src/reactivity/dedupe.ts +132 -132
  108. package/src/reactivity/forwarder.ts +87 -87
  109. package/src/reactivity/index.ts +34 -34
  110. package/src/reactivity/notification.ts +123 -123
  111. package/src/reactivity/policy.ts +79 -79
  112. package/src/reactivity/push-state.ts +310 -310
  113. package/src/reactivity/recover.ts +153 -153
  114. package/src/reactivity/replay-buffer.ts +141 -141
  115. package/src/reactivity/resume.ts +549 -549
  116. package/src/reactivity/rotation.ts +415 -415
  117. package/src/reactivity/subscriber.ts +132 -132
  118. package/src/reactivity/subscription.ts +66 -66
  119. package/src/reactivity/topic-anchor.ts +71 -71
  120. package/src/reactivity/verify.ts +73 -73
  121. package/src/reactivity/wire-validate.ts +13 -13
  122. package/src/reactivity/wire.ts +224 -224
  123. package/src/testing/async-wait.ts +65 -65
  124. package/src/testing/index.ts +2 -2
  125. package/src/testing/test-transactor.ts +638 -502
  126. package/src/transaction/errors.ts +91 -91
  127. package/src/transaction/operations-hash.ts +196 -196
  128. package/src/transaction/read-dependency-collector.ts +78 -78
  129. package/src/transaction/transaction.ts +1 -1
  130. package/src/transactor/change-notifier.ts +80 -80
  131. package/src/transactor/index.ts +5 -5
  132. package/src/transactor/network-transactor.ts +49 -14
  133. package/src/transactor/transactor-source.ts +25 -2
  134. package/src/transform/atomic-proxy.ts +92 -92
  135. package/src/transform/helpers.ts +159 -159
  136. package/src/utility/backoff.ts +95 -95
  137. package/src/utility/batch-coordinator.ts +191 -191
  138. package/dist/src/transaction/context.d.ts +0 -60
  139. package/dist/src/transaction/context.d.ts.map +0 -1
  140. package/dist/src/transaction/context.js +0 -91
  141. package/dist/src/transaction/context.js.map +0 -1
@@ -1,132 +1,132 @@
1
- /**
2
- * Reactivity — subscriber delivery path (`docs/reactivity.md` §Delivery).
3
- *
4
- * A subscriber receiving a notification:
5
- * 1. **verifies** `sig` against the cached tail-cohort `MembershipCertV1` (the {@link NotificationVerifier}
6
- * owns the **one fetch-and-retry** on a stale cache);
7
- * 2. checks `revision == lastRevision + 1`. If not, requests a backfill for the gap
8
- * `[lastRevision + 1, revision]` via the {@link requestBackfill} seam (the `BackfillV1` request shape
9
- * lands in [reactivity-backfill-resume-checkpoints]; this ticket only *detects* the gap and calls the
10
- * hook);
11
- * 3. updates `lastRevision` once revisions are contiguous;
12
- * 4. surfaces the notification to the application layer.
13
- *
14
- * Subscribers dedupe by `(collectionId, revision)`; duplicates from forwarder retries are discarded.
15
- * A fresh subscription (`lastKnownRev == 0`, uninitialized) adopts the first verified notification as its
16
- * baseline rather than demanding a backfill from revision 1.
17
- */
18
-
19
- import type { NotificationV1 } from "./wire.js";
20
- import type { NotificationVerifier } from "./verify.js";
21
-
22
- /** The delivery outcome for one inbound notification. */
23
- export type DeliveryOutcome =
24
- /** Verified and contiguous: surfaced to the application, `lastRevision` advanced. */
25
- | "delivered"
26
- /** `(collectionId, revision)` already delivered: discarded. */
27
- | "duplicate"
28
- /** Verified but ahead of `lastRevision + 1`: a backfill was requested; not yet surfaced. */
29
- | "gap"
30
- /** Signature did not verify (even after the one refetch): dropped. */
31
- | "untrusted"
32
- /** For a different collection than this subscription: ignored. */
33
- | "foreign";
34
-
35
- /** Application sink + backfill seam for a {@link ReactivitySubscriber}. */
36
- export interface ReactivitySubscriberDeps {
37
- /** Collection this subscription tracks, base64url (matches {@link NotificationV1.collectionId}). */
38
- readonly collectionId: string;
39
- /** Verifies inbound notifications (owns the single stale-cache refetch). */
40
- readonly verifier: NotificationVerifier;
41
- /** Surface a verified, contiguous notification to the application. */
42
- readonly deliver: (n: NotificationV1) => void;
43
- /**
44
- * Request a backfill for the inclusive revision gap `[from, to]`. The `BackfillV1` request/response
45
- * lands in [reactivity-backfill-resume-checkpoints]; the returned entries are fed back through
46
- * {@link ReactivitySubscriber.onNotification} to close the gap. Absent ⇒ the gap is recorded only.
47
- */
48
- readonly requestBackfill?: (from: number, to: number) => void;
49
- /** Last revision already held; `0` (default) ⇒ fresh subscribe, adopt the first notification as baseline. */
50
- readonly lastKnownRev?: number;
51
- }
52
-
53
- /** Drives the subscriber-side verify → contiguity → deliver path for one collection. */
54
- export interface ReactivitySubscriber {
55
- /** Last contiguously-delivered revision (`lastKnownRev` until the first delivery). */
56
- readonly lastRevision: number;
57
- /** Process one inbound notification; returns the {@link DeliveryOutcome}. */
58
- onNotification(n: NotificationV1): Promise<DeliveryOutcome>;
59
- /**
60
- * Advance the contiguity head to `revision` because a **verified parent checkpoint** summarized every
61
- * revision up to it (`docs/reactivity.md` §Parent checkpoint summaries). The subscriber then treats
62
- * `revision + 1` as the next contiguous revision, so the checkpoint's `recentEntries` replay without a
63
- * spurious gap/backfill for the digest-covered range. Only advances — never rewinds past delivered
64
- * state. The merged digest itself is applied by the application; this just moves the contiguity head.
65
- */
66
- rebaseline(revision: number): void;
67
- }
68
-
69
- class CollectionSubscriber implements ReactivitySubscriber {
70
- private last: number;
71
- private initialized: boolean;
72
-
73
- constructor(private readonly deps: ReactivitySubscriberDeps) {
74
- const lastKnownRev = deps.lastKnownRev ?? 0;
75
- this.last = lastKnownRev;
76
- // A fresh subscribe (lastKnownRev == 0) has no baseline yet; the first verified notification sets it.
77
- this.initialized = lastKnownRev > 0;
78
- }
79
-
80
- get lastRevision(): number {
81
- return this.last;
82
- }
83
-
84
- rebaseline(revision: number): void {
85
- // A verified checkpoint summarized everything up to `revision`; jump the head forward (never back).
86
- if (!this.initialized || revision > this.last) {
87
- this.last = revision;
88
- this.initialized = true;
89
- }
90
- }
91
-
92
- async onNotification(n: NotificationV1): Promise<DeliveryOutcome> {
93
- if (n.collectionId !== this.deps.collectionId) {
94
- return "foreign";
95
- }
96
- // Verify first (one fetch-and-retry is internal to the verifier). An untrusted notification is
97
- // dropped before any (collectionId, revision) dedupe or lastRevision update.
98
- const verdict = await this.deps.verifier.verify(n);
99
- if (verdict !== "verified") {
100
- return "untrusted";
101
- }
102
-
103
- // Fresh subscription: adopt the first verified notification as the contiguity baseline.
104
- if (!this.initialized) {
105
- this.initialized = true;
106
- this.last = n.revision;
107
- this.deps.deliver(n);
108
- return "delivered";
109
- }
110
-
111
- // Dedupe by (collectionId, revision): anything at or below the contiguous head is already delivered.
112
- if (n.revision <= this.last) {
113
- return "duplicate";
114
- }
115
-
116
- // Contiguity check.
117
- if (n.revision === this.last + 1) {
118
- this.last = n.revision;
119
- this.deps.deliver(n);
120
- return "delivered";
121
- }
122
-
123
- // Gap: request the missing range (inclusive of `revision`); the backfilled entries re-enter here.
124
- this.deps.requestBackfill?.(this.last + 1, n.revision);
125
- return "gap";
126
- }
127
- }
128
-
129
- /** Build a {@link ReactivitySubscriber} for one collection. */
130
- export function createReactivitySubscriber(deps: ReactivitySubscriberDeps): ReactivitySubscriber {
131
- return new CollectionSubscriber(deps);
132
- }
1
+ /**
2
+ * Reactivity — subscriber delivery path (`docs/reactivity.md` §Delivery).
3
+ *
4
+ * A subscriber receiving a notification:
5
+ * 1. **verifies** `sig` against the cached tail-cohort `MembershipCertV1` (the {@link NotificationVerifier}
6
+ * owns the **one fetch-and-retry** on a stale cache);
7
+ * 2. checks `revision == lastRevision + 1`. If not, requests a backfill for the gap
8
+ * `[lastRevision + 1, revision]` via the {@link requestBackfill} seam (the `BackfillV1` request shape
9
+ * lands in [reactivity-backfill-resume-checkpoints]; this ticket only *detects* the gap and calls the
10
+ * hook);
11
+ * 3. updates `lastRevision` once revisions are contiguous;
12
+ * 4. surfaces the notification to the application layer.
13
+ *
14
+ * Subscribers dedupe by `(collectionId, revision)`; duplicates from forwarder retries are discarded.
15
+ * A fresh subscription (`lastKnownRev == 0`, uninitialized) adopts the first verified notification as its
16
+ * baseline rather than demanding a backfill from revision 1.
17
+ */
18
+
19
+ import type { NotificationV1 } from "./wire.js";
20
+ import type { NotificationVerifier } from "./verify.js";
21
+
22
+ /** The delivery outcome for one inbound notification. */
23
+ export type DeliveryOutcome =
24
+ /** Verified and contiguous: surfaced to the application, `lastRevision` advanced. */
25
+ | "delivered"
26
+ /** `(collectionId, revision)` already delivered: discarded. */
27
+ | "duplicate"
28
+ /** Verified but ahead of `lastRevision + 1`: a backfill was requested; not yet surfaced. */
29
+ | "gap"
30
+ /** Signature did not verify (even after the one refetch): dropped. */
31
+ | "untrusted"
32
+ /** For a different collection than this subscription: ignored. */
33
+ | "foreign";
34
+
35
+ /** Application sink + backfill seam for a {@link ReactivitySubscriber}. */
36
+ export interface ReactivitySubscriberDeps {
37
+ /** Collection this subscription tracks, base64url (matches {@link NotificationV1.collectionId}). */
38
+ readonly collectionId: string;
39
+ /** Verifies inbound notifications (owns the single stale-cache refetch). */
40
+ readonly verifier: NotificationVerifier;
41
+ /** Surface a verified, contiguous notification to the application. */
42
+ readonly deliver: (n: NotificationV1) => void;
43
+ /**
44
+ * Request a backfill for the inclusive revision gap `[from, to]`. The `BackfillV1` request/response
45
+ * lands in [reactivity-backfill-resume-checkpoints]; the returned entries are fed back through
46
+ * {@link ReactivitySubscriber.onNotification} to close the gap. Absent ⇒ the gap is recorded only.
47
+ */
48
+ readonly requestBackfill?: (from: number, to: number) => void;
49
+ /** Last revision already held; `0` (default) ⇒ fresh subscribe, adopt the first notification as baseline. */
50
+ readonly lastKnownRev?: number;
51
+ }
52
+
53
+ /** Drives the subscriber-side verify → contiguity → deliver path for one collection. */
54
+ export interface ReactivitySubscriber {
55
+ /** Last contiguously-delivered revision (`lastKnownRev` until the first delivery). */
56
+ readonly lastRevision: number;
57
+ /** Process one inbound notification; returns the {@link DeliveryOutcome}. */
58
+ onNotification(n: NotificationV1): Promise<DeliveryOutcome>;
59
+ /**
60
+ * Advance the contiguity head to `revision` because a **verified parent checkpoint** summarized every
61
+ * revision up to it (`docs/reactivity.md` §Parent checkpoint summaries). The subscriber then treats
62
+ * `revision + 1` as the next contiguous revision, so the checkpoint's `recentEntries` replay without a
63
+ * spurious gap/backfill for the digest-covered range. Only advances — never rewinds past delivered
64
+ * state. The merged digest itself is applied by the application; this just moves the contiguity head.
65
+ */
66
+ rebaseline(revision: number): void;
67
+ }
68
+
69
+ class CollectionSubscriber implements ReactivitySubscriber {
70
+ private last: number;
71
+ private initialized: boolean;
72
+
73
+ constructor(private readonly deps: ReactivitySubscriberDeps) {
74
+ const lastKnownRev = deps.lastKnownRev ?? 0;
75
+ this.last = lastKnownRev;
76
+ // A fresh subscribe (lastKnownRev == 0) has no baseline yet; the first verified notification sets it.
77
+ this.initialized = lastKnownRev > 0;
78
+ }
79
+
80
+ get lastRevision(): number {
81
+ return this.last;
82
+ }
83
+
84
+ rebaseline(revision: number): void {
85
+ // A verified checkpoint summarized everything up to `revision`; jump the head forward (never back).
86
+ if (!this.initialized || revision > this.last) {
87
+ this.last = revision;
88
+ this.initialized = true;
89
+ }
90
+ }
91
+
92
+ async onNotification(n: NotificationV1): Promise<DeliveryOutcome> {
93
+ if (n.collectionId !== this.deps.collectionId) {
94
+ return "foreign";
95
+ }
96
+ // Verify first (one fetch-and-retry is internal to the verifier). An untrusted notification is
97
+ // dropped before any (collectionId, revision) dedupe or lastRevision update.
98
+ const verdict = await this.deps.verifier.verify(n);
99
+ if (verdict !== "verified") {
100
+ return "untrusted";
101
+ }
102
+
103
+ // Fresh subscription: adopt the first verified notification as the contiguity baseline.
104
+ if (!this.initialized) {
105
+ this.initialized = true;
106
+ this.last = n.revision;
107
+ this.deps.deliver(n);
108
+ return "delivered";
109
+ }
110
+
111
+ // Dedupe by (collectionId, revision): anything at or below the contiguous head is already delivered.
112
+ if (n.revision <= this.last) {
113
+ return "duplicate";
114
+ }
115
+
116
+ // Contiguity check.
117
+ if (n.revision === this.last + 1) {
118
+ this.last = n.revision;
119
+ this.deps.deliver(n);
120
+ return "delivered";
121
+ }
122
+
123
+ // Gap: request the missing range (inclusive of `revision`); the backfilled entries re-enter here.
124
+ this.deps.requestBackfill?.(this.last + 1, n.revision);
125
+ return "gap";
126
+ }
127
+ }
128
+
129
+ /** Build a {@link ReactivitySubscriber} for one collection. */
130
+ export function createReactivitySubscriber(deps: ReactivitySubscriberDeps): ReactivitySubscriber {
131
+ return new CollectionSubscriber(deps);
132
+ }
@@ -1,66 +1,66 @@
1
- /**
2
- * Reactivity — subscriber-side subscription state (`docs/reactivity.md` §Subscription).
3
- *
4
- * Subscribing to a collection is an **ordinary cohort-topic registration** at tier T3 with a reactivity
5
- * `appPayload`: `topicId = H(currentTailId(C) ‖ "reactivity")`, the configured TTL (Edge 60 s / Core 90 s),
6
- * and the cohort-topic walk-toward-root / willingness / promotion / TTL-renewal all reused unchanged.
7
- * This module owns only the subscriber-side bookkeeping struct and the `appPayload` builder; the db-p2p
8
- * subscription manager drives the cohort-topic `RegisterV1`.
9
- *
10
- * `tailIdAtAttach` is the subscriber-side detector for tail rotation (the whole-tree migration the
11
- * rotation ticket handles); `cohortEpoch` detects membership drift within the topic.
12
- */
13
-
14
- import { encodeSubscribeAppPayload, type SubscribeAppPayloadV1 } from "./wire.js";
15
-
16
- /** Subscriber-side live state for one active subscription (`docs/reactivity.md` §Subscriber-side state). */
17
- export interface ActiveSubscription {
18
- /** Stable collection identity. */
19
- readonly collectionId: Uint8Array;
20
- /** Current tail-anchored topic id. */
21
- readonly topicId: Uint8Array;
22
- /** Tail block id at registration time — detects tail rotation. */
23
- readonly tailIdAtAttach: Uint8Array;
24
- /** Serving cohort member. */
25
- primary: Uint8Array;
26
- /** Warm-failover cohort members. */
27
- backups: Uint8Array[];
28
- /** Cohort member hint set for fast re-attach. */
29
- cohortHint: Uint8Array[];
30
- /** Cohort epoch for membership-drift detection. */
31
- cohortEpoch: Uint8Array;
32
- /** Last contiguously-delivered revision. */
33
- lastRevision: number;
34
- /** Unix ms of the last delivery. */
35
- lastDeliveredAt: number;
36
- /** Unix ms the subscription attached. */
37
- attachedAt: number;
38
- }
39
-
40
- /** Parameters for building a subscribe `appPayload`. */
41
- export interface SubscribeParams {
42
- /** Collection id, base64url. */
43
- readonly collectionId: string;
44
- /** Tail block id at attach time, base64url. */
45
- readonly tailIdAtAttach: string;
46
- /** Last revision already held; `0` for a fresh subscribe. */
47
- readonly lastKnownRev?: number;
48
- /** Max delta bytes accepted; `0` declines deltas (Edge). */
49
- readonly deltaMaxBytes: number;
50
- }
51
-
52
- /** Build the validated {@link SubscribeAppPayloadV1} for a subscription. */
53
- export function buildSubscribeAppPayload(params: SubscribeParams): SubscribeAppPayloadV1 {
54
- return {
55
- kind: "reactivity",
56
- collectionId: params.collectionId,
57
- tailIdAtAttach: params.tailIdAtAttach,
58
- lastKnownRev: params.lastKnownRev ?? 0,
59
- deltaMaxBytes: params.deltaMaxBytes,
60
- };
61
- }
62
-
63
- /** Build the opaque `RegisterV1.appPayload` bytes for a subscription. */
64
- export function subscribeAppPayloadBytes(params: SubscribeParams): Uint8Array {
65
- return encodeSubscribeAppPayload(buildSubscribeAppPayload(params));
66
- }
1
+ /**
2
+ * Reactivity — subscriber-side subscription state (`docs/reactivity.md` §Subscription).
3
+ *
4
+ * Subscribing to a collection is an **ordinary cohort-topic registration** at tier T3 with a reactivity
5
+ * `appPayload`: `topicId = H(currentTailId(C) ‖ "reactivity")`, the configured TTL (Edge 60 s / Core 90 s),
6
+ * and the cohort-topic walk-toward-root / willingness / promotion / TTL-renewal all reused unchanged.
7
+ * This module owns only the subscriber-side bookkeeping struct and the `appPayload` builder; the db-p2p
8
+ * subscription manager drives the cohort-topic `RegisterV1`.
9
+ *
10
+ * `tailIdAtAttach` is the subscriber-side detector for tail rotation (the whole-tree migration the
11
+ * rotation ticket handles); `cohortEpoch` detects membership drift within the topic.
12
+ */
13
+
14
+ import { encodeSubscribeAppPayload, type SubscribeAppPayloadV1 } from "./wire.js";
15
+
16
+ /** Subscriber-side live state for one active subscription (`docs/reactivity.md` §Subscriber-side state). */
17
+ export interface ActiveSubscription {
18
+ /** Stable collection identity. */
19
+ readonly collectionId: Uint8Array;
20
+ /** Current tail-anchored topic id. */
21
+ readonly topicId: Uint8Array;
22
+ /** Tail block id at registration time — detects tail rotation. */
23
+ readonly tailIdAtAttach: Uint8Array;
24
+ /** Serving cohort member. */
25
+ primary: Uint8Array;
26
+ /** Warm-failover cohort members. */
27
+ backups: Uint8Array[];
28
+ /** Cohort member hint set for fast re-attach. */
29
+ cohortHint: Uint8Array[];
30
+ /** Cohort epoch for membership-drift detection. */
31
+ cohortEpoch: Uint8Array;
32
+ /** Last contiguously-delivered revision. */
33
+ lastRevision: number;
34
+ /** Unix ms of the last delivery. */
35
+ lastDeliveredAt: number;
36
+ /** Unix ms the subscription attached. */
37
+ attachedAt: number;
38
+ }
39
+
40
+ /** Parameters for building a subscribe `appPayload`. */
41
+ export interface SubscribeParams {
42
+ /** Collection id, base64url. */
43
+ readonly collectionId: string;
44
+ /** Tail block id at attach time, base64url. */
45
+ readonly tailIdAtAttach: string;
46
+ /** Last revision already held; `0` for a fresh subscribe. */
47
+ readonly lastKnownRev?: number;
48
+ /** Max delta bytes accepted; `0` declines deltas (Edge). */
49
+ readonly deltaMaxBytes: number;
50
+ }
51
+
52
+ /** Build the validated {@link SubscribeAppPayloadV1} for a subscription. */
53
+ export function buildSubscribeAppPayload(params: SubscribeParams): SubscribeAppPayloadV1 {
54
+ return {
55
+ kind: "reactivity",
56
+ collectionId: params.collectionId,
57
+ tailIdAtAttach: params.tailIdAtAttach,
58
+ lastKnownRev: params.lastKnownRev ?? 0,
59
+ deltaMaxBytes: params.deltaMaxBytes,
60
+ };
61
+ }
62
+
63
+ /** Build the opaque `RegisterV1.appPayload` bytes for a subscription. */
64
+ export function subscribeAppPayloadBytes(params: SubscribeParams): Uint8Array {
65
+ return encodeSubscribeAppPayload(buildSubscribeAppPayload(params));
66
+ }
@@ -1,71 +1,71 @@
1
- /**
2
- * Reactivity — rotating, tail-anchored topic.
3
- *
4
- * Transcribed from `docs/reactivity.md` §Anchor:
5
- *
6
- * ```
7
- * topicId(collection C, tail T) = H(T.blockId ‖ "reactivity")
8
- * ```
9
- *
10
- * Unlike matchmaking (whose anchor is a stable `(kind, label)`), the reactivity anchor **rotates** with
11
- * the collection's tail block: when the tail fills and a new tail block is born, `topicId` changes and
12
- * the cohort-topic layer treats it as an entirely new topic. The whole-tree migration that follows is
13
- * the rotation ticket's concern; this module owns only the pure derivation.
14
- *
15
- * The resulting `topicId` is fed verbatim into cohort-topic tier addressing (`coord_d(self, topicId)`),
16
- * so it is derived with the **same** {@link IRingHash} primitive cohort-topic uses for `coord_d` input —
17
- * db-core's own SHA-256 truncated to the ring width, **not** a FRET import. The trailing `"reactivity"`
18
- * literal domain-separates the reactivity tree from any other application anchoring on the same tail id.
19
- *
20
- * Concatenation is delimiter-free, exactly as the spec writes it; the constant suffix is unambiguous
21
- * because no tail-id byte string can alias a `(tailId, "reactivity")` pair of a different length.
22
- *
23
- * `reactivityTopicId` is the small pure helper the ticket asks for once and the rotation ticket
24
- * ([reactivity-rotation-backpressure-policy]) reuses per-emission — define it here, not at the call site.
25
- */
26
-
27
- import { createRingHash } from "../cohort-topic/ring-hash.js";
28
- import type { IRingHash } from "../cohort-topic/ports.js";
29
-
30
- /** Domain-separation suffix mixed into every reactivity anchor. */
31
- const REACTIVITY_SUFFIX = "reactivity";
32
-
33
- const utf8 = new TextEncoder();
34
-
35
- /**
36
- * `H(tailId ‖ "reactivity")` over the injected ring hash — the cohort-topic `topicId` for the
37
- * reactivity tree anchored on `tailId` (the collection's current tail block id, as raw bytes).
38
- *
39
- * The shared helper consumed by subscriber attachment, origination, and (per-emission) the rotation
40
- * ticket. `hash` defaults to db-core's own 256-bit SHA-256, byte-identical to the cohort-topic host's.
41
- */
42
- export function reactivityTopicId(tailId: Uint8Array, hash: IRingHash = createRingHash()): Uint8Array {
43
- const suffixBytes = utf8.encode(REACTIVITY_SUFFIX);
44
- const input = new Uint8Array(tailId.length + suffixBytes.length);
45
- input.set(tailId, 0);
46
- input.set(suffixBytes, tailId.length);
47
- return hash.H(input);
48
- }
49
-
50
- /** Derives the rotating `topicId` for a collection's current tail. */
51
- export interface ReactivityTopicAnchor {
52
- /** `H(tailId ‖ "reactivity")` for the given tail block id (bytes). */
53
- topicId(tailId: Uint8Array): Uint8Array;
54
- }
55
-
56
- class HashReactivityTopicAnchor implements ReactivityTopicAnchor {
57
- constructor(private readonly hash: IRingHash) {}
58
-
59
- topicId(tailId: Uint8Array): Uint8Array {
60
- return reactivityTopicId(tailId, this.hash);
61
- }
62
- }
63
-
64
- /**
65
- * Build a {@link ReactivityTopicAnchor} over the injected hash. db-p2p passes the same {@link IRingHash}
66
- * it binds to FRET's `RING_BITS` so the anchor and cohort-topic routing keys line up; the default
67
- * constructs db-core's own {@link createRingHash} (256-bit SHA-256).
68
- */
69
- export function createReactivityTopicAnchor(hash: IRingHash = createRingHash()): ReactivityTopicAnchor {
70
- return new HashReactivityTopicAnchor(hash);
71
- }
1
+ /**
2
+ * Reactivity — rotating, tail-anchored topic.
3
+ *
4
+ * Transcribed from `docs/reactivity.md` §Anchor:
5
+ *
6
+ * ```
7
+ * topicId(collection C, tail T) = H(T.blockId ‖ "reactivity")
8
+ * ```
9
+ *
10
+ * Unlike matchmaking (whose anchor is a stable `(kind, label)`), the reactivity anchor **rotates** with
11
+ * the collection's tail block: when the tail fills and a new tail block is born, `topicId` changes and
12
+ * the cohort-topic layer treats it as an entirely new topic. The whole-tree migration that follows is
13
+ * the rotation ticket's concern; this module owns only the pure derivation.
14
+ *
15
+ * The resulting `topicId` is fed verbatim into cohort-topic tier addressing (`coord_d(self, topicId)`),
16
+ * so it is derived with the **same** {@link IRingHash} primitive cohort-topic uses for `coord_d` input —
17
+ * db-core's own SHA-256 truncated to the ring width, **not** a FRET import. The trailing `"reactivity"`
18
+ * literal domain-separates the reactivity tree from any other application anchoring on the same tail id.
19
+ *
20
+ * Concatenation is delimiter-free, exactly as the spec writes it; the constant suffix is unambiguous
21
+ * because no tail-id byte string can alias a `(tailId, "reactivity")` pair of a different length.
22
+ *
23
+ * `reactivityTopicId` is the small pure helper the ticket asks for once and the rotation ticket
24
+ * ([reactivity-rotation-backpressure-policy]) reuses per-emission — define it here, not at the call site.
25
+ */
26
+
27
+ import { createRingHash } from "../cohort-topic/ring-hash.js";
28
+ import type { IRingHash } from "../cohort-topic/ports.js";
29
+
30
+ /** Domain-separation suffix mixed into every reactivity anchor. */
31
+ const REACTIVITY_SUFFIX = "reactivity";
32
+
33
+ const utf8 = new TextEncoder();
34
+
35
+ /**
36
+ * `H(tailId ‖ "reactivity")` over the injected ring hash — the cohort-topic `topicId` for the
37
+ * reactivity tree anchored on `tailId` (the collection's current tail block id, as raw bytes).
38
+ *
39
+ * The shared helper consumed by subscriber attachment, origination, and (per-emission) the rotation
40
+ * ticket. `hash` defaults to db-core's own 256-bit SHA-256, byte-identical to the cohort-topic host's.
41
+ */
42
+ export function reactivityTopicId(tailId: Uint8Array, hash: IRingHash = createRingHash()): Uint8Array {
43
+ const suffixBytes = utf8.encode(REACTIVITY_SUFFIX);
44
+ const input = new Uint8Array(tailId.length + suffixBytes.length);
45
+ input.set(tailId, 0);
46
+ input.set(suffixBytes, tailId.length);
47
+ return hash.H(input);
48
+ }
49
+
50
+ /** Derives the rotating `topicId` for a collection's current tail. */
51
+ export interface ReactivityTopicAnchor {
52
+ /** `H(tailId ‖ "reactivity")` for the given tail block id (bytes). */
53
+ topicId(tailId: Uint8Array): Uint8Array;
54
+ }
55
+
56
+ class HashReactivityTopicAnchor implements ReactivityTopicAnchor {
57
+ constructor(private readonly hash: IRingHash) {}
58
+
59
+ topicId(tailId: Uint8Array): Uint8Array {
60
+ return reactivityTopicId(tailId, this.hash);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Build a {@link ReactivityTopicAnchor} over the injected hash. db-p2p passes the same {@link IRingHash}
66
+ * it binds to FRET's `RING_BITS` so the anchor and cohort-topic routing keys line up; the default
67
+ * constructs db-core's own {@link createRingHash} (256-bit SHA-256).
68
+ */
69
+ export function createReactivityTopicAnchor(hash: IRingHash = createRingHash()): ReactivityTopicAnchor {
70
+ return new HashReactivityTopicAnchor(hash);
71
+ }