@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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kairos-es/read — the read side: a query-driven projection runner over the
|
|
3
|
+
* store's `subscribe` contract, and a scalar checkpoint committed atomically with
|
|
4
|
+
* the read model's own writes (ADR-0007).
|
|
5
|
+
*
|
|
6
|
+
* Platform-neutral by design: peers are `effect` plus `@kairos-es/core` and
|
|
7
|
+
* `@kairos-es/codec` only — the same peer shape as the codec — so the
|
|
8
|
+
* all-in-memory permutation, a property of one MATERIALISATION's (log, view)
|
|
9
|
+
* pairing and including client-side use, needs no database
|
|
10
|
+
* dependency. Every dependency-bearing backend is its own package, which is why
|
|
11
|
+
* the SQL `ProjectionStore` lives in `@kairos-es/read-postgres` rather than behind
|
|
12
|
+
* a sub-entry here: a sub-entry shares one dependency surface, whereas a separate
|
|
13
|
+
* package isolates a new one (`@effect/sql`).
|
|
14
|
+
*
|
|
15
|
+
* ## Where each rationale lives
|
|
16
|
+
*
|
|
17
|
+
* This module re-exports; it does not argue. Every argument the read side rests
|
|
18
|
+
* on is written ONCE, at the definition site a reader would reach for anyway,
|
|
19
|
+
* because a rationale kept in N places is a rationale a correction has to find N
|
|
20
|
+
* times. What follows is the index, R1 and R2 included: each of those two has a
|
|
21
|
+
* module that owns it, so each gets a pointer here like everything else. R3 is the
|
|
22
|
+
* one exception, and the last section says why.
|
|
23
|
+
*
|
|
24
|
+
* - **The log seam is `DcbEventStore.subscribe`, and it belongs entirely to the
|
|
25
|
+
* event store.** `store-postgres` owns LISTEN/NOTIFY as a wake source over
|
|
26
|
+
* `core`'s shared poll machine, `store-sqlite` owns no wake source and rides
|
|
27
|
+
* that poll alone, and `core`'s in-memory layer owns its append `PubSub` as a
|
|
28
|
+
* wake source over that same machine; all three sit behind the
|
|
29
|
+
* identical `subscribe(query, after)` contract, so a projection never learns
|
|
30
|
+
* which it is running against. Nothing in this package adds to that contract —
|
|
31
|
+
* the one claim below that no other module is placed to make.
|
|
32
|
+
* - **The view seam is `ProjectionStore`** — one port rather than two, `commit`
|
|
33
|
+
* taking the view writes as an ARGUMENT, `CheckpointSuperseded` as an error
|
|
34
|
+
* rather than a returned outcome, and `ProjectionStoreError` inverting the store
|
|
35
|
+
* contract's defect rule. `ProjectionStore.ts`'s module doc. The multi-key port
|
|
36
|
+
* against the keyed façade a read model is handed is on `KeyedProjectionStore`
|
|
37
|
+
* and on `forKey` in the same module; the single-atomic-effect requirement a
|
|
38
|
+
* NON-TRANSACTIONAL store's caller must honour is on the `ProjectionStore`
|
|
39
|
+
* interface itself, and `foldIntoRef.ts` is that shape.
|
|
40
|
+
* - **R1, atomicity** — a read model's checkpoint lives WITH its view, in a store
|
|
41
|
+
* where the two can be committed atomically, which is what `commit` is. The
|
|
42
|
+
* argument is on the `ProjectionStore` interface in `ProjectionStore.ts` (R1 is
|
|
43
|
+
* what that interface IS), together with what a view offering no transaction at
|
|
44
|
+
* all falls back to: at-least-once, with POSITION-KEYED idempotent writes, a
|
|
45
|
+
* recommendation because the port cannot check it.
|
|
46
|
+
* - **R2, durability ordering** — checkpoint durability must not EXCEED event-log
|
|
47
|
+
* durability, so a DURABLE view over an EPHEMERAL log is a defect rejected at
|
|
48
|
+
* construction rather than a view that silently freezes. The view store declares
|
|
49
|
+
* its half on the `ProjectionStore` it implements; the log's half, the silent
|
|
50
|
+
* failure the rule prevents, and why the log's class is ONE context tag beside
|
|
51
|
+
* the store layer rather than a field per read model are all in
|
|
52
|
+
* `EventLogDurability.ts`.
|
|
53
|
+
* - **How these errors RENDER themselves** — why `ProjectionStoreError` derives its
|
|
54
|
+
* `message` from `reason` and `CheckpointSuperseded` derives one from `expected`
|
|
55
|
+
* (including the `bigint` argument that once kept the field empty and why it was a
|
|
56
|
+
* category error), how far filling that field reaches (every reader of the error,
|
|
57
|
+
* not one log line) and what it leaves untouched: the two classes in
|
|
58
|
+
* `ProjectionStore.ts`. `PipelineDied` in `ProjectionRunner.ts` and
|
|
59
|
+
* `ProjectionStalled` in `superviseOnProgress.ts` are the other two and point
|
|
60
|
+
* there.
|
|
61
|
+
* - **Why the view store is a VALUE and not a `Context.Tag`**, and what a slice's
|
|
62
|
+
* graduation from memory to Postgres does and does not change: `ReadModel.store`
|
|
63
|
+
* in `runProjection.ts`. It is demonstrated rather than asserted, by
|
|
64
|
+
* `examples/course-subscriptions`'s `src/courseRosterSql.ts` and by
|
|
65
|
+
* `@kairos-es/read-postgres`'s `test/courseRosterGraduation.test.ts`.
|
|
66
|
+
* - **The in-memory implementation, and why it is not merely a test double:**
|
|
67
|
+
* `inMemoryProjectionStore.ts`.
|
|
68
|
+
* - **The runner** — the pipeline stage by stage, and why supervision lives next
|
|
69
|
+
* door: `ProjectionRunner.ts`, which holds the SUBSTRATE both entry points sit on
|
|
70
|
+
* and no entry point of its own. Its two option sets are documented on
|
|
71
|
+
* `ProjectionPipelineOptions` there and on `ProjectionSupervisionOptions` in
|
|
72
|
+
* `superviseOnProgress.ts`; `ProjectionRunnerOptions` says why their union is
|
|
73
|
+
* FLAT and why every figure is bounded rather than merely documented.
|
|
74
|
+
* - **ONE materialisation of one read model** — `runProjection` and the
|
|
75
|
+
* `projectionLayer` over it: `runProjection.ts`, which also says why the singular
|
|
76
|
+
* and the plural entry points are two modules over one substrate and why neither
|
|
77
|
+
* calls the other.
|
|
78
|
+
* - **N MATERIALISATIONS of one read model** — one slice record and one
|
|
79
|
+
* `ProjectionId` for the call, one view store and one runner each, which is what
|
|
80
|
+
* `runProjections` is: `runProjections.ts`. It carries the guarantee at exactly
|
|
81
|
+
* its strength (one query, one decode, N applies — never "they cannot drift",
|
|
82
|
+
* since a SQL `apply` re-implements the fold and agreement between materialised
|
|
83
|
+
* figures is empirical), why the guarantee is construction-scoped, why each entry
|
|
84
|
+
* hands over an UNBOUND `ProjectionStore` while the identity is named once for the
|
|
85
|
+
* call, why the tuning and the two supervision hooks are PER ENTRY, and why there
|
|
86
|
+
* is no `projectionsLayer`. The wiring that is its OWN — two materialisations
|
|
87
|
+
* sharing one store under one resolved `CheckpointKey` — is the set-level rung of
|
|
88
|
+
* the construction gate below, with the two bounds it deliberately does not reach;
|
|
89
|
+
* the gate's other four run in the same pass — two per entry, two once over the
|
|
90
|
+
* shared query and slice record — so no refused call forks anything, whichever
|
|
91
|
+
* rung and whichever entry it was.
|
|
92
|
+
* - **The supervisor** — retry while an externally observed signal moves, give up
|
|
93
|
+
* when it stops; why a `CheckpointSuperseded` loser resynchronises instead of
|
|
94
|
+
* tripping the breaker; why the two hooks are the primary observability channel
|
|
95
|
+
* and the log lines a thin lossy default; why it holds no rendering of a
|
|
96
|
+
* caller's `E` at all, the label being the fault's own `message`; and the
|
|
97
|
+
* wall-clock arithmetic behind the no-progress budget: `superviseOnProgress.ts`.
|
|
98
|
+
* `superviseOnProgress` ITSELF is package-internal and deliberately not exported
|
|
99
|
+
* below — it is the projection runner's supervisor, not a general combinator, and
|
|
100
|
+
* that module records both why and what generalising it would cost. Three
|
|
101
|
+
* symbols from it are exported, each because the PUBLIC surface names it:
|
|
102
|
+
* `ProjectionStalled` is `ProjectionRunner.fibre`'s error channel,
|
|
103
|
+
* `ProjectionRunnerOptions` extends `ProjectionSupervisionOptions`, and tuning
|
|
104
|
+
* the no-progress budget means calling `MaxNoProgressRestarts.make(n)`.
|
|
105
|
+
* - **One writer per MATERIALISATION, and why one read model's stream is never
|
|
106
|
+
* sharded:** ADR-0007, with the partition dimension's own half on `PartitionId`.
|
|
107
|
+
* Per materialisation rather than per read model, because a read model may have
|
|
108
|
+
* several — N of them are N single writers, never shards of one stream.
|
|
109
|
+
* Where apply THROUGHPUT rather than isolation is the constraint, ADR-0007 also
|
|
110
|
+
* carries the escape hatch — coalesce a batch per target row inside the one
|
|
111
|
+
* commit — and there is no library symbol for it, because it is a fold the read
|
|
112
|
+
* model performs over its own batch.
|
|
113
|
+
* - **The tagless-slice rule** — every read-model slice carries at least one tag
|
|
114
|
+
* (a `system:…` tag for a genuinely global slice), enforced at construction by
|
|
115
|
+
* two checks that cannot stand in for each other, since a lone tagless slice
|
|
116
|
+
* composes to a query the store WOULD serve: both are rungs of the wiring gate
|
|
117
|
+
* `projectionWiringFault.ts`, which argues the rules and the order they are judged
|
|
118
|
+
* in, and which is consulted from the one `prepareProjection` phase both runner
|
|
119
|
+
* entry points run before either forks anything.
|
|
120
|
+
* - **Running a read model in a test** — the `@kairos-es/read/testing` subpath
|
|
121
|
+
* (`testing.ts`), which also states its boundary against
|
|
122
|
+
* `@kairos-es/projection-store-contract-tests`.
|
|
123
|
+
* - **The narrative version of all of it** is this package's `README.md`; the
|
|
124
|
+
* decision it implements is ADR-0007.
|
|
125
|
+
*
|
|
126
|
+
* ## R3, the legal permutations, and several materialisations of one read model
|
|
127
|
+
*
|
|
128
|
+
* The one thing this file ARGUES rather than indexes. R1 and R2 each have a module
|
|
129
|
+
* that owns them, pointed at above; R3 has none, because it is a property of how
|
|
130
|
+
* read models are composed into a SUBSCRIPTION and no single module holds that.
|
|
131
|
+
*
|
|
132
|
+
* **R3, composition bound.** Read models sharing one subscription share one cursor
|
|
133
|
+
* and one checkpoint, hence by R1 one view store and one durability class. So
|
|
134
|
+
* per-read-model view selection means one runner per view-store GROUP, not one per
|
|
135
|
+
* read model — and rebuilding one member of a composed bundle means rebuilding the
|
|
136
|
+
* bundle (or running a transient dedicated runner from `ORIGIN`). Composing is
|
|
137
|
+
* still the default, because the NOTIFY channel is per store, not per query.
|
|
138
|
+
*
|
|
139
|
+
* **What R3 does NOT say.** It does not bound how many times ONE read model may be
|
|
140
|
+
* materialised. R3 is about sharing a SUBSCRIPTION, and several materialisations of
|
|
141
|
+
* one read model deliberately do not share one: they pay N subscriptions, N cursors
|
|
142
|
+
* and N checkpoints precisely BECAUSE R1 and R2 forbid the sharing — one atomic
|
|
143
|
+
* `commit` cannot span two stores, and one checkpoint cannot carry two durability
|
|
144
|
+
* classes. So per-read-model view-store selection is a FLOOR rather than a ceiling.
|
|
145
|
+
* One slice record may feed a durable Postgres materialisation and an ephemeral
|
|
146
|
+
* in-memory one AT ONCE, wired by `runProjections`, which holds one `slices` value
|
|
147
|
+
* and one `ProjectionId` across them so their query and their decode cannot diverge.
|
|
148
|
+
* What it does not hold is the FOLDS — each `apply` re-expresses the fold against
|
|
149
|
+
* its own store, and a SQL one writing rows is not `foldIntoRef` — so agreement
|
|
150
|
+
* between two materialised figures is EMPIRICAL, something a test demonstrates, and
|
|
151
|
+
* never something the wiring gives you. The runners do not contend either: each
|
|
152
|
+
* owns its own checkpoint in its own store, so the guarded compare-and-set
|
|
153
|
+
* arbitrates with no lease, no lock and no coordinator.
|
|
154
|
+
*
|
|
155
|
+
* That leaves exactly THREE legal (log, view) permutations: all-in-memory (tests,
|
|
156
|
+
* prototypes, client-side); a durable log with an in-memory view (dev, low-traffic
|
|
157
|
+
* and admin views, in-flux slices — rebuilt from `ORIGIN` each boot, which is
|
|
158
|
+
* affordable at the event counts that are its remit and a reason to graduate a
|
|
159
|
+
* slice once it bakes); and a durable log with a durable view, the production
|
|
160
|
+
* default. Each is a property of ONE MATERIALISATION's pairing. Mixing is a
|
|
161
|
+
* DEPLOYMENT property rather than a fourth permutation: one deployment can run the
|
|
162
|
+
* second and third side by side against the same log — for two different read
|
|
163
|
+
* models, which is the case R3 bounds when they try to share a subscription, or for
|
|
164
|
+
* two materialisations of ONE read model, which R3 never reaches because they never
|
|
165
|
+
* share one.
|
|
166
|
+
*
|
|
167
|
+
* **A third KIND of materialisation sits outside all of this.** A read-time
|
|
168
|
+
* materialisation — `modelAtHead`, in `@kairos-es/codec` — folds the log per query
|
|
169
|
+
* and has no table, no checkpoint, no runner and no view store, so no permutation,
|
|
170
|
+
* no rung of the construction gate and no durability rule applies to it, R2 being
|
|
171
|
+
* VACUOUS where there is no checkpoint whose durability could outlive a log. It is
|
|
172
|
+
* consistent as of the `head` its read returned, which trails nothing, so it is the
|
|
173
|
+
* MOST current view of a slice record and the maintained table beside it is the
|
|
174
|
+
* stale one. Where a read model is small enough to fold at query time, that is a
|
|
175
|
+
* reason not to maintain it at all.
|
|
176
|
+
*/
|
|
177
|
+
export { DurableEventLog, EphemeralEventLog, EventLogDurability, } from './EventLogDurability.js';
|
|
178
|
+
export { foldIntoRef } from './foldIntoRef.js';
|
|
179
|
+
export { makeInMemoryProjectionStore } from './inMemoryProjectionStore.js';
|
|
180
|
+
export { BatchSize, PipelineDied, type ProjectionPipelineOptions, type ProjectionRunner, type ProjectionRunnerOptions, } from './ProjectionRunner.js';
|
|
181
|
+
export { type CheckpointKey, CheckpointSuperseded, checkpointKey, DEFAULT_PARTITION, type Durability, forKey, type KeyedProjectionStore, PartitionId, ProjectionId, type ProjectionStore, ProjectionStoreError, } from './ProjectionStore.js';
|
|
182
|
+
export { projectionLayer, type ReadModel, runProjection, } from './runProjection.js';
|
|
183
|
+
export { type Materialisation, type MaterialisedProjection, runProjections, } from './runProjections.js';
|
|
184
|
+
export { MaxNoProgressRestarts, ProjectionStalled, type ProjectionSupervisionOptions, } from './superviseOnProgress.js';
|
|
185
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+KG;AACH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAA;AACvE,OAAO,EACL,SAAS,EACT,YAAY,EACZ,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,KAAK,aAAa,EAClB,oBAAoB,EACpB,aAAa,EACb,iBAAiB,EACjB,KAAK,UAAU,EACf,MAAM,EACN,KAAK,oBAAoB,EACzB,WAAW,EACX,YAAY,EACZ,KAAK,eAAe,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,aAAa,GACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,4BAA4B,GAClC,MAAM,uBAAuB,CAAA"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read side's CONSTRUCTION GATE: every wiring the read side refuses ON ITS OWN
|
|
3
|
+
* RULES, judged in one place, in one declared order, each verdict one
|
|
4
|
+
* `string | undefined`.
|
|
5
|
+
*
|
|
6
|
+
* ## ONE entry point over five rules, at three arities
|
|
7
|
+
*
|
|
8
|
+
* `projectionWiringFault` judges ONE CALL: the log the whole graph subscribes to,
|
|
9
|
+
* the one composed query and the one slice record every materialisation shares, and
|
|
10
|
+
* a LIST of the resolved per-entry wirings. `runProjection` hands it a singleton and
|
|
11
|
+
* `runProjections` hands it N, and both do so before either forks anything.
|
|
12
|
+
*
|
|
13
|
+
* The rules divide by what they are a function of, not by who called:
|
|
14
|
+
*
|
|
15
|
+
* - the COLLISION rung is a function of the whole list, pairwise;
|
|
16
|
+
* - R2 and the tuning figures are functions of ONE entry, judged for every entry in
|
|
17
|
+
* turn;
|
|
18
|
+
* - the servable grammar and the per-slice tag rule are functions of the SHARED
|
|
19
|
+
* query and the SHARED slice record, so they are judged once for the call.
|
|
20
|
+
*
|
|
21
|
+
* One record with the shared fields stated once and the entries as a list is what
|
|
22
|
+
* lets all three arities compose in one `??` chain. What the gate is stays what it
|
|
23
|
+
* was: one home, one return type, one declared order (below), and a caller that
|
|
24
|
+
* classifies the verdict under a preamble it was handed.
|
|
25
|
+
*
|
|
26
|
+
* ## The ONE refusal that is deliberately not here
|
|
27
|
+
*
|
|
28
|
+
* Naming it is what keeps "one place" honest. A slice record holding two DISTINCT
|
|
29
|
+
* `Schema`s under one event type is refused by `@kairos-es/codec`'s
|
|
30
|
+
* `decodeSlices`, which `prepareProjection` calls a few lines BELOW this gate on
|
|
31
|
+
* behalf of both entry points — so a rejected record still dies as a defect with
|
|
32
|
+
* nothing forked, exactly as a rejected wiring does, but the sentence a reader
|
|
33
|
+
* meets is the codec's. That rule
|
|
34
|
+
* belongs to the codec because BOTH of its consumers need it and only one is on
|
|
35
|
+
* the read side: the write path reaches the same merge through
|
|
36
|
+
* `buildDecisionModel` and never comes near this module, so a rung here
|
|
37
|
+
* would be a second home for one rule, covering half the hazard. `decode.ts` owns
|
|
38
|
+
* the argument; this is a pointer, not a restatement.
|
|
39
|
+
*
|
|
40
|
+
* Which of the two a record wrong in BOTH ways meets FIRST is not this module's to
|
|
41
|
+
* state either, and it has exactly one home: `prepareProjection` in
|
|
42
|
+
* `ProjectionRunner.ts`, the single body in which the gate call sits above the
|
|
43
|
+
* `decodeSlices` call, for both entry points at once. It was a claim two function
|
|
44
|
+
* bodies each made in prose until that phase was extracted, and pinned for only one
|
|
45
|
+
* of them.
|
|
46
|
+
*
|
|
47
|
+
* ## Why this is a module and not a paragraph of `runProjection`
|
|
48
|
+
*
|
|
49
|
+
* `runProjection` was doing two unrelated jobs. One is to refuse a wiring that can
|
|
50
|
+
* only ever be wrong — five rules, none of which mentions a stream, a scope or a
|
|
51
|
+
* fibre, and only one of which so much as compares two stores for identity. The
|
|
52
|
+
* other is to build and fork a pipeline. The refusals had
|
|
53
|
+
* grown to roughly two fifths of that function's body and most of its doc, and
|
|
54
|
+
* three of them wrote their own `Effect.die` with their own copy of the preamble
|
|
55
|
+
* naming the projection.
|
|
56
|
+
*
|
|
57
|
+
* The precedent is this package's own: `superviseOnProgress` was a deep module
|
|
58
|
+
* trapped inside the same function, and pulling it out is what made a supervision
|
|
59
|
+
* quantity pinnable against a stub instead of through the whole pipeline. This is
|
|
60
|
+
* the other half of that function, and it is deeper still by the measure that
|
|
61
|
+
* matters here — its interface is a record in and a sentence out, while everything
|
|
62
|
+
* behind it is five unrelated rules over four vocabularies (a layer graph, a
|
|
63
|
+
* tuning literal, a query grammar, a set of view stores).
|
|
64
|
+
*
|
|
65
|
+
* The fifth rule arriving from a DIFFERENT caller is what the module having been
|
|
66
|
+
* extracted bought. `runProjections` needed a construction-time refusal of its own,
|
|
67
|
+
* and there was already a place for it — with a settled return type, a settled
|
|
68
|
+
* division of labour with its caller, and a suite that states a rule against a
|
|
69
|
+
* record literal. Inline, it would have been a sixth thing inside a second pipeline
|
|
70
|
+
* function, in a second style, with a second preamble. It landed as a RUNG of the
|
|
71
|
+
* one chain rather than as a second entry point, which is what keeps the declared
|
|
72
|
+
* order below a property of this file rather than of which caller a reader opened.
|
|
73
|
+
*
|
|
74
|
+
* ## Why `string | undefined`, and why the shared record takes no `CheckpointKey`
|
|
75
|
+
*
|
|
76
|
+
* `undefined` for a sound wiring, otherwise the SPECIFIC sentence: what is wrong,
|
|
77
|
+
* what it would do, and how to fix it. It does not throw, does not call `Effect.die`
|
|
78
|
+
* and does not return an `Either` — ONE exit route and one return type, which is
|
|
79
|
+
* what lets five rules over four vocabularies compose with `??` and be read as five
|
|
80
|
+
* lines.
|
|
81
|
+
*
|
|
82
|
+
* The shared half of the record names no projection and no partition. The per-entry
|
|
83
|
+
* list does carry a resolved `CheckpointKey` each, but for the collision rung to
|
|
84
|
+
* COMPARE rather than for anything to report: no rule interpolates a `key` into its
|
|
85
|
+
* sentence except the collision rung, which names the partition the two offenders
|
|
86
|
+
* share.
|
|
87
|
+
*
|
|
88
|
+
* ATTRIBUTION and CLASSIFICATION both sit outside. This gate's ONE caller,
|
|
89
|
+
* `prepareProjection`, owns the single `Effect.die`; the PREAMBLE naming the
|
|
90
|
+
* projection (and, where there is one to name, the partition) comes from one level
|
|
91
|
+
* further out still, from whichever entry point was called. So the preamble is
|
|
92
|
+
* written once per ENTRY POINT rather than once per rule — which is exactly what
|
|
93
|
+
* collapsed three `Effect.die` sites to one. The gate
|
|
94
|
+
* writes the third party to that sentence: where there is more than one entry, the
|
|
95
|
+
* per-entry rungs prefix their verdict with the offending INDEX, in the same
|
|
96
|
+
* `materialisations[i]` vocabulary the collision rung already uses. Over a singleton
|
|
97
|
+
* there is no index to name and none is written, so a single wiring's message is
|
|
98
|
+
* what it always was. The same arrangement repeats one level DOWN, inside this file:
|
|
99
|
+
* `positiveFiniteDuration` owns the `Duration` bound and each of the four tuning
|
|
100
|
+
* call sites supplies the `role` clause, because the predicate knows the rule and
|
|
101
|
+
* its caller knows what breaks.
|
|
102
|
+
*
|
|
103
|
+
* The classification half of that is worth spelling out. That a mis-wiring is a
|
|
104
|
+
* DEFECT rather
|
|
105
|
+
* than a channel value is a claim about `runProjection`'s and `runProjections`'
|
|
106
|
+
* signatures — both error channels are `never` because everything that can go wrong
|
|
107
|
+
* once a projection is RUNNING belongs to the supervisor — and a gate returning a
|
|
108
|
+
* string makes no such claim, so it can be called from a plain function, a test, or
|
|
109
|
+
* an `Effect.gen` without three different failure conventions.
|
|
110
|
+
*
|
|
111
|
+
* ## The ORDER is a decision, and this is the only place it is stated
|
|
112
|
+
*
|
|
113
|
+
* The rules run OUTWARD-IN, from the widest wiring decision to the narrowest.
|
|
114
|
+
*
|
|
115
|
+
* The order is over RULES and not over ENTRIES. Entry 1's layer-graph mistake is
|
|
116
|
+
* still wider than entry 0's options-literal mistake, so rung 2 is judged for every
|
|
117
|
+
* entry before rung 3 is judged for any. An entry-major loop would instead let ARRAY
|
|
118
|
+
* POSITION decide which fault an author sees, and position carries no meaning here:
|
|
119
|
+
* the entries are a set, and are a tuple only because inference needs one.
|
|
120
|
+
*
|
|
121
|
+
* 1. **The collision rung**, once over the whole SET. Two materialisations sharing
|
|
122
|
+
* one store under one key is a property of the WHOLE CALL rather than of any one
|
|
123
|
+
* wiring in it, so it is judged before any single wiring is looked at — the same
|
|
124
|
+
* outward-in principle, one level up. And a colliding pair is worth hearing about
|
|
125
|
+
* BEFORE a mis-tuned figure in one of them, because fixing the tuning would leave
|
|
126
|
+
* the two runners racing one cursor. It cannot fire below N = 2, so it is vacuous
|
|
127
|
+
* for the singleton `runProjection` passes.
|
|
128
|
+
* 2. **R2, durability ordering**, PER ENTRY in entry order. A property of the LAYER
|
|
129
|
+
* GRAPH — which log, which view store — decided before any read model existed. If
|
|
130
|
+
* that pairing is illegal then nothing else about the runner matters, because the
|
|
131
|
+
* view it maintains cannot be true however well tuned the daemon is.
|
|
132
|
+
* 3. **The tuning figures**, PER ENTRY in entry order. What the caller wrote in the
|
|
133
|
+
* options literal, a smaller and more local mistake than picking the wrong pair
|
|
134
|
+
* of layers.
|
|
135
|
+
* 4. **The store's own servable grammar**, once over the SHARED composed query,
|
|
136
|
+
* reported in the STORE's vocabulary by `core`'s own `assertServableQuery`.
|
|
137
|
+
* 5. **The read side's stricter per-slice tag rule**, once over the SHARED slice
|
|
138
|
+
* record, and it MUST stay after (4): it catches only what that grammar admits,
|
|
139
|
+
* since a lone type-only item is servable. Were it first, a tagless-plus-tagged
|
|
140
|
+
* union — a violation of the shared grammar — would be reported as a read-side
|
|
141
|
+
* slice convention instead of in the vocabulary every other caller of that
|
|
142
|
+
* grammar meets it in.
|
|
143
|
+
*
|
|
144
|
+
* Hoisting the two SHARED rungs ahead of the two per-entry ones, which the
|
|
145
|
+
* shared-versus-per-entry axis would suggest, was considered and rejected: it
|
|
146
|
+
* changes the message a single wiring wrong in two ways gets, which is a fixed
|
|
147
|
+
* constraint. (2)-(3) ahead of (4)-(5) also restores the relative order that held
|
|
148
|
+
* before the supervisor was extracted, when its three restart durations were checked
|
|
149
|
+
* with `batchWindow` rather than a module away. But the point of writing the order
|
|
150
|
+
* down is not the restoration: it is that the order is now FIVE LINES a reader can
|
|
151
|
+
* read, rather than an emergent property of which of two modules happens to look at
|
|
152
|
+
* a field first.
|
|
153
|
+
*
|
|
154
|
+
* ## Why the input is a record of ALREADY-RESOLVED values
|
|
155
|
+
*
|
|
156
|
+
* Every field is a value, not an option: the caller has already applied its
|
|
157
|
+
* defaults — through `resolveProjection`, the one resolution site both callers
|
|
158
|
+
* share — so the gate judges exactly the figures that will run and cannot be handed
|
|
159
|
+
* one default while the pipeline uses another. That also holds the defaults
|
|
160
|
+
* themselves to the rules they document, for one decode per runner. The keys are
|
|
161
|
+
* RESOLVED `CheckpointKey`s for the same reason and one more: a partition a caller
|
|
162
|
+
* omitted is `DEFAULT_PARTITION`, so a rung judging the raw options would miss the
|
|
163
|
+
* commonest form of the collision, which is two entries that name no partition at
|
|
164
|
+
* all.
|
|
165
|
+
*
|
|
166
|
+
* The two INTEGER options are absent from the record on purpose. `BatchSize` and
|
|
167
|
+
* `MaxNoProgressRestarts` are branded, so their boundary is the caller's own
|
|
168
|
+
* `.make(...)` — a bad value is refused a stack frame away from where it was
|
|
169
|
+
* written and never reaches here at all.
|
|
170
|
+
*
|
|
171
|
+
* `slices` is a SHARED field and is typed as narrowly as the check needs — a query
|
|
172
|
+
* per slice, nothing else — which keeps this module free of a `@kairos-es/codec`
|
|
173
|
+
* import for a rule that has no opinion about schemas, payloads or folds.
|
|
174
|
+
* `Record<string, SliceProjection>` satisfies it structurally, so the record an entry
|
|
175
|
+
* point was handed reaches this rung unconverted.
|
|
176
|
+
*
|
|
177
|
+
* An entry's `cursorIdentity` is typed `object` and never called: it is an identity
|
|
178
|
+
* token the collision rung compares by REFERENCE. `object` rather than `ProjectionStore`
|
|
179
|
+
* because a `KeyedProjectionStore` is not structurally one (`readCheckpoint` is an
|
|
180
|
+
* `Effect` on the façade and a function on the port), so a caller holding only a
|
|
181
|
+
* bound view could not fill the field; and because `object` forbids `undefined`,
|
|
182
|
+
* which would otherwise make two unidentified entries compare equal.
|
|
183
|
+
*
|
|
184
|
+
* It is INTERNAL: nothing here is re-exported from the package index. The gate is
|
|
185
|
+
* how the two runner entry points refuse a wiring, not a capability the library
|
|
186
|
+
* offers — a caller with a wiring to validate has `runProjection` or
|
|
187
|
+
* `runProjections` itself. BOTH judge all five rules before either forks anything.
|
|
188
|
+
*/
|
|
189
|
+
import { type Query } from '@kairos-es/core';
|
|
190
|
+
import { Duration } from 'effect';
|
|
191
|
+
import type { CheckpointKey, Durability } from './ProjectionStore.js';
|
|
192
|
+
/**
|
|
193
|
+
* ONE materialisation as the gate sees it: an identity token, its resolved key, its
|
|
194
|
+
* half of R2 and the four `Duration`s that tune it.
|
|
195
|
+
*
|
|
196
|
+
* `ResolvedProjection` in `ProjectionRunner.ts` satisfies all of it but
|
|
197
|
+
* `cursorIdentity`, which is why both entry points can build an entry by spreading a
|
|
198
|
+
* resolved wiring beside the identity they hold.
|
|
199
|
+
*
|
|
200
|
+
* Exported to the PACKAGE and no further, which is one more symbol than the gate
|
|
201
|
+
* used to offer and still nothing a consumer can reach: `prepareProjection` — the
|
|
202
|
+
* phase that calls this gate on behalf of both entry points — names the entry LIST
|
|
203
|
+
* in its own signature, and a structural re-declaration there would be a second
|
|
204
|
+
* statement of what the gate judges, free to drift from this one.
|
|
205
|
+
*
|
|
206
|
+
* The field is NOT called `store`, and that is the point rather than taste. The two
|
|
207
|
+
* entry points put DIFFERENT objects in it — `runProjections` the UNBOUND port, which is
|
|
208
|
+
* the last surface at which sameness is askable since `forKey` deliberately does not
|
|
209
|
+
* expose what it closed over; `runProjection` the BOUND view it was handed, where a
|
|
210
|
+
* singleton makes the rung vacuous anyway — and each is right for its own arity.
|
|
211
|
+
* Under the name `store` those two meanings sat behind one word in a record built by
|
|
212
|
+
* SPREADING a `Materialisation`, whose own `store` field means only the first of
|
|
213
|
+
* them. Nothing would have complained if a future field on either side shadowed the
|
|
214
|
+
* other, the type being `object`. What the rung needs is not "a store" but "the value
|
|
215
|
+
* whose IDENTITY separates one cursor from another", which is what this name says.
|
|
216
|
+
*
|
|
217
|
+
* Typed `object` because the rung COMPARES it and never calls it (a
|
|
218
|
+
* `KeyedProjectionStore` is not structurally a `ProjectionStore`), and because
|
|
219
|
+
* `object` also refuses `undefined` — which would otherwise make two unidentified
|
|
220
|
+
* entries compare equal and manufacture a collision out of nothing.
|
|
221
|
+
*
|
|
222
|
+
* `batchSize` is deliberately absent along with `maxNoProgressRestarts`: both are
|
|
223
|
+
* branded, so both were bounded at the caller's own `.make(...)`.
|
|
224
|
+
*/
|
|
225
|
+
export type MaterialisationWiring = {
|
|
226
|
+
readonly cursorIdentity: object;
|
|
227
|
+
readonly key: CheckpointKey;
|
|
228
|
+
readonly viewDurability: Durability;
|
|
229
|
+
readonly batchWindow: Duration.DurationInput;
|
|
230
|
+
readonly restartMinDelay: Duration.DurationInput;
|
|
231
|
+
readonly restartMaxDelay: Duration.DurationInput;
|
|
232
|
+
readonly restartResetAfter: Duration.DurationInput;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Judge ONE CALL's whole wiring: `undefined` when it is sound, otherwise the
|
|
236
|
+
* sentence describing the FIRST rule it breaks.
|
|
237
|
+
*
|
|
238
|
+
* Five rules, `??`-chained in the outward-in order the module doc argues for and
|
|
239
|
+
* fixes. `??` rather than an array of predicates or a collected list of every
|
|
240
|
+
* violation, for two reasons: the first fault is the one to fix — the later rules
|
|
241
|
+
* judge a wiring whose wider decisions are already known to be wrong, so their
|
|
242
|
+
* verdicts would be noise — and a chain of five named calls is the whole
|
|
243
|
+
* implementation, which is what makes the declared order checkable against the code
|
|
244
|
+
* rather than merely asserted about it.
|
|
245
|
+
*
|
|
246
|
+
* The two PER-ENTRY rungs go through `perEntry`, which is what keeps the chain
|
|
247
|
+
* rule-major: every entry is judged by rung 2 before any entry is judged by rung 3.
|
|
248
|
+
*
|
|
249
|
+
* Every field is a RESOLVED value: see the module doc for why (and for why the two
|
|
250
|
+
* integer options are not here at all).
|
|
251
|
+
*/
|
|
252
|
+
export declare const projectionWiringFault: (wiring: {
|
|
253
|
+
readonly eventLogDurability: Durability;
|
|
254
|
+
readonly query: Query;
|
|
255
|
+
readonly slices: Record<string, {
|
|
256
|
+
readonly query: Query;
|
|
257
|
+
}>;
|
|
258
|
+
readonly materialisations: ReadonlyArray<MaterialisationWiring>;
|
|
259
|
+
}) => string | undefined;
|
|
260
|
+
//# sourceMappingURL=projectionWiringFault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectionWiringFault.d.ts","sourceRoot":"","sources":["../../src/projectionWiringFault.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2LG;AACH,OAAO,EAAuB,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,EAAE,QAAQ,EAAkB,MAAM,QAAQ,CAAA;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAyRlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAA;IAC3B,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAA;IACnC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAA;IAC5C,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,aAAa,CAAA;IAChD,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,aAAa,CAAA;IAChD,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,aAAa,CAAA;CACnD,CAAA;AAgCD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GAAI,QAAQ;IAC5C,QAAQ,CAAC,kBAAkB,EAAE,UAAU,CAAA;IACvC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAA;IAC1D,QAAQ,CAAC,gBAAgB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAA;CAChE,KAAG,MAAM,GAAG,SAUc,CAAA"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runProjection` — the read side's SINGULAR entry point: one read model, one view
|
|
3
|
+
* store, one supervised daemon forked into the caller's `Scope`; plus
|
|
4
|
+
* `projectionLayer`, the same call shaped as a `Layer` so an application's graph owns
|
|
5
|
+
* that daemon's lifetime.
|
|
6
|
+
*
|
|
7
|
+
* ## Two entry points, one substrate
|
|
8
|
+
*
|
|
9
|
+
* This module and `runProjections.ts` are the read side's TWO entry points, and they
|
|
10
|
+
* are deliberately symmetrical one arity apart: N = 1 here, N >= 1 there. Neither
|
|
11
|
+
* builds a pipeline of its own and neither carries a rule of its own. Both run the
|
|
12
|
+
* three phases in `ProjectionRunner.ts` — `resolveProjection`, `prepareProjection`
|
|
13
|
+
* (which is where the construction gate `projectionWiringFault.ts` is consulted, for
|
|
14
|
+
* both of them, before either forks anything) and `buildAndFork` — so what differs
|
|
15
|
+
* between them is the shape of their INPUT and nothing else.
|
|
16
|
+
*
|
|
17
|
+
* Neither calls the other, and that follows from those input shapes rather than being
|
|
18
|
+
* an omission: a `ReadModel` carries a BOUND `KeyedProjectionStore`, a
|
|
19
|
+
* `Materialisation` an UNBOUND `ProjectionStore` plus the one `ProjectionId` the call
|
|
20
|
+
* names, and `forKey` deliberately does not expose what it closed over — so there is
|
|
21
|
+
* no route back from the first shape to the second. `runProjections.ts` owns that
|
|
22
|
+
* argument in full; this is the pointer to it, not a second home for it.
|
|
23
|
+
*
|
|
24
|
+
* ## Why a module of its own rather than a section of the substrate
|
|
25
|
+
*
|
|
26
|
+
* Because a file declaring the `ProjectionRunner` handle, the two option sets, the
|
|
27
|
+
* three shared phases AND one of the two entry points reads as though that entry
|
|
28
|
+
* point were the privileged one. It is not: it is the N = 1 case, and a read model
|
|
29
|
+
* that grows a second materialisation moves to the sibling without changing anything
|
|
30
|
+
* about how it is run. Keeping the substrate under the name of the type it declares,
|
|
31
|
+
* and each entry point in a file named for the function it exports, is what makes
|
|
32
|
+
* that symmetry visible from the directory listing.
|
|
33
|
+
*/
|
|
34
|
+
import type { RequirementOf, Serializer, SliceProjection } from '@kairos-es/codec';
|
|
35
|
+
import type { DcbEventStore, DecodedEvent } from '@kairos-es/core';
|
|
36
|
+
import { type Array as Arr, Effect, Layer, type Scope } from 'effect';
|
|
37
|
+
import type { EventLogDurability } from './EventLogDurability.js';
|
|
38
|
+
import { type ProjectionRunner, type ProjectionRunnerOptions } from './ProjectionRunner.js';
|
|
39
|
+
import type { KeyedProjectionStore } from './ProjectionStore.js';
|
|
40
|
+
/**
|
|
41
|
+
* One read model: where it materialises (under which key), which slices drive it,
|
|
42
|
+
* and how a batch is applied.
|
|
43
|
+
*
|
|
44
|
+
* The slices are load-bearing twice over: their composed query is the
|
|
45
|
+
* subscription's query (so the runner reads exactly the events the folds want,
|
|
46
|
+
* with no separate query to keep in step), and their merged `type -> Schema` map
|
|
47
|
+
* is the decode lookup. There is no way to declare one without the other.
|
|
48
|
+
*
|
|
49
|
+
* The other half of R2 — the EVENT LOG's durability — is deliberately NOT a field
|
|
50
|
+
* here: it is declared once beside the store layer as the `EventLogDurability`
|
|
51
|
+
* service and read from context. `EventLogDurability.ts` argues why, and states
|
|
52
|
+
* the rule both halves feed.
|
|
53
|
+
*
|
|
54
|
+
* By R3, read models sharing one subscription share one cursor and therefore one
|
|
55
|
+
* `CheckpointKey`, one view store and one durability class — which is why a
|
|
56
|
+
* composed bundle is expressed as ONE `ReadModel` over several slices rather than
|
|
57
|
+
* several `ReadModel`s over one subscription.
|
|
58
|
+
*
|
|
59
|
+
* That bound is about sharing one SUBSCRIPTION, and the boundary is worth stating
|
|
60
|
+
* because the sentence above otherwise reads as one materialisation per read model.
|
|
61
|
+
* It says nothing about materialising ONE read model more than once: several
|
|
62
|
+
* materialisations of one read model deliberately take N subscriptions, N cursors
|
|
63
|
+
* and N checkpoints, exactly because R1 and R2 forbid them to share any of the
|
|
64
|
+
* three. Wire that with `runProjections`, which holds one `slices` value and one
|
|
65
|
+
* `ProjectionId` across N materialisations so their query and their decode cannot
|
|
66
|
+
* diverge, while each keeps its own store, its own `apply`, its own checkpoint and
|
|
67
|
+
* its own supervision hooks. What it does NOT hold is the folds, so agreement
|
|
68
|
+
* between the figures two materialisations arrive at stays empirical.
|
|
69
|
+
*/
|
|
70
|
+
export interface ReadModel<T extends Record<string, SliceProjection>, E = never, R = never> {
|
|
71
|
+
/**
|
|
72
|
+
* The view store this read model materialises into, ALREADY BOUND to the
|
|
73
|
+
* checkpoint it advances (`forKey(store, key)`).
|
|
74
|
+
*
|
|
75
|
+
* A VALUE on the read model rather than a context tag, because the view store
|
|
76
|
+
* is selected per MATERIALISATION: one deployment can run an in-memory admin view
|
|
77
|
+
* and a durable production view against the same event log, and a slice can
|
|
78
|
+
* graduate from the first to the second with its `slices`, its composed query,
|
|
79
|
+
* its `CheckpointKey` and this runner untouched. What necessarily changes is
|
|
80
|
+
* this field and `apply` below — the two that name the view store — and nothing
|
|
81
|
+
* else.
|
|
82
|
+
*
|
|
83
|
+
* Graduation is SEQUENTIAL, memory and then Postgres, and it is not the only
|
|
84
|
+
* shape the selection permits: the same slice record can feed both AT ONCE, which
|
|
85
|
+
* is what `runProjections` wires. This field stays SINGULAR either way — one
|
|
86
|
+
* materialisation, one store, one key, one runner — and N materialisations are N
|
|
87
|
+
* of these values over one `slices` value, never one value holding a list. A list
|
|
88
|
+
* would imply one atomic `commit` across two stores, which R1 says is unsound;
|
|
89
|
+
* the demonstration of the simultaneous shape is
|
|
90
|
+
* `@kairos-es/read-postgres`'s `test/courseRosterGraduation.test.ts`, and the
|
|
91
|
+
* worked production wiring is `examples/course-subscriptions`'s
|
|
92
|
+
* `src/courseRosterMaterialisations.ts`.
|
|
93
|
+
*
|
|
94
|
+
* KEYED rather than a `(key, store)` pair, for the reason `KeyedProjectionStore`
|
|
95
|
+
* in `ProjectionStore.ts` argues — it owns why binding a key to a store turns
|
|
96
|
+
* "one materialisation, one key, one runner" from convention into structure. What
|
|
97
|
+
* is local to this field is that the key stays readable as `store.key`, which is
|
|
98
|
+
* where the runner's log annotations and failure payloads get it.
|
|
99
|
+
*/
|
|
100
|
+
readonly store: KeyedProjectionStore;
|
|
101
|
+
/** The slices whose composed query drives the subscription and whose schemas decode. */
|
|
102
|
+
readonly slices: T;
|
|
103
|
+
/**
|
|
104
|
+
* The per-batch view writes, handed to `ProjectionStore.commit` so they land
|
|
105
|
+
* inside the same transaction as the checkpoint advance.
|
|
106
|
+
*
|
|
107
|
+
* The batch is non-empty by construction, so `apply` needs no empty case. If
|
|
108
|
+
* the store is NON-transactional this must be a SINGLE atomic effect (fold the
|
|
109
|
+
* batch purely, then one write — see `foldIntoRef`): the port cannot roll back
|
|
110
|
+
* a multi-write `apply` that fails part-way, and that requirement, not the
|
|
111
|
+
* store, is where the obligation sits.
|
|
112
|
+
*/
|
|
113
|
+
readonly apply: (batch: Arr.NonEmptyReadonlyArray<DecodedEvent>) => Effect.Effect<void, E, R>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Start the daemon maintaining `readModel`, forked into the caller's `Scope`.
|
|
117
|
+
*
|
|
118
|
+
* The returned effect is INFALLIBLE (`E = never`): starting a projection cannot
|
|
119
|
+
* fail, because everything that can go wrong once it is running is the
|
|
120
|
+
* supervisor's business and lives on the fibre's error channel.
|
|
121
|
+
*
|
|
122
|
+
* ## What can go wrong BEFORE the fork is a programming error, and those are defects
|
|
123
|
+
*
|
|
124
|
+
* Which wirings are refused on the READ SIDE'S rules, why each one is a wiring
|
|
125
|
+
* nothing at runtime would ever notice, and the order they are judged in are all
|
|
126
|
+
* `projectionWiringFault`'s — one gate over five rules, whose only relation to this
|
|
127
|
+
* function is that it is the one place they can be applied before anything exists to
|
|
128
|
+
* be wrong. The gate judges a SET of materialisations, and this function supplies a
|
|
129
|
+
* SINGLETON: the two PER-ENTRY rungs judge that one wiring, the two that are
|
|
130
|
+
* functions of the shared query and slice record judge it once and would do so
|
|
131
|
+
* whatever N was, and the collision rung is a property of the set and is vacuous over
|
|
132
|
+
* one entry, having no sibling to be paired with. That is also why nothing here carries
|
|
133
|
+
* an entry index — the gate names one only where there is a set to index into.
|
|
134
|
+
* A further refusal is the CODEC's and fires just below the gate; `prepareProjection`
|
|
135
|
+
* owns that ordering for both entry points, and the gate's doc says why the rule
|
|
136
|
+
* stays in the codec. This function keeps the two claims that are its own.
|
|
137
|
+
*
|
|
138
|
+
* The verdict is a DEFECT rather than a value on the error channel, because no
|
|
139
|
+
* application can handle its own mis-wiring: the only repair is to change the
|
|
140
|
+
* wiring, and a channel value would ask every caller to write a handler for a
|
|
141
|
+
* mistake that has already been made by the time the program runs. `Effect.die`
|
|
142
|
+
* with an `Error` is the same classification the store gives a mis-built query
|
|
143
|
+
* through `assertServableQuery`, which is one of the rules.
|
|
144
|
+
*
|
|
145
|
+
* And it lands on THIS effect before the `forkScoped` at the foot of `buildAndFork`:
|
|
146
|
+
* the gate is consulted while the daemon is still being
|
|
147
|
+
* assembled, so a rejected wiring has read no checkpoint, opened no subscription
|
|
148
|
+
* and left no half-started fibre behind. That is what makes the defect safe to
|
|
149
|
+
* raise rather than merely early, and every construction case in this package
|
|
150
|
+
* asserts the stored checkpoint is still `ORIGIN` afterwards to keep it true.
|
|
151
|
+
*
|
|
152
|
+
* The preamble on the message is written HERE, once, naming the projection and the
|
|
153
|
+
* partition — the gate returns the sentence about the RULE, and this function knows
|
|
154
|
+
* WHO broke it. The single `Effect.die` that joins them is `prepareProjection`'s,
|
|
155
|
+
* which is why there is one for both entry points rather than one per rule.
|
|
156
|
+
*/
|
|
157
|
+
export declare const runProjection: <T extends Record<string, SliceProjection>, E, R>(readModel: ReadModel<T, E, R>, options?: ProjectionRunnerOptions) => Effect.Effect<ProjectionRunner, never, Scope.Scope | DcbEventStore | EventLogDurability | Serializer | RequirementOf<T> | R>;
|
|
158
|
+
/**
|
|
159
|
+
* `runProjection` as a `Layer` whose own scope owns the daemon.
|
|
160
|
+
*
|
|
161
|
+
* It provides NOTHING (`ROut = never`): a projection is a background process, not
|
|
162
|
+
* a service, and nothing should be able to depend on it as one — a read model is
|
|
163
|
+
* queried through its view store, never through the runner. What the `Layer` buys
|
|
164
|
+
* is lifecycle: dropped into an application's layer graph, the daemon starts when
|
|
165
|
+
* the graph is built and is interrupted when it is released, alongside the store
|
|
166
|
+
* layers it reads from, with no bespoke startup or shutdown code.
|
|
167
|
+
*
|
|
168
|
+
* Discarding the runner discards its fibre too, which is correct and has one
|
|
169
|
+
* consequence worth wiring for: under this `Layer` — the recommended shape — a
|
|
170
|
+
* `ProjectionStalled` has no fibre to be awaited on, so it reaches the outside
|
|
171
|
+
* world only through the supervisor's `Effect.logError` and through `onStalled`.
|
|
172
|
+
* That hook is the reason a stall need not be a log line somebody happens to grep
|
|
173
|
+
* for; pass one in `options` if a stalled projection should fail a health check,
|
|
174
|
+
* exit non-zero, or page.
|
|
175
|
+
*
|
|
176
|
+
* `Layer.scopedDiscard` discharges the `Scope` the fork needs; the store, its
|
|
177
|
+
* `EventLogDurability` declaration, the `Serializer`, the slices' requirements and
|
|
178
|
+
* the read model's own `R` stay on the layer's inputs, so they are satisfied the
|
|
179
|
+
* same way every other layer's are. `EventLogDurability` being an INPUT is what
|
|
180
|
+
* makes the wiring read correctly: the declaration is provided once, alongside the
|
|
181
|
+
* `DcbEventStore` layer it describes, and every projection layer in the graph is
|
|
182
|
+
* then satisfied from that one declaration.
|
|
183
|
+
*/
|
|
184
|
+
export declare const projectionLayer: <T extends Record<string, SliceProjection>, E, R>(readModel: ReadModel<T, E, R>, options?: ProjectionRunnerOptions) => Layer.Layer<never, never, DcbEventStore | EventLogDurability | Serializer | RequirementOf<T> | R>;
|
|
185
|
+
//# sourceMappingURL=runProjection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runProjection.d.ts","sourceRoot":"","sources":["../../src/runProjection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,eAAe,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,KAAK,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAA;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAG7B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,SAAS,CACxB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,CAAC,GAAG,KAAK,EACT,CAAC,GAAG,KAAK;IAET;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAA;IAEpC,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;IAElB;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,KAAK,EAAE,GAAG,CAAC,qBAAqB,CAAC,YAAY,CAAC,KAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,CAAC,EAC3E,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,UAAU,uBAAuB,KAChC,MAAM,CAAC,MAAM,CACd,gBAAgB,EAChB,KAAK,EACH,KAAK,CAAC,KAAK,GACX,aAAa,GACb,kBAAkB,GAClB,UAAU,GACV,aAAa,CAAC,CAAC,CAAC,GAChB,CAAC,CA2CD,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,eAAe,GAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,CAAC,EACD,CAAC,EAED,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,UAAU,uBAAuB,KAChC,KAAK,CAAC,KAAK,CACZ,KAAK,EACL,KAAK,EACL,aAAa,GAAG,kBAAkB,GAAG,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CACb,CAAA"}
|