@kairos-es/read 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 +538 -0
- package/dist/cjs/EventLogDurability.js +184 -0
- package/dist/cjs/EventLogDurability.js.map +1 -0
- package/dist/cjs/ProjectionRunner.js +478 -0
- package/dist/cjs/ProjectionRunner.js.map +1 -0
- package/dist/cjs/ProjectionStore.js +233 -0
- package/dist/cjs/ProjectionStore.js.map +1 -0
- package/dist/cjs/foldIntoRef.js +36 -0
- package/dist/cjs/foldIntoRef.js.map +1 -0
- package/dist/cjs/inMemoryProjectionStore.js +138 -0
- package/dist/cjs/inMemoryProjectionStore.js.map +1 -0
- package/dist/cjs/index.js +128 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/projectionWiringFault.js +532 -0
- package/dist/cjs/projectionWiringFault.js.map +1 -0
- package/dist/cjs/runProjection.js +117 -0
- package/dist/cjs/runProjection.js.map +1 -0
- package/dist/cjs/runProjections.js +144 -0
- package/dist/cjs/runProjections.js.map +1 -0
- package/dist/cjs/superviseOnProgress.js +580 -0
- package/dist/cjs/superviseOnProgress.js.map +1 -0
- package/dist/cjs/testing.js +143 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/dts/EventLogDurability.d.ts +182 -0
- package/dist/dts/EventLogDurability.d.ts.map +1 -0
- package/dist/dts/ProjectionRunner.d.ts +557 -0
- package/dist/dts/ProjectionRunner.d.ts.map +1 -0
- package/dist/dts/ProjectionStore.d.ts +475 -0
- package/dist/dts/ProjectionStore.d.ts.map +1 -0
- package/dist/dts/foldIntoRef.d.ts +39 -0
- package/dist/dts/foldIntoRef.d.ts.map +1 -0
- package/dist/dts/inMemoryProjectionStore.d.ts +11 -0
- package/dist/dts/inMemoryProjectionStore.d.ts.map +1 -0
- package/dist/dts/index.d.ts +185 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/projectionWiringFault.d.ts +260 -0
- package/dist/dts/projectionWiringFault.d.ts.map +1 -0
- package/dist/dts/runProjection.d.ts +185 -0
- package/dist/dts/runProjection.d.ts.map +1 -0
- package/dist/dts/runProjections.d.ts +480 -0
- package/dist/dts/runProjections.d.ts.map +1 -0
- package/dist/dts/superviseOnProgress.d.ts +587 -0
- package/dist/dts/superviseOnProgress.d.ts.map +1 -0
- package/dist/dts/testing.d.ts +207 -0
- package/dist/dts/testing.d.ts.map +1 -0
- package/dist/esm/EventLogDurability.js +175 -0
- package/dist/esm/EventLogDurability.js.map +1 -0
- package/dist/esm/ProjectionRunner.js +468 -0
- package/dist/esm/ProjectionRunner.js.map +1 -0
- package/dist/esm/ProjectionStore.js +223 -0
- package/dist/esm/ProjectionStore.js.map +1 -0
- package/dist/esm/foldIntoRef.js +29 -0
- package/dist/esm/foldIntoRef.js.map +1 -0
- package/dist/esm/inMemoryProjectionStore.js +131 -0
- package/dist/esm/inMemoryProjectionStore.js.map +1 -0
- package/dist/esm/index.js +185 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/projectionWiringFault.js +524 -0
- package/dist/esm/projectionWiringFault.js.map +1 -0
- package/dist/esm/runProjection.js +109 -0
- package/dist/esm/runProjection.js.map +1 -0
- package/dist/esm/runProjections.js +137 -0
- package/dist/esm/runProjections.js.map +1 -0
- package/dist/esm/superviseOnProgress.js +571 -0
- package/dist/esm/superviseOnProgress.js.map +1 -0
- package/dist/esm/testing.js +133 -0
- package/dist/esm/testing.js.map +1 -0
- package/package.json +41 -0
- package/src/EventLogDurability.ts +201 -0
- package/src/ProjectionRunner.ts +923 -0
- package/src/ProjectionStore.ts +528 -0
- package/src/foldIntoRef.ts +63 -0
- package/src/inMemoryProjectionStore.ts +163 -0
- package/src/index.ts +218 -0
- package/src/projectionWiringFault.ts +694 -0
- package/src/runProjection.ts +270 -0
- package/src/runProjections.ts +623 -0
- package/src/superviseOnProgress.ts +897 -0
- package/src/testing.ts +290 -0
- package/testing/package.json +6 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.storedCheckpoint = exports.runProjectionUntil = exports.lastPosition = exports.awaitCheckpoint = void 0;
|
|
7
|
+
var _effect = require("effect");
|
|
8
|
+
var _runProjection = require("./runProjection.js");
|
|
9
|
+
/**
|
|
10
|
+
* How often the waiting fibre re-reads the checkpoint. Small, because the wait
|
|
11
|
+
* ends on the FIRST read that has got there and a coarse interval would add its
|
|
12
|
+
* own latency to every case; not zero, because each read is a real store
|
|
13
|
+
* operation (a SQL round trip on a durable backend).
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_POLL_INTERVAL = '2 millis';
|
|
16
|
+
/**
|
|
17
|
+
* The ceiling on convergence. Deliberately far larger than any well-behaved case
|
|
18
|
+
* needs: it exists to turn a genuine hang into a FAILED assertion naming the
|
|
19
|
+
* position that was never reached, not to police how fast a runner is on a loaded
|
|
20
|
+
* machine. Backends whose live tail has its own poll interval — Postgres wakes on
|
|
21
|
+
* NOTIFY but GUARANTEES delivery by polling — should raise it to several of those
|
|
22
|
+
* intervals rather than lower it.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_TIMEOUT = '10 seconds';
|
|
25
|
+
/**
|
|
26
|
+
* The stored checkpoint of a read model's bound view store, right now.
|
|
27
|
+
*
|
|
28
|
+
* `orDie` because a `ProjectionStoreError` means the test's own fixture cannot
|
|
29
|
+
* read its own checkpoint. The port puts that on the ERROR channel for the
|
|
30
|
+
* runner's supervisor, which has to tell retry-with-backoff from give-up
|
|
31
|
+
* (ADR-0007) — but a test has no supervisor and nothing to retry, so surfacing it
|
|
32
|
+
* as a defect keeps a broken fixture clearly distinct from the case's own
|
|
33
|
+
* assertion failing, and keeps every signature here `E = never` so a case never
|
|
34
|
+
* has to widen its own error channel to look at a view.
|
|
35
|
+
*/
|
|
36
|
+
const storedCheckpoint = store => _effect.Effect.orDie(store.readCheckpoint);
|
|
37
|
+
/**
|
|
38
|
+
* Wait until the stored checkpoint has reached `target`, then return it.
|
|
39
|
+
*
|
|
40
|
+
* The read side's answer to "has the projection caught up yet?", and the reason
|
|
41
|
+
* a read-model test needs no sleep: it polls the OBSERVABLE state, so it ends the
|
|
42
|
+
* instant the runner has actually got there and no sooner. Comparison is `>=`
|
|
43
|
+
* rather than `===` and that is load-bearing — positions are strictly increasing
|
|
44
|
+
* but NOT gapless, so a target derived from an append may be overshot by a batch
|
|
45
|
+
* that carried later events too, and an equality test would then wait for a
|
|
46
|
+
* position the log will never store.
|
|
47
|
+
*
|
|
48
|
+
* A KEYED store rather than a `(store, key)` pair, matching the shape the package
|
|
49
|
+
* settled on everywhere else: a read model's materialisation carries
|
|
50
|
+
* `forKey(store, key)` INSTEAD of a key field, so "one materialisation, one key" is
|
|
51
|
+
* structural rather than conventional, and the same argument applies to observing
|
|
52
|
+
* one. There is no key to transpose, the timeout message names `store.key` and
|
|
53
|
+
* therefore cannot name a key the poll did not use, and `readModel.store` is
|
|
54
|
+
* already exactly the right argument. A caller holding the multi-key port writes
|
|
55
|
+
* `forKey(store, key)`, which is the library's own one-line adapter and is
|
|
56
|
+
* `store.readCheckpoint(key)` with a `suspend` around it — so a suite deliberately
|
|
57
|
+
* observing through the RAW port (as `read-postgres`' live-log cases do, to keep
|
|
58
|
+
* "the façade wrote where the port reads" an observation rather than an assumption)
|
|
59
|
+
* loses nothing.
|
|
60
|
+
*
|
|
61
|
+
* A read model materialised N ways (`runProjections`) is N of these observations,
|
|
62
|
+
* one per store, and there is deliberately no plural helper for it. Under one
|
|
63
|
+
* `ProjectionId` and the default partition the N keys are IDENTICAL — a key names a
|
|
64
|
+
* cursor within a store — so the STORE is what distinguishes the materialisations,
|
|
65
|
+
* and each runner carries its own: `awaitCheckpoint(runners[i].store, target)` is
|
|
66
|
+
* already the per-entry observation, over the very binding the call made. So a plural
|
|
67
|
+
* helper would take nothing a caller does not already hold, and the N waits are one
|
|
68
|
+
* `Effect.all` over the runners it was handed — concurrently, since waiting on them
|
|
69
|
+
* in turn would prove only that each converges once the others have.
|
|
70
|
+
*
|
|
71
|
+
* Exceeding the timeout is a DEFECT, so this stays `E = never` and a case need
|
|
72
|
+
* not thread a timeout error it has no intention of handling. It is the right
|
|
73
|
+
* classification anyway: a projection that never converged is a broken test or a
|
|
74
|
+
* broken library, never an outcome to assert on.
|
|
75
|
+
*/
|
|
76
|
+
exports.storedCheckpoint = storedCheckpoint;
|
|
77
|
+
const awaitCheckpoint = (store, target, options) => _effect.Effect.repeat(_effect.Effect.zipRight(_effect.Effect.sleep(options?.pollInterval ?? DEFAULT_POLL_INTERVAL), storedCheckpoint(store)), {
|
|
78
|
+
until: stored => stored >= target
|
|
79
|
+
}).pipe(_effect.Effect.timeoutFail({
|
|
80
|
+
duration: options?.timeout ?? DEFAULT_TIMEOUT,
|
|
81
|
+
onTimeout: () => new Error(`awaitCheckpoint: the checkpoint for projection ` + `'${store.key.projection}' (partition '${store.key.partition}') ` + `never reached position ${target}`)
|
|
82
|
+
}), _effect.Effect.orDie);
|
|
83
|
+
/**
|
|
84
|
+
* Start the projection maintaining `readModel` and wait until its checkpoint has
|
|
85
|
+
* reached `target` — "run this until it has consumed the log", as one call.
|
|
86
|
+
*
|
|
87
|
+
* This is the surface the read side was missing. `runProjection` returns once the
|
|
88
|
+
* daemon is FORKED, which is the only honest thing it can do, so every read-model
|
|
89
|
+
* test is otherwise a two-step dance the author has to know to write; this pairs
|
|
90
|
+
* the two steps so that a case's next line can assert on the view.
|
|
91
|
+
*
|
|
92
|
+
* It returns the `ProjectionRunner` rather than swallowing it, because everything
|
|
93
|
+
* a case might do next needs the handle: `Fiber.poll` it (is the daemon still
|
|
94
|
+
* up?), `Fiber.interrupt` it early, or await it. The scope is still the teardown
|
|
95
|
+
* mechanism — this forks into the CALLER's `Scope`, exactly as `runProjection`
|
|
96
|
+
* does, so closing that scope interrupts the daemon and waits for it.
|
|
97
|
+
*
|
|
98
|
+
* ## What it deliberately is NOT
|
|
99
|
+
*
|
|
100
|
+
* A TEST helper, not a production freshness API. It reports nothing about lag, it
|
|
101
|
+
* cannot enumerate checkpoints, and no consumer should reach for it to make a
|
|
102
|
+
* request wait for its own write to be projected — a read model is eventually
|
|
103
|
+
* consistent by construction and a `waitUntilProcessed` on the hot path would be
|
|
104
|
+
* a way to pretend otherwise. That is why it lives behind `/testing` and not on
|
|
105
|
+
* the package's main entry. Where a request genuinely must see its own write, the
|
|
106
|
+
* honest answer is not to wait on a MAINTAINED materialisation at all: fold the log
|
|
107
|
+
* at query time with `modelAtHead` from `@kairos-es/codec`, which is consistent as
|
|
108
|
+
* of the `head` its read returned and therefore never behind a checkpoint.
|
|
109
|
+
*
|
|
110
|
+
* It also does not race the daemon's fibre against the wait. A projection that
|
|
111
|
+
* gives up fails `ProjectionStalled`, and it would be possible to surface that
|
|
112
|
+
* here instead of timing out — but the daemon's failure and the convergence
|
|
113
|
+
* ceiling are two independent clocks, and a helper whose diagnosis depended on
|
|
114
|
+
* which fired first is a worse instrument than one that always says the same
|
|
115
|
+
* thing. A case that wants the stall itself has the fibre returned above, and
|
|
116
|
+
* `onStalled` besides.
|
|
117
|
+
*/
|
|
118
|
+
exports.awaitCheckpoint = awaitCheckpoint;
|
|
119
|
+
const runProjectionUntil = (readModel, target, options) => _effect.Effect.tap((0, _runProjection.runProjection)(readModel, options), () =>
|
|
120
|
+
// The read model's OWN bound store, so the cursor being waited on is
|
|
121
|
+
// necessarily the cursor the runner advances. A `store` parameter here would
|
|
122
|
+
// reintroduce exactly the pair `KeyedProjectionStore` exists to abolish.
|
|
123
|
+
awaitCheckpoint(readModel.store, target, options));
|
|
124
|
+
/**
|
|
125
|
+
* The LAST position of a sequence — the convergence target, derived from what
|
|
126
|
+
* `append` actually reported.
|
|
127
|
+
*
|
|
128
|
+
* The companion to the two waits above, and the reason it is a helper rather than
|
|
129
|
+
* `positions[positions.length - 1]`: a fixture that appended nothing would
|
|
130
|
+
* otherwise yield `undefined`, and a wait for `undefined` is either a type error
|
|
131
|
+
* at best or a vacuous assertion at worst. Failing loudly at the point the
|
|
132
|
+
* fixture is wrong is worth four lines.
|
|
133
|
+
*
|
|
134
|
+
* Targets are built this way — from the positions the log HANDED OUT — rather
|
|
135
|
+
* than from a literal `1n..Nn`, because positions are strictly increasing and NOT
|
|
136
|
+
* gapless: a rolled-back append burns an id, and a tag-narrowed subscription
|
|
137
|
+
* never delivers the positions it does not match. Nothing here does arithmetic on
|
|
138
|
+
* a position.
|
|
139
|
+
*/
|
|
140
|
+
exports.runProjectionUntil = runProjectionUntil;
|
|
141
|
+
const lastPosition = positions => _effect.Option.getOrThrowWith(_effect.Array.last(positions), () => new Error('test fixture: no events were appended'));
|
|
142
|
+
exports.lastPosition = lastPosition;
|
|
143
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":["_effect","require","_runProjection","DEFAULT_POLL_INTERVAL","DEFAULT_TIMEOUT","storedCheckpoint","store","Effect","orDie","readCheckpoint","exports","awaitCheckpoint","target","options","repeat","zipRight","sleep","pollInterval","until","stored","pipe","timeoutFail","duration","timeout","onTimeout","Error","key","projection","partition","runProjectionUntil","readModel","tap","runProjection","lastPosition","positions","Option","getOrThrowWith","Arr","last"],"sources":["../../src/testing.ts"],"sourcesContent":[null],"mappings":";;;;;;AAuEA,IAAAA,OAAA,GAAAC,OAAA;AAOA,IAAAC,cAAA,GAAAD,OAAA;AAEA;;;;;;AAMA,MAAME,qBAAqB,GAA2B,UAAU;AAEhE;;;;;;;;AAQA,MAAMC,eAAe,GAA2B,YAAY;AAsB5D;;;;;;;;;;;AAWO,MAAMC,gBAAgB,GAC3BC,KAA2B,IACCC,cAAM,CAACC,KAAK,CAACF,KAAK,CAACG,cAAc,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAC,OAAA,CAAAL,gBAAA,GAAAA,gBAAA;AAuCO,MAAMM,eAAe,GAAGA,CAC7BL,KAA2B,EAC3BM,MAAgB,EAChBC,OAAgC,KAEhCN,cAAM,CAACO,MAAM,CACXP,cAAM,CAACQ,QAAQ,CACbR,cAAM,CAACS,KAAK,CAACH,OAAO,EAAEI,YAAY,IAAId,qBAAqB,CAAC,EAC5DE,gBAAgB,CAACC,KAAK,CAAC,CACxB,EACD;EAAEY,KAAK,EAAGC,MAAM,IAAKA,MAAM,IAAIP;AAAM,CAAE,CACxC,CAACQ,IAAI,CACJb,cAAM,CAACc,WAAW,CAAC;EACjBC,QAAQ,EAAET,OAAO,EAAEU,OAAO,IAAInB,eAAe;EAC7CoB,SAAS,EAAEA,CAAA,KACT,IAAIC,KAAK,CACP,iDAAiD,GAC/C,IAAInB,KAAK,CAACoB,GAAG,CAACC,UAAU,iBAAiBrB,KAAK,CAACoB,GAAG,CAACE,SAAS,KAAK,GACjE,0BAA0BhB,MAAM,EAAE;CAEzC,CAAC,EACFL,cAAM,CAACC,KAAK,CACb;AAeH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAE,OAAA,CAAAC,eAAA,GAAAA,eAAA;AAmCO,MAAMkB,kBAAkB,GAAGA,CAKhCC,SAA6B,EAC7BlB,MAAgB,EAChBC,OAAmC,KAWnCN,cAAM,CAACwB,GAAG,CAAC,IAAAC,4BAAa,EAACF,SAAS,EAAEjB,OAAO,CAAC,EAAE;AAC5C;AACA;AACA;AACAF,eAAe,CAACmB,SAAS,CAACxB,KAAK,EAAEM,MAAM,EAAEC,OAAO,CAAC,CAClD;AAEH;;;;;;;;;;;;;;;;AAAAH,OAAA,CAAAmB,kBAAA,GAAAA,kBAAA;AAgBO,MAAMI,YAAY,GAAIC,SAAkC,IAC7DC,cAAM,CAACC,cAAc,CACnBC,aAAG,CAACC,IAAI,CAACJ,SAAS,CAAC,EACnB,MAAM,IAAIT,KAAK,CAAC,uCAAuC,CAAC,CACzD;AAAAf,OAAA,CAAAuB,YAAA,GAAAA,YAAA","ignoreList":[]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EventLogDurability` — the R2 input the EVENT LOG contributes: how far the log a
|
|
3
|
+
* projection subscribes to survives a restart, declared by the application that
|
|
4
|
+
* chose the store's `Layer` and read from context in ONE place — the
|
|
5
|
+
* `prepareProjection` phase both runner entry points share, which is also the gate's
|
|
6
|
+
* only caller.
|
|
7
|
+
*
|
|
8
|
+
* ## The rule: R2, durability ordering
|
|
9
|
+
*
|
|
10
|
+
* This is R2's home, and every other mention in the read side points here rather
|
|
11
|
+
* than restating it.
|
|
12
|
+
*
|
|
13
|
+
* Checkpoint durability must not EXCEED event-log durability. The comparison has
|
|
14
|
+
* two halves, each declared where it is known: the VIEW store carries its own
|
|
15
|
+
* class on the `ProjectionStore` it implements, and the LOG's class is this tag. A
|
|
16
|
+
* DURABLE view over an EPHEMERAL log is the one illegal pairing of the four.
|
|
17
|
+
*
|
|
18
|
+
* It is illegal because the failure is SILENT. The checkpoint survives a restart
|
|
19
|
+
* the log does not, so on the next boot `subscribe(query, checkpoint)` returns
|
|
20
|
+
* nothing against a re-emptied log: no fibre dies, no error reaches any channel,
|
|
21
|
+
* no log line appears, and the view simply sits frozen and permanently stale while
|
|
22
|
+
* continuing to serve reads. Nothing at runtime is placed to notice it, which is
|
|
23
|
+
* why the two declarations are compared BEFORE anything is forked.
|
|
24
|
+
*
|
|
25
|
+
* `durabilityOrderingFault` at the foot of this module is that comparison — the
|
|
26
|
+
* CHECK beside the argument for it, so a reader who comes here for the rule finds
|
|
27
|
+
* the code that enforces it and the sentence an author meets when they break it,
|
|
28
|
+
* rather than a restatement of the rule four hundred lines away in the pipeline
|
|
29
|
+
* module. It is the first PER-ENTRY rung of the five rules `projectionWiringFault`
|
|
30
|
+
* chains, behind only the set-level collision rung, and is judged for every
|
|
31
|
+
* materialisation in the call; that module owns the argument for the whole order.
|
|
32
|
+
*
|
|
33
|
+
* The asymmetry is deliberate: getting the declaration wrong in the SAFE direction
|
|
34
|
+
* — saying `'ephemeral'` over a genuinely durable log — costs nothing but a durable
|
|
35
|
+
* view, so nothing checks it. Only the unsafe direction is rejected.
|
|
36
|
+
*
|
|
37
|
+
* ## Why it is DECLARED at all
|
|
38
|
+
*
|
|
39
|
+
* `DcbEventStore` deliberately exposes no durability, and that opacity is a
|
|
40
|
+
* feature rather than an omission: a runner never learns which engine it is
|
|
41
|
+
* running against, which is exactly what lets one projection sit over the
|
|
42
|
+
* in-memory log in a test and over Postgres in production with nothing in between
|
|
43
|
+
* changing. The store contract is untouched by the read side. But R2 has to know
|
|
44
|
+
* the log's class, so the only honest source is the application that picked the
|
|
45
|
+
* layer.
|
|
46
|
+
*
|
|
47
|
+
* ## Why a `Context.Tag` rather than a field on `ReadModel`
|
|
48
|
+
*
|
|
49
|
+
* ONE log, ONE declaration. The fact being declared is a property of the EVENT
|
|
50
|
+
* LOG, and every read model in a deployment reads the same log, so authoring it
|
|
51
|
+
* per read model states one fact N times at N sites — and N copies can DISAGREE.
|
|
52
|
+
* One read model declaring `'durable'` beside a neighbour declaring `'ephemeral'`
|
|
53
|
+
* over the identical `DcbEventStore` is not a type error, and nothing anywhere
|
|
54
|
+
* could notice: `runProjection` only ever sees the one pair it was handed, so
|
|
55
|
+
* there is no vantage point from which the contradiction is even visible. The read
|
|
56
|
+
* side's own scaling story is one runner per materialisation — horizontal scale by
|
|
57
|
+
* functional decomposition, never by sharding one read model's stream (ADR-0007) —
|
|
58
|
+
* so N > 1 is the EXPECTED case, and the unsafe direction is then reachable by a
|
|
59
|
+
* copy-paste that gets one of N wrong. N materialisations of ONE read model
|
|
60
|
+
* (`runProjections`) only sharpen that: their whole point is that the view stores
|
|
61
|
+
* differ, so their R2 verdicts differ too and each is judged separately against this
|
|
62
|
+
* one declaration — which is precisely why the LOG's half must not be authored
|
|
63
|
+
* alongside them.
|
|
64
|
+
*
|
|
65
|
+
* Provided beside the store layer, the declaration sits with the choice it
|
|
66
|
+
* describes: `Layer.merge(DcbEventStoreInMemory, EphemeralEventLog)` names the log
|
|
67
|
+
* and its durability class in one expression, and there is exactly one of it
|
|
68
|
+
* however many runners the graph goes on to carry.
|
|
69
|
+
*
|
|
70
|
+
* The cost is real and taken deliberately: this is a REQUIRED service in the `R` of
|
|
71
|
+
* `runProjection`, `projectionLayer` and `runProjections` alike, which every consumer
|
|
72
|
+
* must provide — and most costly to forget at `runProjections`, where one missing
|
|
73
|
+
* layer refuses N materialisations at once. Defaulting it would
|
|
74
|
+
* defeat the point in both directions — defaulting to `'ephemeral'` would make the
|
|
75
|
+
* PRODUCTION wiring the one you have to remember, and defaulting to `'durable'`
|
|
76
|
+
* would turn a forgotten declaration into the silently frozen view R2 exists to
|
|
77
|
+
* prevent.
|
|
78
|
+
*
|
|
79
|
+
* ## Why the store layers do not provide it themselves
|
|
80
|
+
*
|
|
81
|
+
* `DcbEventStoreInMemory` could plausibly provide `'ephemeral'` on its own and make
|
|
82
|
+
* the all-in-memory case correct by default. It deliberately does not, for three
|
|
83
|
+
* reasons that outweigh the ergonomics.
|
|
84
|
+
*
|
|
85
|
+
* This is a READ-SIDE tag, and `@kairos-es/core` knows nothing of the read side —
|
|
86
|
+
* `read` peers `core`, not the other way round — so core would have to DEFINE the
|
|
87
|
+
* tag for its layer to provide it. That puts a read-side rule inside the store
|
|
88
|
+
* contract's own package, which is precisely the coupling the contract's
|
|
89
|
+
* durability-opacity exists to avoid.
|
|
90
|
+
*
|
|
91
|
+
* It would also move the declaration from the APPLICATION to the BACKEND. Then a
|
|
92
|
+
* backend that got its own class wrong (a log that persists only on a flag, say)
|
|
93
|
+
* would be wrong for every read model at once with no wiring site left to correct
|
|
94
|
+
* it, and the whole reason R2 keys on a declaration rather than on the store is
|
|
95
|
+
* that only the application knows.
|
|
96
|
+
*
|
|
97
|
+
* And two providers of one tag is not an error in a layer graph — it is resolved
|
|
98
|
+
* by merge order. An application that declared `DurableEventLog` beside a store
|
|
99
|
+
* layer declaring `'ephemeral'` would get whichever merge happened to win, silently,
|
|
100
|
+
* which is a worse failure than the one being fixed. The single token
|
|
101
|
+
* `EphemeralEventLog` costs is not worth any of that.
|
|
102
|
+
*/
|
|
103
|
+
import { Context, Layer } from 'effect';
|
|
104
|
+
import type { Durability } from './ProjectionStore.js';
|
|
105
|
+
declare const EventLogDurability_base: Context.TagClass<EventLogDurability, "@kairos-es/read/EventLogDurability", Durability>;
|
|
106
|
+
/**
|
|
107
|
+
* The event log's durability class, as the application declares it.
|
|
108
|
+
*
|
|
109
|
+
* The service value is the bare `Durability` rather than a record wrapping it:
|
|
110
|
+
* the tag's name says exactly what it holds, so `yield* EventLogDurability` is the
|
|
111
|
+
* whole read, and `Layer.succeed(EventLogDurability, 'durable')` is the whole
|
|
112
|
+
* declaration for anyone not reaching for the two layers below.
|
|
113
|
+
*/
|
|
114
|
+
export declare class EventLogDurability extends EventLogDurability_base {
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Declare that the event log OUTLIVES the process — the production wiring, beside
|
|
118
|
+
* a durable `DcbEventStore` layer such as `@kairos-es/store-postgres`'s.
|
|
119
|
+
*
|
|
120
|
+
* Shipped as a `Layer` rather than left to `Layer.succeed` at each wiring site for
|
|
121
|
+
* the same reason `SerializerDefault` is: it names the declaration, so the two
|
|
122
|
+
* legal values cannot be typo'd, and it composes into a store layer's own merge
|
|
123
|
+
* without a second import from `effect`.
|
|
124
|
+
*/
|
|
125
|
+
export declare const DurableEventLog: Layer.Layer<EventLogDurability>;
|
|
126
|
+
/**
|
|
127
|
+
* Declare that the event log DIES WITH THE PROCESS — `core`'s
|
|
128
|
+
* `DcbEventStoreInMemory`, and any other log whose contents do not survive a
|
|
129
|
+
* restart.
|
|
130
|
+
*
|
|
131
|
+
* Under this declaration a DURABLE `ProjectionStore` is rejected by R2, because
|
|
132
|
+
* its checkpoint would outlive the log it points into. It is also the honest
|
|
133
|
+
* conservative choice over a log whose durability is genuinely unknown: it forgoes
|
|
134
|
+
* a durable view and nothing else.
|
|
135
|
+
*/
|
|
136
|
+
export declare const EphemeralEventLog: Layer.Layer<EventLogDurability>;
|
|
137
|
+
/**
|
|
138
|
+
* R2 itself: `undefined` when the two declarations are legally ordered, otherwise
|
|
139
|
+
* the sentence an author meets when they are not.
|
|
140
|
+
*
|
|
141
|
+
* The one illegal pairing of the four is a DURABLE view over an EPHEMERAL log. The
|
|
142
|
+
* asymmetry is deliberate and the module doc argues it: declaring `'ephemeral'`
|
|
143
|
+
* over a genuinely durable log costs nothing but a durable view, so only the unsafe
|
|
144
|
+
* direction is rejected.
|
|
145
|
+
*
|
|
146
|
+
* ## Why NAMED FIELDS rather than two positional arguments
|
|
147
|
+
*
|
|
148
|
+
* The two operands are the same unbranded `Durability` union, so a positional pair
|
|
149
|
+
* would typecheck transposed — and a transposed R2 check is not a broken check, it
|
|
150
|
+
* is an INVERTED one: it would accept the one pairing that silently freezes a view
|
|
151
|
+
* and reject the three that are fine, which is worse than having no check at all.
|
|
152
|
+
* Named fields make that transposition unwritable without visibly naming the wrong
|
|
153
|
+
* field, which is as much as a call-site convention can be asked to carry.
|
|
154
|
+
*
|
|
155
|
+
* It is as much as is WANTED here, too. Branding the two halves so the type system
|
|
156
|
+
* separated them was considered and rejected: `Durability` is a two-value union
|
|
157
|
+
* read straight off a `Layer` and a `ProjectionStore`, both of them public surfaces
|
|
158
|
+
* a caller writes by hand, so branding it would put a constructor between an
|
|
159
|
+
* application and `Layer.succeed(EventLogDurability, 'durable')` for a mistake the
|
|
160
|
+
* R2 case and its non-vacuity control already catch — that control provides
|
|
161
|
+
* `DurableEventLog` over the SAME durable view store and asserts the runner starts,
|
|
162
|
+
* which no mis-comparison in here can satisfy while still failing the rejection
|
|
163
|
+
* half.
|
|
164
|
+
*
|
|
165
|
+
* The sentence begins with `R2 violated` and states the rule, the silent failure it
|
|
166
|
+
* prevents, and BOTH fixes, because it is written for whoever meets it in a log with
|
|
167
|
+
* no file open. It names `EventLogDurability` in particular: the log's half of the
|
|
168
|
+
* comparison is this service, provided beside the `DcbEventStore` layer, so that is
|
|
169
|
+
* where the reader has to go — a read model carries no field to correct. It does not
|
|
170
|
+
* name the projection; the caller's preamble carries that, for every rule at once.
|
|
171
|
+
* Whether it also names a PARTITION is the caller's to decide: `runProjection` writes
|
|
172
|
+
* one, and `runProjections` deliberately writes none — not because its entries share a
|
|
173
|
+
* partition (each resolves its own, and a distinct one is the escape hatch for two
|
|
174
|
+
* materialisations cohabiting a store) but because a fault there is already attributed
|
|
175
|
+
* by entry INDEX, which discriminates where a partition may not.
|
|
176
|
+
*/
|
|
177
|
+
export declare const durabilityOrderingFault: (declarations: {
|
|
178
|
+
readonly eventLogDurability: Durability;
|
|
179
|
+
readonly viewDurability: Durability;
|
|
180
|
+
}) => string | undefined;
|
|
181
|
+
export {};
|
|
182
|
+
//# sourceMappingURL=EventLogDurability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventLogDurability.d.ts","sourceRoot":"","sources":["../../src/EventLogDurability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqGG;AACH,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;;AAEnD;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,uBAEL;CAAG;AAEtC;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAG3D,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAG7D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,uBAAuB,GAAI,cAAc;IACpD,QAAQ,CAAC,kBAAkB,EAAE,UAAU,CAAA;IACvC,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAA;CACpC,KAAG,MAAM,GAAG,SAWE,CAAA"}
|