@kairos-es/feed 0.0.0-stage → 0.0.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/LICENSE +28 -0
- package/README.md +109 -2
- package/dist/cjs/Feed.js +6 -0
- package/dist/cjs/Feed.js.map +1 -0
- package/dist/cjs/FeedSink.js +54 -0
- package/dist/cjs/FeedSink.js.map +1 -0
- package/dist/cjs/FeedSyncStore.js +10 -0
- package/dist/cjs/FeedSyncStore.js.map +1 -0
- package/dist/cjs/deliver.js +86 -0
- package/dist/cjs/deliver.js.map +1 -0
- package/dist/cjs/errors.js +114 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/identifiers.js +63 -0
- package/dist/cjs/identifiers.js.map +1 -0
- package/dist/cjs/inMemoryFeed.js +328 -0
- package/dist/cjs/inMemoryFeed.js.map +1 -0
- package/dist/cjs/inMemorySink.js +142 -0
- package/dist/cjs/inMemorySink.js.map +1 -0
- package/dist/cjs/index.js +184 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/inspect.js +68 -0
- package/dist/cjs/inspect.js.map +1 -0
- package/dist/cjs/readiness.js +100 -0
- package/dist/cjs/readiness.js.map +1 -0
- package/dist/cjs/reconcile.js +164 -0
- package/dist/cjs/reconcile.js.map +1 -0
- package/dist/cjs/runFeedSync.js +603 -0
- package/dist/cjs/runFeedSync.js.map +1 -0
- package/dist/cjs/tuning.js +60 -0
- package/dist/cjs/tuning.js.map +1 -0
- package/dist/dts-cjs/Feed.d.ts +99 -0
- package/dist/dts-cjs/Feed.d.ts.map +1 -0
- package/dist/dts-cjs/FeedSink.d.ts +189 -0
- package/dist/dts-cjs/FeedSink.d.ts.map +1 -0
- package/dist/dts-cjs/FeedSyncStore.d.ts +281 -0
- package/dist/dts-cjs/FeedSyncStore.d.ts.map +1 -0
- package/dist/dts-cjs/deliver.d.ts +16 -0
- package/dist/dts-cjs/deliver.d.ts.map +1 -0
- package/dist/dts-cjs/errors.d.ts +131 -0
- package/dist/dts-cjs/errors.d.ts.map +1 -0
- package/dist/dts-cjs/identifiers.d.ts +67 -0
- package/dist/dts-cjs/identifiers.d.ts.map +1 -0
- package/dist/dts-cjs/inMemoryFeed.d.ts +32 -0
- package/dist/dts-cjs/inMemoryFeed.d.ts.map +1 -0
- package/dist/dts-cjs/inMemorySink.d.ts +61 -0
- package/dist/dts-cjs/inMemorySink.d.ts.map +1 -0
- package/dist/dts-cjs/index.d.ts +63 -0
- package/dist/dts-cjs/index.d.ts.map +1 -0
- package/dist/dts-cjs/inspect.d.ts +71 -0
- package/dist/dts-cjs/inspect.d.ts.map +1 -0
- package/dist/dts-cjs/package.json +3 -0
- package/dist/dts-cjs/readiness.d.ts +84 -0
- package/dist/dts-cjs/readiness.d.ts.map +1 -0
- package/dist/dts-cjs/reconcile.d.ts +68 -0
- package/dist/dts-cjs/reconcile.d.ts.map +1 -0
- package/dist/dts-cjs/runFeedSync.d.ts +220 -0
- package/dist/dts-cjs/runFeedSync.d.ts.map +1 -0
- package/dist/dts-cjs/tuning.d.ts +40 -0
- package/dist/dts-cjs/tuning.d.ts.map +1 -0
- package/dist/dts-esm/Feed.d.ts +99 -0
- package/dist/dts-esm/Feed.d.ts.map +1 -0
- package/dist/dts-esm/FeedSink.d.ts +189 -0
- package/dist/dts-esm/FeedSink.d.ts.map +1 -0
- package/dist/dts-esm/FeedSyncStore.d.ts +281 -0
- package/dist/dts-esm/FeedSyncStore.d.ts.map +1 -0
- package/dist/dts-esm/deliver.d.ts +16 -0
- package/dist/dts-esm/deliver.d.ts.map +1 -0
- package/dist/dts-esm/errors.d.ts +131 -0
- package/dist/dts-esm/errors.d.ts.map +1 -0
- package/dist/dts-esm/identifiers.d.ts +67 -0
- package/dist/dts-esm/identifiers.d.ts.map +1 -0
- package/dist/dts-esm/inMemoryFeed.d.ts +32 -0
- package/dist/dts-esm/inMemoryFeed.d.ts.map +1 -0
- package/dist/dts-esm/inMemorySink.d.ts +61 -0
- package/dist/dts-esm/inMemorySink.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +63 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/inspect.d.ts +71 -0
- package/dist/dts-esm/inspect.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/readiness.d.ts +84 -0
- package/dist/dts-esm/readiness.d.ts.map +1 -0
- package/dist/dts-esm/reconcile.d.ts +68 -0
- package/dist/dts-esm/reconcile.d.ts.map +1 -0
- package/dist/dts-esm/runFeedSync.d.ts +220 -0
- package/dist/dts-esm/runFeedSync.d.ts.map +1 -0
- package/dist/dts-esm/tuning.d.ts +40 -0
- package/dist/dts-esm/tuning.d.ts.map +1 -0
- package/dist/esm/Feed.js +2 -0
- package/dist/esm/Feed.js.map +1 -0
- package/dist/esm/FeedSink.js +46 -0
- package/dist/esm/FeedSink.js.map +1 -0
- package/dist/esm/FeedSyncStore.js +3 -0
- package/dist/esm/FeedSyncStore.js.map +1 -0
- package/dist/esm/deliver.js +78 -0
- package/dist/esm/deliver.js.map +1 -0
- package/dist/esm/errors.js +98 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/identifiers.js +56 -0
- package/dist/esm/identifiers.js.map +1 -0
- package/dist/esm/inMemoryFeed.js +320 -0
- package/dist/esm/inMemoryFeed.js.map +1 -0
- package/dist/esm/inMemorySink.js +134 -0
- package/dist/esm/inMemorySink.js.map +1 -0
- package/dist/esm/index.js +62 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/inspect.js +58 -0
- package/dist/esm/inspect.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/readiness.js +92 -0
- package/dist/esm/readiness.js.map +1 -0
- package/dist/esm/reconcile.js +153 -0
- package/dist/esm/reconcile.js.map +1 -0
- package/dist/esm/runFeedSync.js +595 -0
- package/dist/esm/runFeedSync.js.map +1 -0
- package/dist/esm/tuning.js +51 -0
- package/dist/esm/tuning.js.map +1 -0
- package/package.json +34 -3
- package/src/Feed.ts +111 -0
- package/src/FeedSink.ts +130 -0
- package/src/FeedSyncStore.ts +326 -0
- package/src/deliver.ts +108 -0
- package/src/errors.ts +134 -0
- package/src/identifiers.ts +92 -0
- package/src/inMemoryFeed.ts +582 -0
- package/src/inMemorySink.ts +215 -0
- package/src/index.ts +136 -0
- package/src/inspect.ts +114 -0
- package/src/readiness.ts +137 -0
- package/src/reconcile.ts +224 -0
- package/src/runFeedSync.ts +882 -0
- package/src/tuning.ts +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Neverbland
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kairos-es/feed
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A state-only change feed for kairos-es. A read model writes the newest state of each record it touches into a **feed**, inside the same commit as its view and its checkpoint. A **sync worker** copies the rows past its cursor to a **sink** (a search index, a cache, a mirror) and moves its cursor only once the sink confirms them. Use it for a target that has no transaction of its own and so cannot be a `ProjectionStore` (ADR-0019).
|
|
4
|
+
|
|
5
|
+
The feed copies **state**: each record once per batch, at its newest version, however many events touched it, and a delete when the read model removes it. It never tells anyone each thing that happened; for that, use `@kairos-es/automation`.
|
|
6
|
+
|
|
7
|
+
This package holds the ports, the driver and the in-memory implementations every other backend is held against:
|
|
8
|
+
|
|
9
|
+
- `FeedWriter` / `BatchedFeedWriter` (what a read model writes) and `FeedSource` (what a worker reads);
|
|
10
|
+
- `FeedSink`, with its optional `FeedResync` and `FeedReconcile` parts;
|
|
11
|
+
- `FeedSyncStore`, the worker's cursor, parked list and lease;
|
|
12
|
+
- `runFeedSync`, the worker, with `materialisationCaughtUp` for its `readiness`, and `reconcileFeed`, `inspectFeed`, `retryParked` and `removeSink`;
|
|
13
|
+
- `makeInMemoryFeed` / `withInMemoryFeed` and `makeInMemorySink`.
|
|
14
|
+
|
|
15
|
+
`@kairos-es/feed-postgres` is the durable feed, and `@kairos-es/feed-algolia` the first sink.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
`@kairos-es/core`, `@kairos-es/read` and `effect` are peer dependencies. `@kairos-es/codec` is on the line because `@kairos-es/read` peers it:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add @kairos-es/feed@alpha @kairos-es/read@alpha @kairos-es/codec@alpha \
|
|
23
|
+
@kairos-es/core@alpha effect@^4.0.5
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Writing the feed
|
|
27
|
+
|
|
28
|
+
Make the feed's write the read model's own: in `apply`, call `feed.write(at, body)` with the batch's last position, and in the body `upsert(key, doc)` or `remove(key)` each record the batch changed. The feed keeps the last write per key within a batch, skips a document equal to the one it holds (compared on its encoded form, so key order does not matter), and lands the batch only if the body succeeds.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { FeedKey, FeedName, makeInMemoryFeed, withInMemoryFeed } from '@kairos-es/feed'
|
|
32
|
+
import { makeInMemoryProjectionStore } from '@kairos-es/read'
|
|
33
|
+
|
|
34
|
+
const feed = yield* makeInMemoryFeed({ feed: FeedName.make('course-search'), doc: CourseDoc })
|
|
35
|
+
const store = yield* withInMemoryFeed(yield* makeInMemoryProjectionStore, feed, key)
|
|
36
|
+
|
|
37
|
+
const apply = (batch) =>
|
|
38
|
+
feed.write(Arr.lastNonEmpty(batch).sequenced.position, (writer) =>
|
|
39
|
+
Effect.forEach(touchedCourses(batch), (course) =>
|
|
40
|
+
course.listed
|
|
41
|
+
? writer.upsert(FeedKey.make(course.id), toDoc(course))
|
|
42
|
+
: writer.remove(FeedKey.make(course.id)),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Things to know:
|
|
48
|
+
|
|
49
|
+
- **One feed per materialisation.** A feed's order is the order its one writer commits in, so a feed is bound to one checkpoint key, and binding it to a second is refused. Use a feed per read model, and a sink per feed.
|
|
50
|
+
- **A reset rebuilds the sinks.** Reset the checkpoint through the store `withInMemoryFeed` (or `feed-postgres`'s `withFeed`) returns, and the feed is emptied and its generation moves on. Every sink then rebuilds from what the replay writes. A sink with `FeedResync` keeps serving its old copy until the new one is published; a sink without it is rebuilt in place, over the copy it is serving, so it serves a mixture of old and new from the first page on. Give the worker a `readiness` (below) so that a published copy is complete, and so that an in-place rebuild does not delete what the replay has not written yet.
|
|
51
|
+
- **A remove is a tombstone.** It reaches every sink as a delete, and is collected once every sink registered on the feed has passed it, so a sink that is resyncing holds collection back until its resync is saved. So does a sink you stopped running: remove it (below) when you retire it.
|
|
52
|
+
- **A document that fails to encode through its `Schema` is a defect** in the read model, not a sink's refusal.
|
|
53
|
+
- **In memory, write the feed last in `apply`.** The in-memory store cannot roll back a view written before a step that failed; the in-memory feed lands its batch in one update and fails only by a defect. The in-memory feed is ephemeral, like the in-memory store: its generation is fresh on every boot, so a sink that outlives the process rebuilds rather than keeping a copy of a feed that no longer exists, and it refuses a durable store.
|
|
54
|
+
|
|
55
|
+
## Running the sync
|
|
56
|
+
|
|
57
|
+
Start one worker per feed and sink, in a scope beside your projections:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { runFeedSync } from '@kairos-es/feed'
|
|
61
|
+
|
|
62
|
+
yield* Effect.forkScoped(
|
|
63
|
+
runFeedSync({ source: feed.source, sink, store: feed.syncStore }),
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
What it does:
|
|
68
|
+
|
|
69
|
+
- **It holds a lease.** A sink has no transaction, so two workers could send out of order; a second worker for the same feed and sink waits as a standby until the first lets go. The cursor's compare-and-set also checks the holder, so a worker that has lost its lease cannot move the cursor.
|
|
70
|
+
- **The cursor moves only after the sink confirms** the batch is applied and visible. A crash before that resends the batch, so a sink's writes must be idempotent.
|
|
71
|
+
- **A record the sink refuses is parked**, with the sink's reason, and the rest of the batch moves on. It is sent again when its row next changes, or when you call `retryParked`. A later delete of a parked record is sent and clears it.
|
|
72
|
+
- **A new generation is a resync.** With `FeedResync`, the worker loads every live row into a fresh target and publishes it over the live one in one step. Without it, the worker resyncs in place, sending every row over the live copy, and then reconciles away what the feed no longer holds. In place, a refusal leaves the old copy's record live: a refused upsert leaves the previous generation's document of that key in the sink, and an orphan whose delete is refused stays in it. Both are in the parked list with the sink's reason, and the closing reconcile's report goes to `onReconcile`.
|
|
73
|
+
- **A rebuild waits for the replay when you give it `readiness`.** After a reset, the worker starts its resync at once, while the read model is still replaying the log into the feed. Without `readiness`, the worker finishes the rebuild when it reaches the end of the feed — publishing, or in place reconciling — so **search can serve a partial index** until the replay is done and the tail fills in the rest. With `readiness`, the worker keeps sending what the replay writes, and finishes only when the readiness says the read model has caught up. It checks the generation on every poll while it waits, so a second reset abandons the attempt and starts a fresh one. `materialisationCaughtUp(readModel)` is the usual readiness: the read model's checkpoint has reached the last event its query selected when the attempt started. Pass the read model whose `apply` writes this feed:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { materialisationCaughtUp, runFeedSync } from '@kairos-es/feed'
|
|
77
|
+
|
|
78
|
+
const readiness = yield* materialisationCaughtUp(readModel) // needs DcbEventStore
|
|
79
|
+
yield* Effect.forkScoped(
|
|
80
|
+
runFeedSync({ source: feed.source, sink, store: feed.syncStore, readiness }),
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Each rebuild attempt starts with one paged read of the read model's events, which is the backlog the replay reads anyway. Your own `FeedReadiness` is a function from the generation to a probe: the function is called once per attempt, and the probe it returns is asked on every poll until it answers `true`. For a sink without `resync`, the wait holds the final reconcile and the cursor save instead, so the reconcile does not delete records the replay has not reached yet; without `readiness`, it does delete them, and the tail sends each back as the replay writes it.
|
|
85
|
+
|
|
86
|
+
The wait has no time limit: publishing early is what it exists to prevent, so a readiness that never answers `true` holds the rebuild for good, with no error. You can see it two ways. `inspectFeed` reports the sink `resyncDue` for as long as the wait lasts, and the worker logs a warning, `feed sync rebuild held on readiness`, every 60 poll intervals it has waited (a minute at the default poll), with the feed, sink, generation, polls and `heldMillis`.
|
|
87
|
+
- **Reconcile** deletes the records a sink holds that the feed no longer has. Set `reconcileEvery` to run it between batches, or call `reconcileFeed` (which takes the lease itself).
|
|
88
|
+
- **It supervises itself.** A failure ends the session and the worker restarts with a jittered back-off (100 ms to 30 s by default, never sooner than a sink's `Retry-After`). It gives up with `FeedSyncStalled` only after `maxNoProgressRestarts` restarts in a row with no cursor advance. Every knob is in `tuning`.
|
|
89
|
+
|
|
90
|
+
The worker polls on `pollInterval` (1 second by default); a source's optional `listen` stream only wakes it sooner.
|
|
91
|
+
|
|
92
|
+
## Writing a sink
|
|
93
|
+
|
|
94
|
+
Implement `FeedSink<D, Receipt>`:
|
|
95
|
+
|
|
96
|
+
- `apply(batch)` sends a non-empty batch of `FeedChange`s, each an `Upsert` with the whole document or a `Delete`, and returns a receipt. Write whole records only: a partial update is not idempotent, and a batch may be sent twice.
|
|
97
|
+
- `confirm(receipt)` resolves once that batch is applied **and visible**. Resolving early lets the cursor pass a batch a crash could still lose.
|
|
98
|
+
- Fail with `SinkRetryable` (try again later, optionally with `retryAfter`), `SinkRejected` (these keys, with a reason each; the rest of the batch is resent) or `SinkHalted` (stop; supervision decides).
|
|
99
|
+
- Give it `resync` (`begin`, `load`, `publish`, for a target with an atomic swap), `reconcile` (`listKeys`, a stream of every key the target holds), or both. `runFeedSync` refuses a sink with neither, because it could never rebuild.
|
|
100
|
+
- A `resync` must keep each attempt's target its own. Every `begin` returns a name no earlier `begin` returned, even for the same generation; every `begin` drops every other target an earlier attempt left; and a target that has gone is never published: a `load` into it may fail, or may recreate it if your target springs into being on its first write, but either way its `publish` fails `SinkRetryable` and leaves the live copy alone. Without all three, a worker that has lost its lease without knowing it yet can publish the half-built target of the worker that holds the lease now, and the pages that worker had not loaded yet are lost from the live copy without any error. The hover docs on `FeedResync` and `ResyncTarget` state the full contract.
|
|
101
|
+
|
|
102
|
+
`makeInMemorySink` is the reference sink, and the one to run your read model against in a test. The law suite this repository holds every sink to is not published for the alpha.
|
|
103
|
+
|
|
104
|
+
## Operating a feed
|
|
105
|
+
|
|
106
|
+
`inspectFeed(source, store)` reports, per sink, its generation, cursor, when the cursor last moved (`lastAdvance`), lag, lease and parked records with their reasons, and `retryParked(store, { feed, sink, key })` sends one parked record again.
|
|
107
|
+
|
|
108
|
+
Every sink registered on a feed holds its tombstone collection back, a sink you no longer run included. A sink whose `lastAdvance` stays put while the feed's head moves is one nothing is running. Stop its worker, then call `removeSink(store, { feed, sink })`: it deletes the sink's cursor, lease record and parked records, and fails `LeaseHeld` without removing anything while a worker for the sink is still running. Starting a worker for that sink again later registers it afresh, and it resyncs.
|
|
109
|
+
|
|
110
|
+
None of these needs the sink's client or credentials.
|
package/dist/cjs/Feed.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Feed.js","names":[],"sources":["../../src/Feed.ts"],"sourcesContent":[null],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.changeOf = exports.FeedChange = void 0;
|
|
7
|
+
var _effect = require("effect");
|
|
8
|
+
/**
|
|
9
|
+
* The sink port: what a target a feed is copied to must offer the driver.
|
|
10
|
+
*
|
|
11
|
+
* ## Three parts, two of them optional
|
|
12
|
+
*
|
|
13
|
+
* - `apply` and `confirm` are the whole of the steady state. `apply` sends a
|
|
14
|
+
* batch and returns a `Receipt`; `confirm` resolves once that batch is applied
|
|
15
|
+
* AND VISIBLE in the target. The driver moves its cursor only after `confirm`
|
|
16
|
+
* (ADR-0019 §6), so a crash between the two resends the batch — which is why
|
|
17
|
+
* every sink must be IDEMPOTENT: the same batch applied twice leaves the target
|
|
18
|
+
* as once. Full-record replace satisfies that; a partial operation such as an
|
|
19
|
+
* increment does not, and a sink must never use one.
|
|
20
|
+
* - `resync` is how a sink rebuilds after the feed's generation moves: load every
|
|
21
|
+
* row into a fresh target that is not yet serving — tombstones too, as deletes,
|
|
22
|
+
* since the feed can delete a key the same resync has already loaded — then swap
|
|
23
|
+
* it over the live one atomically, so search keeps serving the old copy until the new one is
|
|
24
|
+
* complete and records deleted from the feed do not survive the rebuild.
|
|
25
|
+
* - `reconcile` lists the keys the target holds, so the driver can delete those
|
|
26
|
+
* the feed no longer has. Deletes go through the ordinary `apply`.
|
|
27
|
+
*
|
|
28
|
+
* A sink with no atomic swap leaves out `resync`, and the driver then resyncs IN
|
|
29
|
+
* PLACE: it sends every row to the live target and reconciles away what the feed
|
|
30
|
+
* no longer holds. That needs `reconcile`, so `runFeedSync` refuses at
|
|
31
|
+
* construction a sink that has neither part: such a sink could never be rebuilt.
|
|
32
|
+
*
|
|
33
|
+
* ## Why `FeedChange` is not `FeedRow`
|
|
34
|
+
*
|
|
35
|
+
* A sink is handed the change to make, not the row it came from: an `Upsert` with
|
|
36
|
+
* the document or a `Delete`. The sequence and the event position are the
|
|
37
|
+
* driver's business, and a sink that could see them could be tempted to keep a
|
|
38
|
+
* cursor of its own, which would be a second record of progress.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** `FeedChange.Upsert({ key, doc })`, `FeedChange.Delete({ key })`, and matchers. */
|
|
42
|
+
const FeedChange = exports.FeedChange = /*#__PURE__*/_effect.Data.taggedEnum();
|
|
43
|
+
/** The change a row asks of a sink: its document as an upsert, a tombstone as a delete. */
|
|
44
|
+
const changeOf = row => _effect.Option.match(row.doc, {
|
|
45
|
+
onNone: () => FeedChange.Delete({
|
|
46
|
+
key: row.key
|
|
47
|
+
}),
|
|
48
|
+
onSome: doc => FeedChange.Upsert({
|
|
49
|
+
key: row.key,
|
|
50
|
+
doc
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
exports.changeOf = changeOf;
|
|
54
|
+
//# sourceMappingURL=FeedSink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedSink.js","names":["_effect","require","FeedChange","exports","Data","taggedEnum","changeOf","row","Option","match","doc","onNone","Delete","key","onSome","Upsert"],"sources":["../../src/FeedSink.ts"],"sourcesContent":[null],"mappings":";;;;;;AAiCA,IAAAA,OAAA,GAAAC,OAAA;AAjCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA;AACO,MAAMC,UAAU,GAAAC,OAAA,CAAAD,UAAA,gBAAGE,YAAI,CAACC,UAAU,EAAwB;AAEjE;AACO,MAAMC,QAAQ,GAAOC,GAAe,IACzCC,cAAM,CAACC,KAAK,CAACF,GAAG,CAACG,GAAG,EAAE;EACpBC,MAAM,EAAEA,CAAA,KAAMT,UAAU,CAACU,MAAM,CAAC;IAAEC,GAAG,EAAEN,GAAG,CAACM;EAAG,CAAE,CAAC;EACjDC,MAAM,EAAGJ,GAAG,IAAKR,UAAU,CAACa,MAAM,CAAC;IAAEF,GAAG,EAAEN,GAAG,CAACM,GAAG;IAAEH;EAAG,CAAE;CACzD,CAAC;AAAAP,OAAA,CAAAG,QAAA,GAAAA,QAAA","ignoreList":[]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.sameCursor = void 0;
|
|
7
|
+
/** Whether two optional cursors are the same cursor, as `casCursor` compares them. */
|
|
8
|
+
const sameCursor = (left, right) => left._tag === 'None' ? right._tag === 'None' : right._tag === 'Some' && left.value.generation === right.value.generation && left.value.seq === right.value.seq;
|
|
9
|
+
exports.sameCursor = sameCursor;
|
|
10
|
+
//# sourceMappingURL=FeedSyncStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedSyncStore.js","names":["sameCursor","left","right","_tag","value","generation","seq","exports"],"sources":["../../src/FeedSyncStore.ts"],"sourcesContent":[null],"mappings":";;;;;;AA4NA;AACO,MAAMA,UAAU,GAAGA,CACxBC,IAA+B,EAC/BC,KAAgC,KAEhCD,IAAI,CAACE,IAAI,KAAK,MAAM,GAChBD,KAAK,CAACC,IAAI,KAAK,MAAM,GACrBD,KAAK,CAACC,IAAI,KAAK,MAAM,IACrBF,IAAI,CAACG,KAAK,CAACC,UAAU,KAAKH,KAAK,CAACE,KAAK,CAACC,UAAU,IAChDJ,IAAI,CAACG,KAAK,CAACE,GAAG,KAAKJ,KAAK,CAACE,KAAK,CAACE,GAAG;AAAAC,OAAA,CAAAP,UAAA,GAAAA,UAAA","ignoreList":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.deliver = exports.UNEXPLAINED_REFUSAL = void 0;
|
|
7
|
+
var _effect = require("effect");
|
|
8
|
+
var _errors = require("./errors.js");
|
|
9
|
+
/**
|
|
10
|
+
* One batch to a sink, end to end: send, park what the sink refuses, send the rest
|
|
11
|
+
* again, then confirm. Package-internal, and the ONE place the driver talks to a
|
|
12
|
+
* sink's write path, so the steady state, a resync's load and a reconcile's
|
|
13
|
+
* deletes cannot each grow their own idea of what a refusal means.
|
|
14
|
+
*
|
|
15
|
+
* ## Why a refusal re-sends the rest rather than skipping the batch
|
|
16
|
+
*
|
|
17
|
+
* A sink that refuses one record of a batch has, by the port's contract, applied
|
|
18
|
+
* none of it — Algolia, the first sink, rejects a whole `batch` call for one
|
|
19
|
+
* oversized record. So the records it did NOT name must go again, without the
|
|
20
|
+
* refused ones, until the sink takes what is left or there is nothing left. Each
|
|
21
|
+
* round removes at least one key, so the loop ends. A sink that names no key of
|
|
22
|
+
* the batch would loop forever instead, and that is a sink bug the driver turns
|
|
23
|
+
* into `SinkHalted` rather than spin on.
|
|
24
|
+
*
|
|
25
|
+
* ## The lease, before every send
|
|
26
|
+
*
|
|
27
|
+
* `ensureHeld` runs before each `send`, not once per batch: a refusal round is a
|
|
28
|
+
* send like any other, and a worker that lost its lease between two rounds must
|
|
29
|
+
* not make the second.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** The reason recorded for a refused key the sink gave no reason for. */
|
|
33
|
+
const UNEXPLAINED_REFUSAL = exports.UNEXPLAINED_REFUSAL = 'refused by the sink, with no reason given';
|
|
34
|
+
/**
|
|
35
|
+
* Send `pending` through `send`, parking each refused key with the sink's reason,
|
|
36
|
+
* and confirm the receipt of whatever was finally taken. See the module doc.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
const deliver = args => _effect.Effect.gen(function* () {
|
|
41
|
+
let remaining = args.pending;
|
|
42
|
+
const parked = [];
|
|
43
|
+
for (;;) {
|
|
44
|
+
if (!_effect.Array.isReadonlyArrayNonEmpty(remaining)) {
|
|
45
|
+
return {
|
|
46
|
+
sent: [],
|
|
47
|
+
parked
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const batch = _effect.Array.map(remaining, p => p.change);
|
|
51
|
+
yield* args.lease.ensureHeld;
|
|
52
|
+
const sent = yield* args.send(batch).pipe(_effect.Effect.map(receipt => ({
|
|
53
|
+
_tag: 'Taken',
|
|
54
|
+
receipt
|
|
55
|
+
})), _effect.Effect.catchTag('SinkRejected', rejected => _effect.Effect.succeed({
|
|
56
|
+
_tag: 'Refused',
|
|
57
|
+
rejected
|
|
58
|
+
})));
|
|
59
|
+
if (sent._tag === 'Taken') {
|
|
60
|
+
yield* args.confirm(sent.receipt);
|
|
61
|
+
return {
|
|
62
|
+
sent: remaining.map(p => p.change.key),
|
|
63
|
+
parked
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const refused = new Set(sent.rejected.keys);
|
|
67
|
+
const named = remaining.filter(p => refused.has(p.change.key));
|
|
68
|
+
if (named.length === 0) {
|
|
69
|
+
return yield* new _errors.SinkHalted({
|
|
70
|
+
reason: `the sink refused keys that are not in the batch (${sent.rejected.keys.join(', ')})`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const parkedAt = yield* _effect.DateTime.now;
|
|
74
|
+
for (const p of named) {
|
|
75
|
+
parked.push({
|
|
76
|
+
key: p.change.key,
|
|
77
|
+
seq: p.seq,
|
|
78
|
+
reason: sent.rejected.reasons.get(p.change.key) ?? UNEXPLAINED_REFUSAL,
|
|
79
|
+
parkedAt
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
remaining = remaining.filter(p => !refused.has(p.change.key));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
exports.deliver = deliver;
|
|
86
|
+
//# sourceMappingURL=deliver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.js","names":["_effect","require","_errors","UNEXPLAINED_REFUSAL","exports","deliver","args","Effect","gen","remaining","pending","parked","Arr","isReadonlyArrayNonEmpty","sent","batch","map","p","change","lease","ensureHeld","send","pipe","receipt","_tag","catchTag","rejected","succeed","confirm","key","refused","Set","keys","named","filter","has","length","SinkHalted","reason","join","parkedAt","DateTime","now","push","seq","reasons","get"],"sources":["../../src/deliver.ts"],"sourcesContent":[null],"mappings":";;;;;;AAsBA,IAAAA,OAAA,GAAAC,OAAA;AAEA,IAAAC,OAAA,GAAAD,OAAA;AAxBA;;;;;;;;;;;;;;;;;;;;;;;AA+CA;AACO,MAAME,mBAAmB,GAAAC,OAAA,CAAAD,mBAAA,GAAG,2CAA2C;AAE9E;;;;;;AAMO,MAAME,OAAO,GAAgBC,IASnC,IAICC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,IAAIC,SAAS,GAA8BH,IAAI,CAACI,OAAO;EACvD,MAAMC,MAAM,GAAwB,EAAE;EACtC,SAAS;IACP,IAAI,CAACC,aAAG,CAACC,uBAAuB,CAACJ,SAAS,CAAC,EAAE;MAC3C,OAAO;QAAEK,IAAI,EAAE,EAAE;QAAEH;MAAM,CAAE;IAC7B;IACA,MAAMI,KAAK,GAAGH,aAAG,CAACI,GAAG,CAACP,SAAS,EAAGQ,CAAC,IAAKA,CAAC,CAACC,MAAM,CAAC;IACjD,OAAOZ,IAAI,CAACa,KAAK,CAACC,UAAU;IAC5B,MAAMN,IAAI,GAAG,OAAOR,IAAI,CAACe,IAAI,CAACN,KAAK,CAAC,CAACO,IAAI,CACvCf,cAAM,CAACS,GAAG,CAAEO,OAAO,KAAM;MAAEC,IAAI,EAAE,OAAgB;MAAED;IAAO,CAAE,CAAC,CAAC,EAC9DhB,cAAM,CAACkB,QAAQ,CAAC,cAAc,EAAGC,QAAQ,IACvCnB,cAAM,CAACoB,OAAO,CAAC;MAAEH,IAAI,EAAE,SAAkB;MAAEE;IAAQ,CAAE,CAAC,CACvD,CACF;IACD,IAAIZ,IAAI,CAACU,IAAI,KAAK,OAAO,EAAE;MACzB,OAAOlB,IAAI,CAACsB,OAAO,CAACd,IAAI,CAACS,OAAO,CAAC;MACjC,OAAO;QAAET,IAAI,EAAEL,SAAS,CAACO,GAAG,CAAEC,CAAC,IAAKA,CAAC,CAACC,MAAM,CAACW,GAAG,CAAC;QAAElB;MAAM,CAAE;IAC7D;IACA,MAAMmB,OAAO,GAAG,IAAIC,GAAG,CAAUjB,IAAI,CAACY,QAAQ,CAACM,IAAI,CAAC;IACpD,MAAMC,KAAK,GAAGxB,SAAS,CAACyB,MAAM,CAAEjB,CAAC,IAAKa,OAAO,CAACK,GAAG,CAAClB,CAAC,CAACC,MAAM,CAACW,GAAG,CAAC,CAAC;IAChE,IAAII,KAAK,CAACG,MAAM,KAAK,CAAC,EAAE;MACtB,OAAO,OAAO,IAAIC,kBAAU,CAAC;QAC3BC,MAAM,EAAE,oDAAoDxB,IAAI,CAACY,QAAQ,CAACM,IAAI,CAACO,IAAI,CAAC,IAAI,CAAC;OAC1F,CAAC;IACJ;IACA,MAAMC,QAAQ,GAAG,OAAOC,gBAAQ,CAACC,GAAG;IACpC,KAAK,MAAMzB,CAAC,IAAIgB,KAAK,EAAE;MACrBtB,MAAM,CAACgC,IAAI,CAAC;QACVd,GAAG,EAAEZ,CAAC,CAACC,MAAM,CAACW,GAAG;QACjBe,GAAG,EAAE3B,CAAC,CAAC2B,GAAG;QACVN,MAAM,EACJxB,IAAI,CAACY,QAAQ,CAACmB,OAAO,CAACC,GAAG,CAAC7B,CAAC,CAACC,MAAM,CAACW,GAAG,CAAC,IAAI1B,mBAAmB;QAChEqC;OACD,CAAC;IACJ;IACA/B,SAAS,GAAGA,SAAS,CAACyB,MAAM,CAAEjB,CAAC,IAAK,CAACa,OAAO,CAACK,GAAG,CAAClB,CAAC,CAACC,MAAM,CAACW,GAAG,CAAC,CAAC;EACjE;AACF,CAAC,CAAC;AAAAzB,OAAA,CAAAC,OAAA,GAAAA,OAAA","ignoreList":[]}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SinkRetryable = exports.SinkRejected = exports.SinkHalted = exports.LeaseLost = exports.LeaseHeld = exports.FeedWriteFailed = exports.FeedUnreadable = exports.FeedSyncStalled = exports.CursorSuperseded = void 0;
|
|
7
|
+
var _effect = require("effect");
|
|
8
|
+
/**
|
|
9
|
+
* The change feed's error vocabulary.
|
|
10
|
+
*
|
|
11
|
+
* ## The sink's three answers
|
|
12
|
+
*
|
|
13
|
+
* A sink classifies every failure into one of three, and the driver's whole
|
|
14
|
+
* behaviour on failure follows from which. The names borrow the meaning of
|
|
15
|
+
* `@kairos-es/automation`'s classification without depending on that package: a
|
|
16
|
+
* feed is not an automation (ADR-0019), and a dependency for three class names
|
|
17
|
+
* would make it look like one.
|
|
18
|
+
*
|
|
19
|
+
* - `SinkRetryable` — the sink could not take the batch NOW (a rate limit, a 5xx, a
|
|
20
|
+
* timeout). The cursor stays where it is and the batch is sent again after a
|
|
21
|
+
* back-off, never sooner than `retryAfter` when the sink names one.
|
|
22
|
+
* - `SinkRejected` — the sink will never take THESE records as they stand (too
|
|
23
|
+
* large, invalid). The named keys are parked with the sink's reasons, the rest of
|
|
24
|
+
* the batch is sent again without them, and the cursor moves past the whole page.
|
|
25
|
+
* A parked record is retried when its row next changes.
|
|
26
|
+
* - `SinkHalted` — something is wrong that retrying will not mend (bad
|
|
27
|
+
* credentials, a missing index). The worker stops, and supervision decides.
|
|
28
|
+
*
|
|
29
|
+
* ## The feed's own tables
|
|
30
|
+
*
|
|
31
|
+
* `FeedWriteFailed` and `FeedUnreadable` are infrastructure faults on the feed's
|
|
32
|
+
* own durable state, split by direction: a write that did not land, a read that
|
|
33
|
+
* could not be made. They are tagged errors rather than defects for the reason the
|
|
34
|
+
* `ProjectionStore` port gives for its own: the driver's supervision has to tell
|
|
35
|
+
* "back off and try again" from "give up", and only a value can carry that.
|
|
36
|
+
*
|
|
37
|
+
* ## The worker's lease and cursor
|
|
38
|
+
*
|
|
39
|
+
* `LeaseHeld`, `LeaseLost` and `CursorSuperseded` are the three ways the
|
|
40
|
+
* single-worker guarantee shows itself: another worker holds the lease, this
|
|
41
|
+
* worker's lease went while it ran, or the cursor it expected to advance is not
|
|
42
|
+
* the one stored (a rival moved it, or the lease changed hands).
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** The sink cannot take the batch now: keep the cursor and back off. */
|
|
46
|
+
class SinkRetryable extends /*#__PURE__*/_effect.Data.TaggedError('SinkRetryable') {
|
|
47
|
+
get message() {
|
|
48
|
+
return this.retryAfter === undefined ? this.reason : `${this.reason} (retry after ${_effect.Duration.format(this.retryAfter)})`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The sink refuses these records as they stand: park them, send the rest. */
|
|
52
|
+
exports.SinkRetryable = SinkRetryable;
|
|
53
|
+
class SinkRejected extends /*#__PURE__*/_effect.Data.TaggedError('SinkRejected') {
|
|
54
|
+
get message() {
|
|
55
|
+
return `the sink refused ${this.keys.length} record(s): ${this.keys.join(', ')}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Something retrying will not mend: stop the worker. */
|
|
59
|
+
exports.SinkRejected = SinkRejected;
|
|
60
|
+
class SinkHalted extends /*#__PURE__*/_effect.Data.TaggedError('SinkHalted') {
|
|
61
|
+
get message() {
|
|
62
|
+
return this.reason;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A write to the feed's own tables did not land. */
|
|
66
|
+
exports.SinkHalted = SinkHalted;
|
|
67
|
+
class FeedWriteFailed extends /*#__PURE__*/_effect.Data.TaggedError('FeedWriteFailed') {
|
|
68
|
+
get message() {
|
|
69
|
+
return this.reason;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** A read of the feed's own tables could not be made. */
|
|
73
|
+
exports.FeedWriteFailed = FeedWriteFailed;
|
|
74
|
+
class FeedUnreadable extends /*#__PURE__*/_effect.Data.TaggedError('FeedUnreadable') {
|
|
75
|
+
get message() {
|
|
76
|
+
return this.reason;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Another worker holds the lease for this feed and sink. */
|
|
80
|
+
exports.FeedUnreadable = FeedUnreadable;
|
|
81
|
+
class LeaseHeld extends /*#__PURE__*/_effect.Data.TaggedError('LeaseHeld') {
|
|
82
|
+
get message() {
|
|
83
|
+
return `another worker holds the lease for feed '${this.feed}' and sink '${this.sink}'`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** This worker's lease is no longer its own; it must stop sending at once. */
|
|
87
|
+
exports.LeaseHeld = LeaseHeld;
|
|
88
|
+
class LeaseLost extends /*#__PURE__*/_effect.Data.TaggedError('LeaseLost') {
|
|
89
|
+
get message() {
|
|
90
|
+
return `the lease for feed '${this.feed}' and sink '${this.sink}' was lost`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The cursor compare-and-set lost: the stored cursor is not the one expected, or
|
|
95
|
+
* the lease is not held by the worker that asked. Nothing the call carried landed.
|
|
96
|
+
*/
|
|
97
|
+
exports.LeaseLost = LeaseLost;
|
|
98
|
+
class CursorSuperseded extends /*#__PURE__*/_effect.Data.TaggedError('CursorSuperseded') {
|
|
99
|
+
get message() {
|
|
100
|
+
return `the cursor for feed '${this.feed}' and sink '${this.sink}' was superseded`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The sync worker gave up: it restarted `restarts` times in a row without moving
|
|
105
|
+
* its cursor once. `reason` is the failure that ended the last attempt.
|
|
106
|
+
*/
|
|
107
|
+
exports.CursorSuperseded = CursorSuperseded;
|
|
108
|
+
class FeedSyncStalled extends /*#__PURE__*/_effect.Data.TaggedError('FeedSyncStalled') {
|
|
109
|
+
get message() {
|
|
110
|
+
return `the sync of feed '${this.feed}' to sink '${this.sink}' made no progress in ${this.restarts} consecutive restarts`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.FeedSyncStalled = FeedSyncStalled;
|
|
114
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":["_effect","require","SinkRetryable","Data","TaggedError","message","retryAfter","undefined","reason","Duration","format","exports","SinkRejected","keys","length","join","SinkHalted","FeedWriteFailed","FeedUnreadable","LeaseHeld","feed","sink","LeaseLost","CursorSuperseded","FeedSyncStalled","restarts"],"sources":["../../src/errors.ts"],"sourcesContent":[null],"mappings":";;;;;;AAqCA,IAAAA,OAAA,GAAAC,OAAA;AArCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA;AACM,MAAOC,aAAc,sBAAQC,YAAI,CAACC,WAAW,CAAC,eAAe,CAIjE;EACA,IAAaC,OAAOA,CAAA;IAClB,OAAO,IAAI,CAACC,UAAU,KAAKC,SAAS,GAChC,IAAI,CAACC,MAAM,GACX,GAAG,IAAI,CAACA,MAAM,iBAAiBC,gBAAQ,CAACC,MAAM,CAAC,IAAI,CAACJ,UAAU,CAAC,GAAG;EACxE;;AAGF;AAAAK,OAAA,CAAAT,aAAA,GAAAA,aAAA;AACM,MAAOU,YAAa,sBAAQT,YAAI,CAACC,WAAW,CAAC,cAAc,CAI/D;EACA,IAAaC,OAAOA,CAAA;IAClB,OAAO,oBAAoB,IAAI,CAACQ,IAAI,CAACC,MAAM,eAAe,IAAI,CAACD,IAAI,CAACE,IAAI,CAAC,IAAI,CAAC,EAAE;EAClF;;AAGF;AAAAJ,OAAA,CAAAC,YAAA,GAAAA,YAAA;AACM,MAAOI,UAAW,sBAAQb,YAAI,CAACC,WAAW,CAAC,YAAY,CAE3D;EACA,IAAaC,OAAOA,CAAA;IAClB,OAAO,IAAI,CAACG,MAAM;EACpB;;AAGF;AAAAG,OAAA,CAAAK,UAAA,GAAAA,UAAA;AACM,MAAOC,eAAgB,sBAAQd,YAAI,CAACC,WAAW,CAAC,iBAAiB,CAGrE;EACA,IAAaC,OAAOA,CAAA;IAClB,OAAO,IAAI,CAACG,MAAM;EACpB;;AAGF;AAAAG,OAAA,CAAAM,eAAA,GAAAA,eAAA;AACM,MAAOC,cAAe,sBAAQf,YAAI,CAACC,WAAW,CAAC,gBAAgB,CAGnE;EACA,IAAaC,OAAOA,CAAA;IAClB,OAAO,IAAI,CAACG,MAAM;EACpB;;AAGF;AAAAG,OAAA,CAAAO,cAAA,GAAAA,cAAA;AACM,MAAOC,SAAU,sBAAQhB,YAAI,CAACC,WAAW,CAAC,WAAW,CAAa;EACtE,IAAaC,OAAOA,CAAA;IAClB,OAAO,4CAA4C,IAAI,CAACe,IAAI,eAAe,IAAI,CAACC,IAAI,GAAG;EACzF;;AAGF;AAAAV,OAAA,CAAAQ,SAAA,GAAAA,SAAA;AACM,MAAOG,SAAU,sBAAQnB,YAAI,CAACC,WAAW,CAAC,WAAW,CAAa;EACtE,IAAaC,OAAOA,CAAA;IAClB,OAAO,uBAAuB,IAAI,CAACe,IAAI,eAAe,IAAI,CAACC,IAAI,YAAY;EAC7E;;AAGF;;;;AAAAV,OAAA,CAAAW,SAAA,GAAAA,SAAA;AAIM,MAAOC,gBAAiB,sBAAQpB,YAAI,CAACC,WAAW,CACpD,kBAAkB,CACP;EACX,IAAaC,OAAOA,CAAA;IAClB,OAAO,wBAAwB,IAAI,CAACe,IAAI,eAAe,IAAI,CAACC,IAAI,kBAAkB;EACpF;;AAGF;;;;AAAAV,OAAA,CAAAY,gBAAA,GAAAA,gBAAA;AAIM,MAAOC,eAAgB,sBAAQrB,YAAI,CAACC,WAAW,CAAC,iBAAiB,CAKrE;EACA,IAAaC,OAAOA,CAAA;IAClB,OAAO,qBAAqB,IAAI,CAACe,IAAI,cAAc,IAAI,CAACC,IAAI,yBAAyB,IAAI,CAACI,QAAQ,uBAAuB;EAC3H","ignoreList":[]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SinkName = exports.LeaseHolder = exports.Generation = exports.FeedSeq = exports.FeedName = exports.FeedKey = exports.FEED_ORIGIN = void 0;
|
|
7
|
+
var _effect = require("effect");
|
|
8
|
+
/**
|
|
9
|
+
* The change feed's branded identifiers.
|
|
10
|
+
*
|
|
11
|
+
* Every one of them is a branded `Schema`, as every domain identifier in this
|
|
12
|
+
* repository is, so a raw string or `bigint` cannot stand in for one by accident.
|
|
13
|
+
* Two of them are worth a word, because they look like things the read side
|
|
14
|
+
* already has and are deliberately not those things.
|
|
15
|
+
*
|
|
16
|
+
* - `FeedSeq` is NOT a `Position`. A `Position` is a place in the global event log,
|
|
17
|
+
* and one event may fan out into many feed rows (a teacher renamed, fifty courses
|
|
18
|
+
* touched). Give those fifty rows the event's position as their cursor and they
|
|
19
|
+
* all share one value, so a page boundary that falls among them loses the rest of
|
|
20
|
+
* them: the next page asks for rows strictly after a value the lost ones also
|
|
21
|
+
* carry. A feed-local sequence gives every row a cursor value of its own. The
|
|
22
|
+
* event position is still kept on each row, as `eventPosition`, for the one
|
|
23
|
+
* question it answers — "is the sink at least as fresh as P?" — and is never a
|
|
24
|
+
* cursor.
|
|
25
|
+
* - `Generation` is NOT a counter a caller reads meaning into. It is compared for
|
|
26
|
+
* EQUALITY only: a sink whose cursor was saved at a generation other than the
|
|
27
|
+
* feed's current one resyncs. That is what lets the in-memory feed draw a fresh
|
|
28
|
+
* generation from `Random` on every boot while a durable feed counts upwards, and
|
|
29
|
+
* both satisfy the one rule the driver needs.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The name of one change feed. A feed is bound to ONE materialisation, because a
|
|
34
|
+
* feed sequence taken inside a commit is ascending in commit order only while one
|
|
35
|
+
* transaction at a time writes that feed — which the read side gives per
|
|
36
|
+
* materialisation and no wider.
|
|
37
|
+
*/
|
|
38
|
+
const FeedName = exports.FeedName = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isNonEmpty()), /*#__PURE__*/_effect.Schema.brand('FeedName'));
|
|
39
|
+
/** A record's key within a feed, and the sink's object id for that record. */
|
|
40
|
+
const FeedKey = exports.FeedKey = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isNonEmpty()), /*#__PURE__*/_effect.Schema.brand('FeedKey'));
|
|
41
|
+
/**
|
|
42
|
+
* The feed-local cursor: unique, and ascending in commit order. Compared, never
|
|
43
|
+
* arithmetic'd, and never assumed contiguous — a sequence shared by several feeds
|
|
44
|
+
* leaves gaps in each, and a coalesced row gives up its old value when it moves.
|
|
45
|
+
*/
|
|
46
|
+
const FeedSeq = exports.FeedSeq = /*#__PURE__*/_effect.Schema.BigInt.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isGreaterThanOrEqualToBigInt(0n)), /*#__PURE__*/_effect.Schema.brand('FeedSeq'));
|
|
47
|
+
/** The sentinel feed sequence meaning "before the first row". */
|
|
48
|
+
const FEED_ORIGIN = exports.FEED_ORIGIN = /*#__PURE__*/FeedSeq.make(0n);
|
|
49
|
+
/**
|
|
50
|
+
* The feed's generation: it moves on every reset of the materialisation the feed
|
|
51
|
+
* is bound to, and on every boot of an ephemeral feed. Compared for equality only;
|
|
52
|
+
* see the module doc.
|
|
53
|
+
*/
|
|
54
|
+
const Generation = exports.Generation = /*#__PURE__*/_effect.Schema.BigInt.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isGreaterThanOrEqualToBigInt(0n)), /*#__PURE__*/_effect.Schema.brand('Generation'));
|
|
55
|
+
/** The name of one sink. A feed keeps one cursor per sink. */
|
|
56
|
+
const SinkName = exports.SinkName = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isNonEmpty()), /*#__PURE__*/_effect.Schema.brand('SinkName'));
|
|
57
|
+
/**
|
|
58
|
+
* Who holds a sync worker's lease. Issued by the `FeedSyncStore` when the lease is
|
|
59
|
+
* taken, and checked again by every cursor compare-and-set, so a worker that has
|
|
60
|
+
* lost its lease cannot move the cursor even if it is still running.
|
|
61
|
+
*/
|
|
62
|
+
const LeaseHolder = exports.LeaseHolder = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isNonEmpty()), /*#__PURE__*/_effect.Schema.brand('LeaseHolder'));
|
|
63
|
+
//# sourceMappingURL=identifiers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identifiers.js","names":["_effect","require","FeedName","exports","Schema","String","pipe","check","isNonEmpty","brand","FeedKey","FeedSeq","BigInt","isGreaterThanOrEqualToBigInt","FEED_ORIGIN","make","Generation","SinkName","LeaseHolder"],"sources":["../../src/identifiers.ts"],"sourcesContent":[null],"mappings":";;;;;;AAuBA,IAAAA,OAAA,GAAAC,OAAA;AAvBA;;;;;;;;;;;;;;;;;;;;;;;;AAyBA;;;;;;AAMO,MAAMC,QAAQ,GAAAC,OAAA,CAAAD,QAAA,gBAAGE,cAAM,CAACC,MAAM,CAACC,IAAI,cACxCF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACI,UAAU,EAAE,CAAC,eACjCJ,cAAM,CAACK,KAAK,CAAC,UAAU,CAAC,CACzB;AAGD;AACO,MAAMC,OAAO,GAAAP,OAAA,CAAAO,OAAA,gBAAGN,cAAM,CAACC,MAAM,CAACC,IAAI,cACvCF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACI,UAAU,EAAE,CAAC,eACjCJ,cAAM,CAACK,KAAK,CAAC,SAAS,CAAC,CACxB;AAGD;;;;;AAKO,MAAME,OAAO,GAAAR,OAAA,CAAAQ,OAAA,gBAAGP,cAAM,CAACQ,MAAM,CAACN,IAAI,cACvCF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACS,4BAA4B,CAAC,EAAE,CAAC,CAAC,eACrDT,cAAM,CAACK,KAAK,CAAC,SAAS,CAAC,CACxB;AAGD;AACO,MAAMK,WAAW,GAAAX,OAAA,CAAAW,WAAA,gBAAYH,OAAO,CAACI,IAAI,CAAC,EAAE,CAAC;AAEpD;;;;;AAKO,MAAMC,UAAU,GAAAb,OAAA,CAAAa,UAAA,gBAAGZ,cAAM,CAACQ,MAAM,CAACN,IAAI,cAC1CF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACS,4BAA4B,CAAC,EAAE,CAAC,CAAC,eACrDT,cAAM,CAACK,KAAK,CAAC,YAAY,CAAC,CAC3B;AAGD;AACO,MAAMQ,QAAQ,GAAAd,OAAA,CAAAc,QAAA,gBAAGb,cAAM,CAACC,MAAM,CAACC,IAAI,cACxCF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACI,UAAU,EAAE,CAAC,eACjCJ,cAAM,CAACK,KAAK,CAAC,UAAU,CAAC,CACzB;AAGD;;;;;AAKO,MAAMS,WAAW,GAAAf,OAAA,CAAAe,WAAA,gBAAGd,cAAM,CAACC,MAAM,CAACC,IAAI,cAC3CF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACI,UAAU,EAAE,CAAC,eACjCJ,cAAM,CAACK,KAAK,CAAC,aAAa,CAAC,CAC5B","ignoreList":[]}
|