@kairos-es/core 1.0.0-alpha.5 → 1.0.0-alpha.6
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/dist/cjs/SubscribeMachine.js +149 -56
- package/dist/cjs/SubscribeMachine.js.map +1 -1
- package/dist/dts-cjs/SubscribeMachine.d.ts +138 -24
- package/dist/dts-cjs/SubscribeMachine.d.ts.map +1 -1
- package/dist/dts-esm/SubscribeMachine.d.ts +138 -24
- package/dist/dts-esm/SubscribeMachine.d.ts.map +1 -1
- package/dist/esm/SubscribeMachine.js +147 -55
- package/dist/esm/SubscribeMachine.js.map +1 -1
- package/package.json +1 -1
- package/src/SubscribeMachine.ts +255 -80
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* the cursor's exclusive lower bound) and the poll loop that together turn a paged
|
|
6
6
|
* read into a live subscription.
|
|
7
7
|
*
|
|
8
|
+
* The algorithm is `subscribeItems`, generic over the item `A` it delivers and the
|
|
9
|
+
* cursor `C` it resumes from, because nothing in it is about an event: any source
|
|
10
|
+
* that can page-read strictly after a cursor and name the cursor of what it
|
|
11
|
+
* returned is subscribable by the same loop. `subscribeStream` is the event-store
|
|
12
|
+
* shape — `SequencedEvent` ordered by `Position` — over the same loop, and it is
|
|
13
|
+
* what every engine calls, unchanged by the generalisation. A second kind of
|
|
14
|
+
* source reuses the loop rather than growing a second one, for the reason the next
|
|
15
|
+
* paragraph gives.
|
|
16
|
+
*
|
|
8
17
|
* It lives in core because delivery semantics are contract semantics. An engine
|
|
9
18
|
* supplies TRANSPORT through two STUBBABLE seams — `fetch`, a paged read strictly
|
|
10
19
|
* after a cursor, and `listen`, an optional wake source — and inherits the delivery
|
|
@@ -15,48 +24,52 @@
|
|
|
15
24
|
* Both seams take plain in-memory values in a test, so the wake/poll/de-dup logic
|
|
16
25
|
* is exercisable with no live backend at all.
|
|
17
26
|
*
|
|
18
|
-
* All THREE are
|
|
19
|
-
* is exactly ONE delivery algorithm in this repo, and it is the code below. The
|
|
27
|
+
* All THREE engines are callers, and the count matters to a reader here: it means
|
|
28
|
+
* there is exactly ONE delivery algorithm in this repo, and it is the code below. The
|
|
20
29
|
* in-memory store is a caller like the rest — it hands in a `fetch` over its log
|
|
21
30
|
* `Ref` and its append `PubSub` as a wake source and keeps no delivery logic of its
|
|
22
31
|
* own — which is what makes the shared contract suite's subscribe cases a comparison
|
|
23
32
|
* of two BACKENDS rather than of two delivery algorithms.
|
|
24
33
|
*
|
|
25
|
-
* Correctness rests on the periodic
|
|
34
|
+
* Correctness rests on the periodic cursor-based reconcile — a re-read from the
|
|
26
35
|
* cursor every poll interval — and NEVER on the wake source, which only lowers
|
|
27
36
|
* delivery latency below that interval. A subscription with no wake source at all
|
|
28
37
|
* is therefore complete and correct, merely slower, which is why `listen` is
|
|
29
38
|
* optional in the TYPE rather than by the convention of passing `Stream.never`:
|
|
30
39
|
* poll-only delivery is a supported shape, not a degraded one.
|
|
31
40
|
*
|
|
32
|
-
* ## The precondition every
|
|
41
|
+
* ## The precondition every caller inherits: ASCENDING COMMIT ORDER
|
|
33
42
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
43
|
+
* It binds EVERY caller, over whatever cursor it chose — an engine's `Position`, or
|
|
44
|
+
* any other `C` handed to `subscribeItems` — and the example below is written in
|
|
45
|
+
* positions only because the engines are the callers it was first written for. A
|
|
46
|
+
* cursor that is committed and VISIBLE must never be below one already delivered.
|
|
47
|
+
* Gaps are fine: the cursor is an ascending exclusive lower bound, so a number
|
|
48
|
+
* that is allocated and never committed, or never allocated at all, is simply a
|
|
49
|
+
* number the machine never sees. Out-of-order commits are not fine. If one
|
|
38
50
|
* writer allocates position 5 and another allocates 6, and 6 commits first, a poll
|
|
39
51
|
* landing between the two commits delivers 6 and advances the cursor past it — and
|
|
40
52
|
* 5 is then lost forever, because no later read looks below the cursor and nothing
|
|
41
|
-
* anywhere notices the hole.
|
|
53
|
+
* anywhere notices the hole. A caller whose concurrent writers can commit out of
|
|
42
54
|
* order MUST serialise them, or this machine will silently skip. An engine that
|
|
43
55
|
* takes an exclusive lock on both of its append paths discharges the obligation
|
|
44
56
|
* that way; an engine whose append is a single statement discharges it
|
|
45
57
|
* structurally.
|
|
46
58
|
*
|
|
47
59
|
* The obligation is written down HERE, in the algorithm that depends on it, rather
|
|
48
|
-
* than in whichever
|
|
49
|
-
* loop and of nothing else, so an
|
|
60
|
+
* than in whichever caller happens to satisfy it. It is a property of this delivery
|
|
61
|
+
* loop and of nothing else, so an author reading only their own source has
|
|
50
62
|
* no way to discover that anything downstream needs it — and the failure mode is
|
|
51
63
|
* the worst kind there is: no error, no defect, no failing test, just an event that
|
|
52
64
|
* a subscriber never receives. A single engine owning both its append path and its
|
|
53
65
|
* own wake loop can leave the assumption implicit; a shared machine cannot.
|
|
54
66
|
*
|
|
55
|
-
* ## `catchUpPageSize` bounds the READ, not the emission
|
|
67
|
+
* ## `catchUpPageSize` bounds the READ, not the emission — unless `emit: 'page'`
|
|
56
68
|
*
|
|
57
|
-
*
|
|
58
|
-
* `
|
|
59
|
-
*
|
|
69
|
+
* Under the default `emit: 'backlog'`, which is what `subscribeStream` and so every
|
|
70
|
+
* engine uses, `drainFrom` below PAGES the catch-up read — `catchUpPageSize` bounds
|
|
71
|
+
* each `fetch` — but it COLLECTS every page into one array, which is emitted as a
|
|
72
|
+
* SINGLE chunk. So peak memory at subscription start is O(backlog) rather than O(page): a
|
|
60
73
|
* subscriber joining behind a large log materialises the whole backlog before its
|
|
61
74
|
* consumer sees the first element.
|
|
62
75
|
*
|
|
@@ -67,6 +80,19 @@
|
|
|
67
80
|
* stay small even against a one-chunk catch-up. The cost therefore lands on backlog
|
|
68
81
|
* memory alone, and it lands hardest on the durable-log-with-in-memory-view mode,
|
|
69
82
|
* where an ephemeral view re-catches-up the entire log on every boot.
|
|
83
|
+
*
|
|
84
|
+
* `emit: 'page'` changes exactly that and nothing else. Each fetched page is
|
|
85
|
+
* emitted as its own chunk and the cursor advances past it before the next read, so
|
|
86
|
+
* peak memory is O(page) and the consumer sees the first item after ONE read rather
|
|
87
|
+
* than after the whole backlog. Delivery is unchanged: the same items in the same
|
|
88
|
+
* order, the same de-dup, the same wake and poll. What a caller gives up is the
|
|
89
|
+
* one-chunk catch-up, so it must not read meaning into chunk boundaries under
|
|
90
|
+
* either mode. It is for a consumer that does NOT re-chunk downstream and whose
|
|
91
|
+
* catch-up can be the whole of a large source — a full resync of a table, say —
|
|
92
|
+
* where holding the backlog would be the cost the paging was meant to avoid. The
|
|
93
|
+
* engines keep `'backlog'` because the read side's re-chunking already bounds what
|
|
94
|
+
* they commit, and changing their emission would move a behaviour nobody asked to
|
|
95
|
+
* move.
|
|
70
96
|
*/
|
|
71
97
|
import { Effect, Schema, type Scope, Stream } from 'effect';
|
|
72
98
|
import type { Position, SequencedEvent } from './Event.js';
|
|
@@ -130,9 +156,74 @@ export declare const subscriptionTuningFields: {
|
|
|
130
156
|
readonly catchUpPageSize: Schema.optionalKey<Schema.Int>;
|
|
131
157
|
};
|
|
132
158
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
159
|
+
* How a subscription hands its catch-up to the consumer: the whole backlog as ONE
|
|
160
|
+
* chunk (`'backlog'`), or each fetched page as a chunk of its own (`'page'`). The
|
|
161
|
+
* module doc's "`catchUpPageSize` bounds the READ" section owns what each costs.
|
|
162
|
+
*
|
|
163
|
+
* A closed union of two words rather than a boolean, so a call site says WHICH
|
|
164
|
+
* behaviour it chose instead of what it turned off, and a third shape, should one
|
|
165
|
+
* ever be argued for, is added beside these rather than encoded in a second flag.
|
|
166
|
+
*/
|
|
167
|
+
export type SubscriptionEmission = 'backlog' | 'page';
|
|
168
|
+
/**
|
|
169
|
+
* The machine's inputs over ANY item `A` with an ascending cursor `C`: the two seams,
|
|
170
|
+
* where the cursor comes from and starts, and the tuning.
|
|
171
|
+
*
|
|
172
|
+
* Generic in the item and the cursor because nothing in the algorithm is about an
|
|
173
|
+
* EVENT. The wake queue, the poll, the paged reconcile and the exclusive-lower-bound
|
|
174
|
+
* de-dup need only three things of what they deliver: a page-read strictly after a
|
|
175
|
+
* cursor, a way to read the cursor OFF a delivered item, and a starting cursor. A
|
|
176
|
+
* log of `SequencedEvent`s ordered by `Position` is one such source; a feed of rows
|
|
177
|
+
* ordered by a feed-local sequence is another, and a second copy of this loop for it
|
|
178
|
+
* would be exactly the divergence the module doc argues against.
|
|
179
|
+
*
|
|
180
|
+
* The machine never COMPARES two cursors. `fetch` is the only thing that interprets
|
|
181
|
+
* one — its exclusive lower bound is what does the de-dup — so `C` needs no ordering
|
|
182
|
+
* instance here, and the ascending-commit-order obligation in the module doc is the
|
|
183
|
+
* caller's, stated over whatever `C` it chose.
|
|
184
|
+
*
|
|
185
|
+
* `initial` is REQUIRED, unlike `subscribeStream`'s `after`. Only a cursor type the
|
|
186
|
+
* machine knows can have a default — `ORIGIN` for `Position` — and for any other the
|
|
187
|
+
* caller is the only party that knows what "before everything" is.
|
|
188
|
+
*
|
|
189
|
+
* `pollInterval` and `catchUpPageSize` are optional here and fall back to
|
|
190
|
+
* `DEFAULT_POLL_INTERVAL_MILLIS` and `DEFAULT_CATCH_UP_PAGE_SIZE`: a caller with no
|
|
191
|
+
* config of its own takes the same figures every engine does, from the same place.
|
|
192
|
+
* The event-store shape, `SubscriptionDeps`, keeps them required, because every
|
|
193
|
+
* engine already resolves them from its config and that type is unchanged.
|
|
194
|
+
*/
|
|
195
|
+
export interface ItemSubscriptionDeps<A, C, E> {
|
|
196
|
+
/**
|
|
197
|
+
* Page-read the items strictly after `after`, up to `limit`, ascending by cursor.
|
|
198
|
+
* Returning FEWER than `limit` signals the tail is drained. The exclusive lower
|
|
199
|
+
* bound IS the de-dup: a re-read from the cursor never returns what was delivered.
|
|
200
|
+
*/
|
|
201
|
+
readonly fetch: (after: C, limit: ReadLimit) => Effect.Effect<ReadonlyArray<A>, E>;
|
|
202
|
+
/** The cursor of a delivered item: the next `fetch` starts strictly after it. */
|
|
203
|
+
readonly cursorOf: (item: A) => C;
|
|
204
|
+
/** The cursor to start from, exclusive: the first `fetch` reads strictly after it. */
|
|
205
|
+
readonly initial: C;
|
|
206
|
+
/** The raw wake source — see `SubscriptionDeps.listen`, whose rules it shares. */
|
|
207
|
+
readonly listen?: Stream.Stream<unknown, E>;
|
|
208
|
+
/**
|
|
209
|
+
* The poll interval in milliseconds (default `DEFAULT_POLL_INTERVAL_MILLIS`):
|
|
210
|
+
* finite and greater than 0, or `subscribeItems` dies.
|
|
211
|
+
*/
|
|
212
|
+
readonly pollInterval?: number;
|
|
213
|
+
/**
|
|
214
|
+
* The catch-up page size (default `DEFAULT_CATCH_UP_PAGE_SIZE`): an integer
|
|
215
|
+
* greater than 0, or `subscribeItems` dies.
|
|
216
|
+
*/
|
|
217
|
+
readonly catchUpPageSize?: number;
|
|
218
|
+
/** The catch-up's chunking (default `'backlog'`); see `SubscriptionEmission`. */
|
|
219
|
+
readonly emit?: SubscriptionEmission;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The two seams the event-store subscribe machine runs over, plus the two tuning
|
|
223
|
+
* knobs: `ItemSubscriptionDeps` fixed to `SequencedEvent` and `Position`, the shape
|
|
224
|
+
* every engine's `subscribe` already hands to `subscribeStream`. `E` is the engine's
|
|
225
|
+
* own infrastructure error — whatever its reads fail with — and it stays on the
|
|
226
|
+
* channel through to `makeContractStore`, which dies it.
|
|
136
227
|
*
|
|
137
228
|
* ONE type parameter and not two, deliberately: a wake source with an error type of
|
|
138
229
|
* its own is `Stream.orDie`d by the caller, yielding a `Stream<unknown, never>`
|
|
@@ -164,12 +255,35 @@ export interface SubscriptionDeps<E> {
|
|
|
164
255
|
readonly catchUpPageSize: number;
|
|
165
256
|
}
|
|
166
257
|
/**
|
|
167
|
-
* Build
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
258
|
+
* Build a subscription `Stream` over any ascending-cursor source: catch up from
|
|
259
|
+
* `initial`, then track the live tail. The cursor threads through the
|
|
260
|
+
* `Stream.paginate` state, starting at `initial` and advancing to the cursor of the
|
|
261
|
+
* last delivered item each batch; the stream never completes on its own (the next
|
|
262
|
+
* state is always `Option.some`), so the enclosing scope interrupts it on close. An
|
|
263
|
+
* `E` mid-stream stays on the channel, for the caller to die or to supervise.
|
|
264
|
+
*
|
|
265
|
+
* This is THE delivery algorithm, and `subscribeStream` runs the same machine. Under
|
|
266
|
+
* `emit: 'backlog'` a batch is everything a drain to the tail returned; under
|
|
267
|
+
* `'page'` it is one fetched page, so the paginate step and the page coincide and a
|
|
268
|
+
* full page is followed at once by the next read rather than by a wait.
|
|
269
|
+
*
|
|
270
|
+
* A `pollInterval` or `catchUpPageSize` the config fields would refuse is a DEFECT
|
|
271
|
+
* here, raised by the stream before it reads or forks anything, with a message
|
|
272
|
+
* naming the field. This caller may have no config whose decode would have caught
|
|
273
|
+
* it, and unchecked the two failures are silent or late: a page size of 0 gives a
|
|
274
|
+
* stream that never emits and never fails, and 1.5 dies only at the first read,
|
|
275
|
+
* inside `ReadLimit`, in words that name neither knob.
|
|
276
|
+
*/
|
|
277
|
+
export declare const subscribeItems: <A, C, E>(deps: ItemSubscriptionDeps<A, C, E>) => Stream.Stream<A, E, Scope.Scope>;
|
|
278
|
+
/**
|
|
279
|
+
* Build the event subscription `Stream`: catch up from `after`, then track the live
|
|
280
|
+
* tail. A THIN wrapper over the machine `subscribeItems` runs, fixing the item to
|
|
281
|
+
* `SequencedEvent`, the cursor to its `position`, the start to `ORIGIN` when `after`
|
|
282
|
+
* is absent, and the emission to the default `'backlog'` — so every engine's call
|
|
283
|
+
* site, and every figure it delivers, is what it was before the machine became
|
|
284
|
+
* generic. It skips `subscribeItems`' tuning check, for the reason on `machine`. An `E`
|
|
285
|
+
* mid-stream stays on the channel; `makeContractStore` dies it (subscribe is
|
|
286
|
+
* `E = never` on the contract).
|
|
173
287
|
*/
|
|
174
288
|
export declare const subscribeStream: <E>(deps: SubscriptionDeps<E>, after: Position | undefined) => Stream.Stream<SequencedEvent, E, Scope.Scope>;
|
|
175
289
|
//# sourceMappingURL=SubscribeMachine.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscribeMachine.d.ts","sourceRoot":"","sources":["../../src/SubscribeMachine.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SubscribeMachine.d.ts","sourceRoot":"","sources":["../../src/SubscribeMachine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+FG;AACH,OAAO,EACL,MAAM,EAIN,MAAM,EACN,KAAK,KAAK,EACV,MAAM,EACP,MAAM,QAAQ,CAAA;AACf,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAU,SAAS,EAAE,MAAM,SAAS,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAA;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAA;AAWhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,wBAAwB;;;CAG3B,CAAA;AA2CV;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,CAAA;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,SAAS,KACb,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACvC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAA;IACjC,sFAAsF;IACtF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IACnB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3C;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,oBAAoB,CAAA;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,SAAS,KACb,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAA;IACpD;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3C,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EACpC,MAAM,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAClC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAGjC,CAAA;AA4HD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAC/B,MAAM,gBAAgB,CAAC,CAAC,CAAC,EACzB,OAAO,QAAQ,GAAG,SAAS,KAC1B,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAQ3C,CAAA"}
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* the cursor's exclusive lower bound) and the poll loop that together turn a paged
|
|
6
6
|
* read into a live subscription.
|
|
7
7
|
*
|
|
8
|
+
* The algorithm is `subscribeItems`, generic over the item `A` it delivers and the
|
|
9
|
+
* cursor `C` it resumes from, because nothing in it is about an event: any source
|
|
10
|
+
* that can page-read strictly after a cursor and name the cursor of what it
|
|
11
|
+
* returned is subscribable by the same loop. `subscribeStream` is the event-store
|
|
12
|
+
* shape — `SequencedEvent` ordered by `Position` — over the same loop, and it is
|
|
13
|
+
* what every engine calls, unchanged by the generalisation. A second kind of
|
|
14
|
+
* source reuses the loop rather than growing a second one, for the reason the next
|
|
15
|
+
* paragraph gives.
|
|
16
|
+
*
|
|
8
17
|
* It lives in core because delivery semantics are contract semantics. An engine
|
|
9
18
|
* supplies TRANSPORT through two STUBBABLE seams — `fetch`, a paged read strictly
|
|
10
19
|
* after a cursor, and `listen`, an optional wake source — and inherits the delivery
|
|
@@ -15,48 +24,52 @@
|
|
|
15
24
|
* Both seams take plain in-memory values in a test, so the wake/poll/de-dup logic
|
|
16
25
|
* is exercisable with no live backend at all.
|
|
17
26
|
*
|
|
18
|
-
* All THREE are
|
|
19
|
-
* is exactly ONE delivery algorithm in this repo, and it is the code below. The
|
|
27
|
+
* All THREE engines are callers, and the count matters to a reader here: it means
|
|
28
|
+
* there is exactly ONE delivery algorithm in this repo, and it is the code below. The
|
|
20
29
|
* in-memory store is a caller like the rest — it hands in a `fetch` over its log
|
|
21
30
|
* `Ref` and its append `PubSub` as a wake source and keeps no delivery logic of its
|
|
22
31
|
* own — which is what makes the shared contract suite's subscribe cases a comparison
|
|
23
32
|
* of two BACKENDS rather than of two delivery algorithms.
|
|
24
33
|
*
|
|
25
|
-
* Correctness rests on the periodic
|
|
34
|
+
* Correctness rests on the periodic cursor-based reconcile — a re-read from the
|
|
26
35
|
* cursor every poll interval — and NEVER on the wake source, which only lowers
|
|
27
36
|
* delivery latency below that interval. A subscription with no wake source at all
|
|
28
37
|
* is therefore complete and correct, merely slower, which is why `listen` is
|
|
29
38
|
* optional in the TYPE rather than by the convention of passing `Stream.never`:
|
|
30
39
|
* poll-only delivery is a supported shape, not a degraded one.
|
|
31
40
|
*
|
|
32
|
-
* ## The precondition every
|
|
41
|
+
* ## The precondition every caller inherits: ASCENDING COMMIT ORDER
|
|
33
42
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
43
|
+
* It binds EVERY caller, over whatever cursor it chose — an engine's `Position`, or
|
|
44
|
+
* any other `C` handed to `subscribeItems` — and the example below is written in
|
|
45
|
+
* positions only because the engines are the callers it was first written for. A
|
|
46
|
+
* cursor that is committed and VISIBLE must never be below one already delivered.
|
|
47
|
+
* Gaps are fine: the cursor is an ascending exclusive lower bound, so a number
|
|
48
|
+
* that is allocated and never committed, or never allocated at all, is simply a
|
|
49
|
+
* number the machine never sees. Out-of-order commits are not fine. If one
|
|
38
50
|
* writer allocates position 5 and another allocates 6, and 6 commits first, a poll
|
|
39
51
|
* landing between the two commits delivers 6 and advances the cursor past it — and
|
|
40
52
|
* 5 is then lost forever, because no later read looks below the cursor and nothing
|
|
41
|
-
* anywhere notices the hole.
|
|
53
|
+
* anywhere notices the hole. A caller whose concurrent writers can commit out of
|
|
42
54
|
* order MUST serialise them, or this machine will silently skip. An engine that
|
|
43
55
|
* takes an exclusive lock on both of its append paths discharges the obligation
|
|
44
56
|
* that way; an engine whose append is a single statement discharges it
|
|
45
57
|
* structurally.
|
|
46
58
|
*
|
|
47
59
|
* The obligation is written down HERE, in the algorithm that depends on it, rather
|
|
48
|
-
* than in whichever
|
|
49
|
-
* loop and of nothing else, so an
|
|
60
|
+
* than in whichever caller happens to satisfy it. It is a property of this delivery
|
|
61
|
+
* loop and of nothing else, so an author reading only their own source has
|
|
50
62
|
* no way to discover that anything downstream needs it — and the failure mode is
|
|
51
63
|
* the worst kind there is: no error, no defect, no failing test, just an event that
|
|
52
64
|
* a subscriber never receives. A single engine owning both its append path and its
|
|
53
65
|
* own wake loop can leave the assumption implicit; a shared machine cannot.
|
|
54
66
|
*
|
|
55
|
-
* ## `catchUpPageSize` bounds the READ, not the emission
|
|
67
|
+
* ## `catchUpPageSize` bounds the READ, not the emission — unless `emit: 'page'`
|
|
56
68
|
*
|
|
57
|
-
*
|
|
58
|
-
* `
|
|
59
|
-
*
|
|
69
|
+
* Under the default `emit: 'backlog'`, which is what `subscribeStream` and so every
|
|
70
|
+
* engine uses, `drainFrom` below PAGES the catch-up read — `catchUpPageSize` bounds
|
|
71
|
+
* each `fetch` — but it COLLECTS every page into one array, which is emitted as a
|
|
72
|
+
* SINGLE chunk. So peak memory at subscription start is O(backlog) rather than O(page): a
|
|
60
73
|
* subscriber joining behind a large log materialises the whole backlog before its
|
|
61
74
|
* consumer sees the first element.
|
|
62
75
|
*
|
|
@@ -67,6 +80,19 @@
|
|
|
67
80
|
* stay small even against a one-chunk catch-up. The cost therefore lands on backlog
|
|
68
81
|
* memory alone, and it lands hardest on the durable-log-with-in-memory-view mode,
|
|
69
82
|
* where an ephemeral view re-catches-up the entire log on every boot.
|
|
83
|
+
*
|
|
84
|
+
* `emit: 'page'` changes exactly that and nothing else. Each fetched page is
|
|
85
|
+
* emitted as its own chunk and the cursor advances past it before the next read, so
|
|
86
|
+
* peak memory is O(page) and the consumer sees the first item after ONE read rather
|
|
87
|
+
* than after the whole backlog. Delivery is unchanged: the same items in the same
|
|
88
|
+
* order, the same de-dup, the same wake and poll. What a caller gives up is the
|
|
89
|
+
* one-chunk catch-up, so it must not read meaning into chunk boundaries under
|
|
90
|
+
* either mode. It is for a consumer that does NOT re-chunk downstream and whose
|
|
91
|
+
* catch-up can be the whole of a large source — a full resync of a table, say —
|
|
92
|
+
* where holding the backlog would be the cost the paging was meant to avoid. The
|
|
93
|
+
* engines keep `'backlog'` because the read side's re-chunking already bounds what
|
|
94
|
+
* they commit, and changing their emission would move a behaviour nobody asked to
|
|
95
|
+
* move.
|
|
70
96
|
*/
|
|
71
97
|
import { Effect, Option, Queue, Schedule, Schema, Stream } from 'effect';
|
|
72
98
|
import { ORIGIN, ReadLimit } from "./Event.js";
|
|
@@ -95,6 +121,10 @@ export const DEFAULT_CATCH_UP_PAGE_SIZE = 512;
|
|
|
95
121
|
* one-second fallback poll (`conn.wait(..., interval=1)`).
|
|
96
122
|
*/
|
|
97
123
|
export const DEFAULT_POLL_INTERVAL_MILLIS = 1000;
|
|
124
|
+
/** A usable `pollInterval`: see `subscriptionTuningFields` for the bounds' argument. */
|
|
125
|
+
const PollIntervalMillis = /*#__PURE__*/Schema.Number.check(/*#__PURE__*/Schema.isGreaterThan(0), /*#__PURE__*/Schema.isFinite());
|
|
126
|
+
/** A usable `catchUpPageSize`: see `subscriptionTuningFields` for the bounds' argument. */
|
|
127
|
+
const CatchUpPageSize = /*#__PURE__*/Schema.Int.check(/*#__PURE__*/Schema.isGreaterThan(0));
|
|
98
128
|
/**
|
|
99
129
|
* The two subscribe-tuning config FIELDS, for an engine whose config is a `Schema`
|
|
100
130
|
* to spread into its own struct.
|
|
@@ -125,8 +155,24 @@ export const DEFAULT_POLL_INTERVAL_MILLIS = 1000;
|
|
|
125
155
|
* absent key, never an explicit `undefined`.
|
|
126
156
|
*/
|
|
127
157
|
export const subscriptionTuningFields = {
|
|
128
|
-
pollInterval: /*#__PURE__*/Schema.optionalKey(
|
|
129
|
-
catchUpPageSize: /*#__PURE__*/Schema.optionalKey(
|
|
158
|
+
pollInterval: /*#__PURE__*/Schema.optionalKey(PollIntervalMillis),
|
|
159
|
+
catchUpPageSize: /*#__PURE__*/Schema.optionalKey(CatchUpPageSize)
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Why a caller-supplied tuning knob is unusable, naming the field and the value, or
|
|
163
|
+
* `undefined` when both are usable or absent. It judges with the SAME two schemas
|
|
164
|
+
* the config fields are built from, so `subscribeItems`, whose caller may have no
|
|
165
|
+
* config to decode, accepts exactly what an engine's config accepts and the bounds
|
|
166
|
+
* have one home.
|
|
167
|
+
*/
|
|
168
|
+
const tuningFault = deps => {
|
|
169
|
+
if (deps.pollInterval !== undefined && !Schema.is(PollIntervalMillis)(deps.pollInterval)) {
|
|
170
|
+
return `subscribeItems: pollInterval must be a finite number of milliseconds greater than 0, got ${deps.pollInterval}`;
|
|
171
|
+
}
|
|
172
|
+
if (deps.catchUpPageSize !== undefined && !Schema.is(CatchUpPageSize)(deps.catchUpPageSize)) {
|
|
173
|
+
return `subscribeItems: catchUpPageSize must be an integer greater than 0, got ${deps.catchUpPageSize}`;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
130
176
|
};
|
|
131
177
|
/**
|
|
132
178
|
* Retry a TRANSIENT poll fault (an `E` from one reconcile read) on a capped
|
|
@@ -137,14 +183,39 @@ export const subscriptionTuningFields = {
|
|
|
137
183
|
*/
|
|
138
184
|
const POLL_RETRY_SCHEDULE = /*#__PURE__*/Schedule.max([/*#__PURE__*/Schedule.min([/*#__PURE__*/Schedule.exponential('100 millis', 2), /*#__PURE__*/Schedule.spaced('2 seconds')]), /*#__PURE__*/Schedule.recurs(20)]);
|
|
139
185
|
/**
|
|
140
|
-
* Build
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
186
|
+
* Build a subscription `Stream` over any ascending-cursor source: catch up from
|
|
187
|
+
* `initial`, then track the live tail. The cursor threads through the
|
|
188
|
+
* `Stream.paginate` state, starting at `initial` and advancing to the cursor of the
|
|
189
|
+
* last delivered item each batch; the stream never completes on its own (the next
|
|
190
|
+
* state is always `Option.some`), so the enclosing scope interrupts it on close. An
|
|
191
|
+
* `E` mid-stream stays on the channel, for the caller to die or to supervise.
|
|
192
|
+
*
|
|
193
|
+
* This is THE delivery algorithm, and `subscribeStream` runs the same machine. Under
|
|
194
|
+
* `emit: 'backlog'` a batch is everything a drain to the tail returned; under
|
|
195
|
+
* `'page'` it is one fetched page, so the paginate step and the page coincide and a
|
|
196
|
+
* full page is followed at once by the next read rather than by a wait.
|
|
197
|
+
*
|
|
198
|
+
* A `pollInterval` or `catchUpPageSize` the config fields would refuse is a DEFECT
|
|
199
|
+
* here, raised by the stream before it reads or forks anything, with a message
|
|
200
|
+
* naming the field. This caller may have no config whose decode would have caught
|
|
201
|
+
* it, and unchecked the two failures are silent or late: a page size of 0 gives a
|
|
202
|
+
* stream that never emits and never fails, and 1.5 dies only at the first read,
|
|
203
|
+
* inside `ReadLimit`, in words that name neither knob.
|
|
204
|
+
*/
|
|
205
|
+
export const subscribeItems = deps => {
|
|
206
|
+
const fault = tuningFault(deps);
|
|
207
|
+
return fault === undefined ? machine(deps) : Stream.die(new Error(fault));
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* The machine itself, with no check of its tuning. `subscribeStream` runs it
|
|
211
|
+
* directly, and deliberately: the event path stays exactly as it was, with no
|
|
212
|
+
* second check of figures each SQL engine has already decoded from its config
|
|
213
|
+
* through the same bounds, and the in-memory store's are a test's own options.
|
|
146
214
|
*/
|
|
147
|
-
|
|
215
|
+
const machine = deps => Stream.unwrap(Effect.gen(function* () {
|
|
216
|
+
const pageSize = deps.catchUpPageSize ?? DEFAULT_CATCH_UP_PAGE_SIZE;
|
|
217
|
+
const pollInterval = deps.pollInterval ?? DEFAULT_POLL_INTERVAL_MILLIS;
|
|
218
|
+
const limit = ReadLimit.make(pageSize);
|
|
148
219
|
// The wake primitive: a sliding(1) queue holding at most one pending
|
|
149
220
|
// signal. The wake source `offer`s an opaque wake on every notification;
|
|
150
221
|
// the poll loop `take`s (with a timeout). The lost-wakeup guard is
|
|
@@ -165,61 +236,82 @@ export const subscribeStream = (deps, after) => Stream.unwrap(Effect.gen(functio
|
|
|
165
236
|
// scope releases it. We never read its elements directly — every
|
|
166
237
|
// notification is an opaque wake signal.
|
|
167
238
|
yield* listener.pipe(Stream.runDrain, Effect.forkScoped);
|
|
239
|
+
// One page strictly after `from`: the step `drainFrom` loops, and the whole
|
|
240
|
+
// of a `'page'` read.
|
|
241
|
+
const fetchPage = from => deps.fetch(from, limit);
|
|
168
242
|
// Reconcile: page-read from `from` until a short page, advancing to the last
|
|
169
|
-
// delivered
|
|
243
|
+
// delivered cursor. Retries a TRANSIENT poll fault on a capped schedule so
|
|
170
244
|
// a single connection blip does not kill a days-long subscription; a
|
|
171
|
-
// persistent fault propagates
|
|
245
|
+
// persistent fault propagates. The retry wraps the WHOLE drain, so a fault on
|
|
246
|
+
// page three re-reads pages one and two: nothing has been emitted yet, so
|
|
247
|
+
// that costs reads and never a duplicate.
|
|
172
248
|
const drainFrom = from => Effect.gen(function* () {
|
|
173
249
|
const collected = [];
|
|
174
250
|
let cursor = from;
|
|
175
251
|
// Loop pages until a short page signals the tail is drained.
|
|
176
252
|
while (true) {
|
|
177
|
-
const
|
|
178
|
-
for (const
|
|
179
|
-
collected.push(
|
|
253
|
+
const items = yield* fetchPage(cursor);
|
|
254
|
+
for (const item of items) {
|
|
255
|
+
collected.push(item);
|
|
180
256
|
}
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
257
|
+
const lastItem = items.at(-1);
|
|
258
|
+
if (lastItem === undefined || items.length < pageSize) {
|
|
183
259
|
break;
|
|
184
260
|
}
|
|
185
|
-
cursor =
|
|
261
|
+
cursor = deps.cursorOf(lastItem);
|
|
186
262
|
}
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
events: collected,
|
|
190
|
-
last
|
|
191
|
-
};
|
|
263
|
+
return collected;
|
|
192
264
|
}).pipe(Effect.retry(POLL_RETRY_SCHEDULE));
|
|
265
|
+
// The read one paginate step performs, by emission mode. Under `'page'` the
|
|
266
|
+
// retry wraps the single page, which is the whole of what is unemitted.
|
|
267
|
+
const read = (deps.emit ?? 'backlog') === 'page' ? from => fetchPage(from).pipe(Effect.retry(POLL_RETRY_SCHEDULE)) : drainFrom;
|
|
193
268
|
/**
|
|
194
|
-
* Produce the NEXT non-empty batch from a cursor
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
269
|
+
* Produce the NEXT non-empty batch from a cursor, together with the advanced
|
|
270
|
+
* cursor — a pure `C → Effect<[items, next]>`. The `> boundary` de-dup is
|
|
271
|
+
* enforced by `fetch`'s exclusive lower bound, so a live re-read after a wake
|
|
272
|
+
* never re-emits an already-delivered item.
|
|
198
273
|
*
|
|
199
|
-
* Drain the wake queue BEFORE reading (lost-wakeup guard), then
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* close.
|
|
274
|
+
* Drain the wake queue BEFORE reading (lost-wakeup guard), then read from the
|
|
275
|
+
* cursor. If a read yields nothing, wait for a wake but only up to the poll
|
|
276
|
+
* interval, then loop and re-read ANYWAY — the periodic cursor-based poll that
|
|
277
|
+
* makes delivery correct independently of any wake. This effect only
|
|
278
|
+
* completes by returning a non-empty batch; the scope interrupts it on close.
|
|
205
279
|
*/
|
|
206
280
|
const nextBatch = from => Effect.gen(function* () {
|
|
207
281
|
while (true) {
|
|
208
282
|
// `Queue.clear` and not `Queue.takeAll`: the drain must NOT wait, and
|
|
209
283
|
// `takeAll` suspends until at least one signal is present.
|
|
210
284
|
yield* Queue.clear(wake);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (events.length > 0) {
|
|
216
|
-
return [events, last];
|
|
285
|
+
const items = yield* read(from);
|
|
286
|
+
const last = items.at(-1);
|
|
287
|
+
if (last !== undefined) {
|
|
288
|
+
return [items, deps.cursorOf(last)];
|
|
217
289
|
}
|
|
218
290
|
// Nothing new: block on a wake, but only up to the poll interval — on
|
|
219
291
|
// timeout we fall through and the loop re-reads regardless.
|
|
220
|
-
yield* Effect.ignore(Effect.timeout(Queue.take(wake),
|
|
292
|
+
yield* Effect.ignore(Effect.timeout(Queue.take(wake), pollInterval));
|
|
221
293
|
}
|
|
222
294
|
});
|
|
223
|
-
return Stream.paginate(
|
|
295
|
+
return Stream.paginate(deps.initial, from => Effect.map(nextBatch(from), ([items, next]) => [items, Option.some(next)]));
|
|
224
296
|
}));
|
|
297
|
+
/**
|
|
298
|
+
* Build the event subscription `Stream`: catch up from `after`, then track the live
|
|
299
|
+
* tail. A THIN wrapper over the machine `subscribeItems` runs, fixing the item to
|
|
300
|
+
* `SequencedEvent`, the cursor to its `position`, the start to `ORIGIN` when `after`
|
|
301
|
+
* is absent, and the emission to the default `'backlog'` — so every engine's call
|
|
302
|
+
* site, and every figure it delivers, is what it was before the machine became
|
|
303
|
+
* generic. It skips `subscribeItems`' tuning check, for the reason on `machine`. An `E`
|
|
304
|
+
* mid-stream stays on the channel; `makeContractStore` dies it (subscribe is
|
|
305
|
+
* `E = never` on the contract).
|
|
306
|
+
*/
|
|
307
|
+
export const subscribeStream = (deps, after) => machine({
|
|
308
|
+
fetch: deps.fetch,
|
|
309
|
+
cursorOf: event => event.position,
|
|
310
|
+
initial: after ?? ORIGIN,
|
|
311
|
+
...(deps.listen === undefined ? {} : {
|
|
312
|
+
listen: deps.listen
|
|
313
|
+
}),
|
|
314
|
+
pollInterval: deps.pollInterval,
|
|
315
|
+
catchUpPageSize: deps.catchUpPageSize
|
|
316
|
+
});
|
|
225
317
|
//# sourceMappingURL=SubscribeMachine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscribeMachine.js","names":["Effect","Option","Queue","Schedule","Schema","Stream","ORIGIN","ReadLimit","DEFAULT_CATCH_UP_PAGE_SIZE","DEFAULT_POLL_INTERVAL_MILLIS","
|
|
1
|
+
{"version":3,"file":"SubscribeMachine.js","names":["Effect","Option","Queue","Schedule","Schema","Stream","ORIGIN","ReadLimit","DEFAULT_CATCH_UP_PAGE_SIZE","DEFAULT_POLL_INTERVAL_MILLIS","PollIntervalMillis","Number","check","isGreaterThan","isFinite","CatchUpPageSize","Int","subscriptionTuningFields","pollInterval","optionalKey","catchUpPageSize","tuningFault","deps","undefined","is","POLL_RETRY_SCHEDULE","max","min","exponential","spaced","recurs","subscribeItems","fault","machine","die","Error","unwrap","gen","pageSize","limit","make","wake","sliding","listener","listen","never","pipe","tap","offer","retry","runDrain","forkScoped","fetchPage","from","fetch","drainFrom","collected","cursor","items","item","push","lastItem","at","length","cursorOf","read","emit","nextBatch","clear","last","ignore","timeout","take","paginate","initial","map","next","some","subscribeStream","after","event","position"],"sources":["../../src/SubscribeMachine.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|