@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,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-memory `ProjectionStore`: a `Ref` of checkpoints plus the exclusive
|
|
3
|
+
* critical section that makes the guarded advance atomic.
|
|
4
|
+
*
|
|
5
|
+
* This is NOT merely a test double. It is load-bearing for the in-process
|
|
6
|
+
* supervision restart, where the subscription dies (the event store's poll retry
|
|
7
|
+
* exhausted) but the process and its in-memory view survive: the restart must
|
|
8
|
+
* resume from the checkpoint rather than replay from `ORIGIN` into an
|
|
9
|
+
* already-populated view, and an ephemeral checkpoint stored beside an ephemeral
|
|
10
|
+
* view is exactly what makes that resume correct. It is also the whole
|
|
11
|
+
* all-in-memory permutation — tests, prototypes, client-side use — and the
|
|
12
|
+
* durable-log-with-in-memory-view mode's view store (dev, admin and in-flux
|
|
13
|
+
* slices, rebuilt from `ORIGIN` each boot).
|
|
14
|
+
*
|
|
15
|
+
* A factory `Effect`, deliberately not a `Layer`: the view store is selected PER
|
|
16
|
+
* MATERIALISATION — so a slice may start in memory and GRADUATE to Postgres, which
|
|
17
|
+
* `ReadModel.store` owns the claim about — and it is therefore a value handed to a
|
|
18
|
+
* read model, not a context tag that one store could claim process-wide.
|
|
19
|
+
*
|
|
20
|
+
* That selection is a FLOOR rather than a ceiling, and this store is on the common
|
|
21
|
+
* side of it: the two variants above are also available AT ONCE, an in-memory
|
|
22
|
+
* materialisation of a slice record beside a durable one from the SAME record —
|
|
23
|
+
* a Postgres table somebody queries with SQL, and an in-process view of the same
|
|
24
|
+
* slices holding one aggregate's hot figures with no round trip to reach them. That
|
|
25
|
+
* is `runProjections`, and every call of this factory is an independent store, so
|
|
26
|
+
* two materialisations wired that way share nothing but their slices. What they do
|
|
27
|
+
* not share is their FOLDS: each `apply` re-expresses one, so agreement between
|
|
28
|
+
* their figures is something a test demonstrates rather than something the wiring
|
|
29
|
+
* gives.
|
|
30
|
+
*
|
|
31
|
+
* The critical-section design mirrors `core`'s in-memory event store: one
|
|
32
|
+
* `Effect.makeSemaphore(1)` serialises the guarded advance, exactly as that store
|
|
33
|
+
* serialises check-then-append, and for the same reason — the check and the write
|
|
34
|
+
* must not be separable, and that atomicity IS the concurrency guarantee.
|
|
35
|
+
*/
|
|
36
|
+
import { ORIGIN, type Position } from '@kairos-es/core'
|
|
37
|
+
import { Effect, Ref } from 'effect'
|
|
38
|
+
import type { CheckpointKey, ProjectionStore } from './ProjectionStore'
|
|
39
|
+
import { CheckpointSuperseded } from './ProjectionStore'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Flatten a `CheckpointKey` into a map key.
|
|
43
|
+
*
|
|
44
|
+
* The separator is NUL. A `ProjectionId`/`PartitionId` is any non-empty string,
|
|
45
|
+
* so a printable separator (`:`, `/`, `|`) could be forged inside either half and
|
|
46
|
+
* two distinct keys would collide onto one checkpoint — one read model silently
|
|
47
|
+
* reading and advancing another's cursor. NUL cannot appear in a name any human
|
|
48
|
+
* or config file produces, so the flattening stays injective in practice without
|
|
49
|
+
* narrowing the id grammar.
|
|
50
|
+
*
|
|
51
|
+
* The other route to that same failure is a WIRING rather than a forged id: two
|
|
52
|
+
* materialisations handed this one store under equal keys, which would be two
|
|
53
|
+
* runners over one cursor. That one is caught before anything is forked, by the
|
|
54
|
+
* set-level collision rung of the construction gate in `projectionWiringFault.ts`,
|
|
55
|
+
* and its sentence names the two fixes. Nothing here can catch it — a store sees
|
|
56
|
+
* keys, never who is holding it.
|
|
57
|
+
*/
|
|
58
|
+
const mapKey = (key: CheckpointKey): string =>
|
|
59
|
+
`${key.projection}\u0000${key.partition}`
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build an in-memory `ProjectionStore`.
|
|
63
|
+
*
|
|
64
|
+
* Every call is an independent store with its own checkpoints and its own
|
|
65
|
+
* semaphore, so two read models wired to two calls of this factory cannot
|
|
66
|
+
* interfere — and a test needing a fresh store just calls it again.
|
|
67
|
+
*/
|
|
68
|
+
export const makeInMemoryProjectionStore: Effect.Effect<ProjectionStore> =
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const checkpoints = yield* Ref.make<ReadonlyMap<string, Position>>(
|
|
71
|
+
new Map(),
|
|
72
|
+
)
|
|
73
|
+
const mutex = yield* Effect.makeSemaphore(1)
|
|
74
|
+
|
|
75
|
+
/** The stored position, or `ORIGIN` for a key never committed. */
|
|
76
|
+
const positionOf = (
|
|
77
|
+
stored: ReadonlyMap<string, Position>,
|
|
78
|
+
key: CheckpointKey,
|
|
79
|
+
): Position => stored.get(mapKey(key)) ?? ORIGIN
|
|
80
|
+
|
|
81
|
+
const readCheckpoint = (
|
|
82
|
+
key: CheckpointKey,
|
|
83
|
+
): Effect.Effect<Position, never> =>
|
|
84
|
+
// No lock needed: `Ref.get` is a single atomic read, and the guard that
|
|
85
|
+
// actually protects a commit is re-checked INSIDE the critical section
|
|
86
|
+
// below — a value read here is only ever an expectation to be guarded on,
|
|
87
|
+
// never a licence to write.
|
|
88
|
+
Effect.map(Ref.get(checkpoints), (stored) => positionOf(stored, key))
|
|
89
|
+
|
|
90
|
+
const commit = <E, R>(args: {
|
|
91
|
+
readonly key: CheckpointKey
|
|
92
|
+
readonly expected: Position
|
|
93
|
+
readonly next: Position
|
|
94
|
+
readonly viewWrites: Effect.Effect<void, E, R>
|
|
95
|
+
}): Effect.Effect<void, E | CheckpointSuperseded, R> =>
|
|
96
|
+
// The permit is acquired INTERRUPTIBLY (a fibre waiting its turn can still
|
|
97
|
+
// be torn down), and only the section itself is uninterruptible.
|
|
98
|
+
mutex.withPermits(1)(
|
|
99
|
+
Effect.uninterruptible(
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const stored = positionOf(yield* Ref.get(checkpoints), args.key)
|
|
102
|
+
|
|
103
|
+
// The guard is checked FIRST, before the view writes run. There is no
|
|
104
|
+
// transaction here to roll anything back, so the ONLY way a lost
|
|
105
|
+
// guard can leave the view untouched is to lose before touching it.
|
|
106
|
+
// Check-then-act is safe despite `viewWrites` suspending in between,
|
|
107
|
+
// because the section is exclusive: no other fibre can observe or
|
|
108
|
+
// advance this checkpoint until the permit is released, so the value
|
|
109
|
+
// read here cannot go stale within the section.
|
|
110
|
+
//
|
|
111
|
+
// On a lost guard `viewWrites` was NEVER RUN, which is observably
|
|
112
|
+
// identical to the SQL store's rollback — the port's abort contract
|
|
113
|
+
// is met by omission rather than by undo. (The one case omission
|
|
114
|
+
// cannot cover is a `viewWrites` that fails part-way, which is why
|
|
115
|
+
// the port requires a non-transactional store's per-batch write to be
|
|
116
|
+
// a single atomic effect; see `ProjectionStore` and `foldIntoRef`.)
|
|
117
|
+
if (stored !== args.expected) {
|
|
118
|
+
return yield* new CheckpointSuperseded({
|
|
119
|
+
key: args.key,
|
|
120
|
+
expected: args.expected,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
yield* args.viewWrites
|
|
125
|
+
|
|
126
|
+
// Uninterruptibility earns its keep here: an interrupt landing
|
|
127
|
+
// between the view writes and this advance would commit the batch's
|
|
128
|
+
// effects with the cursor left behind, and the next run would apply
|
|
129
|
+
// the same batch a second time. Copy-on-write so a concurrent reader
|
|
130
|
+
// holding the previous map sees a consistent snapshot.
|
|
131
|
+
yield* Ref.update(checkpoints, (current) =>
|
|
132
|
+
new Map(current).set(mapKey(args.key), args.next),
|
|
133
|
+
)
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const resetCheckpoint = (key: CheckpointKey): Effect.Effect<void> =>
|
|
139
|
+
// Under the same permit as `commit`, so a reset can never interleave with a
|
|
140
|
+
// guarded advance and leave the map half-updated relative to the view.
|
|
141
|
+
// Deleting rather than storing `ORIGIN` keeps "never committed" and "reset"
|
|
142
|
+
// one state, which is what the port promises the read-back is.
|
|
143
|
+
mutex.withPermits(1)(
|
|
144
|
+
Ref.update(checkpoints, (current) => {
|
|
145
|
+
const next = new Map(current)
|
|
146
|
+
next.delete(mapKey(key))
|
|
147
|
+
return next
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Annotated at the definition site rather than only through the exported
|
|
152
|
+
// signature, per the rule on `ProjectionStore` itself.
|
|
153
|
+
const store: ProjectionStore = {
|
|
154
|
+
// Dies with the process, which is the R2 input: an ephemeral view is legal
|
|
155
|
+
// over either an ephemeral or a durable log (it just re-catches-up), so
|
|
156
|
+
// this value never trips the durability-ordering check.
|
|
157
|
+
durability: 'ephemeral',
|
|
158
|
+
readCheckpoint,
|
|
159
|
+
commit,
|
|
160
|
+
resetCheckpoint,
|
|
161
|
+
}
|
|
162
|
+
return store
|
|
163
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
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 {
|
|
178
|
+
DurableEventLog,
|
|
179
|
+
EphemeralEventLog,
|
|
180
|
+
EventLogDurability,
|
|
181
|
+
} from './EventLogDurability'
|
|
182
|
+
export { foldIntoRef } from './foldIntoRef'
|
|
183
|
+
export { makeInMemoryProjectionStore } from './inMemoryProjectionStore'
|
|
184
|
+
export {
|
|
185
|
+
BatchSize,
|
|
186
|
+
PipelineDied,
|
|
187
|
+
type ProjectionPipelineOptions,
|
|
188
|
+
type ProjectionRunner,
|
|
189
|
+
type ProjectionRunnerOptions,
|
|
190
|
+
} from './ProjectionRunner'
|
|
191
|
+
export {
|
|
192
|
+
type CheckpointKey,
|
|
193
|
+
CheckpointSuperseded,
|
|
194
|
+
checkpointKey,
|
|
195
|
+
DEFAULT_PARTITION,
|
|
196
|
+
type Durability,
|
|
197
|
+
forKey,
|
|
198
|
+
type KeyedProjectionStore,
|
|
199
|
+
PartitionId,
|
|
200
|
+
ProjectionId,
|
|
201
|
+
type ProjectionStore,
|
|
202
|
+
ProjectionStoreError,
|
|
203
|
+
} from './ProjectionStore'
|
|
204
|
+
export {
|
|
205
|
+
projectionLayer,
|
|
206
|
+
type ReadModel,
|
|
207
|
+
runProjection,
|
|
208
|
+
} from './runProjection'
|
|
209
|
+
export {
|
|
210
|
+
type Materialisation,
|
|
211
|
+
type MaterialisedProjection,
|
|
212
|
+
runProjections,
|
|
213
|
+
} from './runProjections'
|
|
214
|
+
export {
|
|
215
|
+
MaxNoProgressRestarts,
|
|
216
|
+
ProjectionStalled,
|
|
217
|
+
type ProjectionSupervisionOptions,
|
|
218
|
+
} from './superviseOnProgress'
|