@optimystic/db-core 0.22.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.
- package/README.md +336 -336
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +17 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +24 -2
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +121 -8
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +48 -13
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/package.json +1 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +25 -2
- package/src/collections/diary/diary.ts +68 -68
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/tree.ts +320 -312
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -502
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +49 -14
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/helpers.ts +159 -159
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- 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
|
+
}
|