@kairos-es/core 1.0.0-alpha.4 → 1.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cjs/AppendCondition.js +1 -3
- package/dist/cjs/AppendCondition.js.map +1 -1
- package/dist/cjs/AppendIsOwned.js +6 -6
- package/dist/cjs/AppendIsOwned.js.map +1 -1
- package/dist/cjs/ContractStore.js +2 -2
- package/dist/cjs/ContractStore.js.map +1 -1
- package/dist/cjs/DcbEventStore.js +1 -1
- package/dist/cjs/DcbEventStore.js.map +1 -1
- package/dist/cjs/Event.js +29 -13
- package/dist/cjs/Event.js.map +1 -1
- package/dist/cjs/Query.js +1 -1
- package/dist/cjs/Query.js.map +1 -1
- package/dist/cjs/SubscribeMachine.js +158 -67
- package/dist/cjs/SubscribeMachine.js.map +1 -1
- package/dist/cjs/inMemory.js +4 -4
- package/dist/cjs/inMemory.js.map +1 -1
- package/dist/cjs/retryOnConflict.js +4 -10
- package/dist/cjs/retryOnConflict.js.map +1 -1
- package/dist/cjs/testing.js +2 -2
- package/dist/cjs/testing.js.map +1 -1
- package/dist/{dts → dts-cjs}/AppendCondition.d.ts +5 -7
- package/dist/{dts → dts-cjs}/AppendCondition.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/AppendIsOwned.d.ts +6 -6
- package/dist/{dts → dts-cjs}/AppendIsOwned.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/ContractStore.d.ts +3 -2
- package/dist/dts-cjs/ContractStore.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/DcbEventStore.d.ts +2 -2
- package/dist/{dts → dts-cjs}/DcbEventStore.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/Event.d.ts +31 -20
- package/dist/dts-cjs/Event.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/Query.d.ts +5 -5
- package/dist/dts-cjs/SubscribeMachine.d.ts +289 -0
- package/dist/dts-cjs/SubscribeMachine.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/inMemory.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/index.d.ts +1 -1
- package/dist/dts-cjs/package.json +3 -0
- package/dist/{dts → dts-cjs}/retryOnConflict.d.ts +1 -7
- package/dist/dts-cjs/retryOnConflict.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/testing.d.ts +1 -1
- package/dist/dts-esm/AppendCondition.d.ts +21 -0
- package/dist/dts-esm/AppendCondition.d.ts.map +1 -0
- package/dist/dts-esm/AppendIsOwned.d.ts +78 -0
- package/dist/dts-esm/AppendIsOwned.d.ts.map +1 -0
- package/dist/dts-esm/ContractStore.d.ts +62 -0
- package/dist/dts-esm/ContractStore.d.ts.map +1 -0
- package/dist/dts-esm/DcbEventStore.d.ts +64 -0
- package/dist/dts-esm/DcbEventStore.d.ts.map +1 -0
- package/dist/dts-esm/Event.d.ts +174 -0
- package/dist/dts-esm/Event.d.ts.map +1 -0
- package/dist/dts-esm/Projection.d.ts +124 -0
- package/dist/dts-esm/Projection.d.ts.map +1 -0
- package/dist/dts-esm/Query.d.ts +180 -0
- package/dist/dts-esm/Query.d.ts.map +1 -0
- package/dist/dts-esm/SubscribeMachine.d.ts +289 -0
- package/dist/dts-esm/SubscribeMachine.d.ts.map +1 -0
- package/dist/dts-esm/errors.d.ts +17 -0
- package/dist/dts-esm/errors.d.ts.map +1 -0
- package/dist/dts-esm/inMemory.d.ts +76 -0
- package/dist/dts-esm/inMemory.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +40 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/retryOnConflict.d.ts +64 -0
- package/dist/dts-esm/retryOnConflict.d.ts.map +1 -0
- package/dist/dts-esm/testing.d.ts +39 -0
- package/dist/dts-esm/testing.d.ts.map +1 -0
- package/dist/esm/AppendCondition.js +1 -3
- package/dist/esm/AppendCondition.js.map +1 -1
- package/dist/esm/AppendIsOwned.js +6 -6
- package/dist/esm/AppendIsOwned.js.map +1 -1
- package/dist/esm/ContractStore.js +2 -2
- package/dist/esm/ContractStore.js.map +1 -1
- package/dist/esm/DcbEventStore.js +1 -1
- package/dist/esm/DcbEventStore.js.map +1 -1
- package/dist/esm/Event.js +30 -14
- package/dist/esm/Event.js.map +1 -1
- package/dist/esm/Query.js +1 -1
- package/dist/esm/Query.js.map +1 -1
- package/dist/esm/SubscribeMachine.js +157 -67
- package/dist/esm/SubscribeMachine.js.map +1 -1
- package/dist/esm/inMemory.js +5 -5
- package/dist/esm/inMemory.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/retryOnConflict.js +4 -10
- package/dist/esm/retryOnConflict.js.map +1 -1
- package/dist/esm/testing.js +2 -2
- package/dist/esm/testing.js.map +1 -1
- package/package.json +21 -11
- package/src/AppendCondition.ts +1 -1
- package/src/AppendIsOwned.ts +7 -7
- package/src/ContractStore.ts +5 -4
- package/src/DcbEventStore.ts +3 -3
- package/src/Event.ts +49 -14
- package/src/Query.ts +1 -1
- package/src/SubscribeMachine.ts +278 -96
- package/src/inMemory.ts +6 -6
- package/src/index.ts +1 -1
- package/src/retryOnConflict.ts +6 -11
- package/src/testing.ts +2 -2
- package/testing/package.json +1 -1
- package/dist/dts/ContractStore.d.ts.map +0 -1
- package/dist/dts/Event.d.ts.map +0 -1
- package/dist/dts/SubscribeMachine.d.ts +0 -179
- package/dist/dts/SubscribeMachine.d.ts.map +0 -1
- package/dist/dts/retryOnConflict.d.ts.map +0 -1
- /package/dist/{dts → dts-cjs}/Projection.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/Projection.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/Query.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/errors.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/errors.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/inMemory.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/index.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/testing.d.ts.map +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AppendIsOwned` — the marker saying *"an append is already owned here"*, and the
|
|
3
|
+
* check that refuses a second one.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a write-side concept core does not need for its own job lives in core
|
|
6
|
+
*
|
|
7
|
+
* A bracket that derives one `AppendCondition` from every read its body performed
|
|
8
|
+
* (`@kairos-es/codec`'s `transact`) owns exactly one append: the one it issues at
|
|
9
|
+
* close, under that derived condition. Any OTHER append inside that body is a
|
|
10
|
+
* wiring mistake, and the dangerous case is not the one that looks dangerous. A
|
|
11
|
+
* command runner appending mid-body usually writes into the very slice the body
|
|
12
|
+
* read, so its events match the union the bracket is about to guard on — and the
|
|
13
|
+
* bracket's own closing append is then refused by a condition its own body
|
|
14
|
+
* invalidated. The failure arrives at the wrong line, describing a conflict that
|
|
15
|
+
* was self-inflicted.
|
|
16
|
+
*
|
|
17
|
+
* The refusal therefore has to catch a RAW `store.append` and not merely a nested
|
|
18
|
+
* bracket, which is what puts it here rather than in the codec. Enforcing it one
|
|
19
|
+
* layer up would catch a nested bracket and a command runner and miss a direct
|
|
20
|
+
* store call — and a syntactic lint rule cannot close that gap either, having no
|
|
21
|
+
* call graph, so an append one function away in a helper is invisible to both.
|
|
22
|
+
* `makeContractStore` is the one seam every engine's append already passes
|
|
23
|
+
* through, so the check is written once and inherited whole, exactly as
|
|
24
|
+
* `assertServableQuery` is.
|
|
25
|
+
*
|
|
26
|
+
* The cost is real and is accepted: core learns a concept it does not need for its
|
|
27
|
+
* own job. It is affordable because core already owns the write contract, and
|
|
28
|
+
* *"an append is already owned"* is a write-side fact about that contract rather
|
|
29
|
+
* than a fact about the codec.
|
|
30
|
+
*
|
|
31
|
+
* ## Present means refused, so a sanctioned append REMOVES the marker
|
|
32
|
+
*
|
|
33
|
+
* The check reads the service through `Effect.serviceOption`, so its ABSENCE is the
|
|
34
|
+
* normal case and costs the `append` signature nothing — `DcbEventStore.append`
|
|
35
|
+
* keeps `R = never` and no caller provides anything. Presence is what refuses.
|
|
36
|
+
*
|
|
37
|
+
* That shape decides how the two legitimate appends inside a bracket get through,
|
|
38
|
+
* and there is only one route: run with the marker STRIPPED from the context
|
|
39
|
+
* (`Effect.updateContext` over `Context.omit(AppendIsOwned)`). The bracket's own
|
|
40
|
+
* closing append is issued outside the scope it provided the marker in, and the
|
|
41
|
+
* codec's escape hatch strips it explicitly. Neither needs a second service, a
|
|
42
|
+
* sanctioning flag or a mutable cell, and there is no way to be exempt by
|
|
43
|
+
* accident: an append is either lexically outside the marked region or it says in
|
|
44
|
+
* as many words that it is removing the marker.
|
|
45
|
+
*/
|
|
46
|
+
import { Context, Effect } from 'effect';
|
|
47
|
+
declare const AppendIsOwned_base: Context.ServiceClass<AppendIsOwned, "@kairos-es/core/AppendIsOwned", {
|
|
48
|
+
readonly by: string;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Marks that an append is already owned by an enclosing bracket, and names what
|
|
52
|
+
* owns it.
|
|
53
|
+
*
|
|
54
|
+
* `by` is a human-readable owner name and exists for the defect message alone —
|
|
55
|
+
* the refusal is decided by the service's PRESENCE, never by this value, so nothing
|
|
56
|
+
* branches on it and no caller need agree on a vocabulary for it.
|
|
57
|
+
*/
|
|
58
|
+
export declare class AppendIsOwned extends AppendIsOwned_base {
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Die if an append is already owned here; do nothing otherwise.
|
|
62
|
+
*
|
|
63
|
+
* A DEFECT and not an error-channel value, deliberately. There is no
|
|
64
|
+
* data-dependent recovery from it: the events this append would have written
|
|
65
|
+
* belong to a bracket that is going to write its own, so no caller could sensibly
|
|
66
|
+
* catch it and carry on. It is the same class of fault as a non-servable query,
|
|
67
|
+
* and it is refused in the same place and in the same way. The remedy is one
|
|
68
|
+
* identifier away in every case — the codec's `decideOnly` for a command runner,
|
|
69
|
+
* its escape hatch for a deliberate second append — which is what makes a defect
|
|
70
|
+
* affordable here.
|
|
71
|
+
*
|
|
72
|
+
* `Effect.serviceOption` is what keeps this free: it reads the service if present and
|
|
73
|
+
* adds NOTHING to the requirement channel, so every existing append is unchanged
|
|
74
|
+
* and unprovisioned.
|
|
75
|
+
*/
|
|
76
|
+
export declare const assertAppendNotOwned: Effect.Effect<void>;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=AppendIsOwned.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppendIsOwned.d.ts","sourceRoot":"","sources":["../../src/AppendIsOwned.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;;iBAYvB,MAAM;;AAVvB;;;;;;;GAOG;AACH,qBAAa,aAAc,SAAQ,kBAGC;CAAG;AAEvC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAepD,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `makeContractStore` — the shared contract-enforcement POLICY every
|
|
3
|
+
* `DcbEventStore` engine wears, factored out of the engines so the policy is
|
|
4
|
+
* DEFINITIONALLY identical across backends rather than identical by inspection.
|
|
5
|
+
*
|
|
6
|
+
* Each engine implements only the MECHANICS (`RawDcbEventStore`): the actual
|
|
7
|
+
* read/append/subscribe, free to surface infrastructure faults on the error
|
|
8
|
+
* channel (`never` for the pure in-memory engine; `SqlError` for either SQL one).
|
|
9
|
+
* The decorator wraps those mechanics with the policy the contract demands — the
|
|
10
|
+
* policy the first two engines hand-duplicated, with the Postgres one even
|
|
11
|
+
* needing a bespoke `catchAll`/`instanceof` (Effect 3's `catchAll`, which Effect 4
|
|
12
|
+
* names `catch`):
|
|
13
|
+
*
|
|
14
|
+
* - `assertServableQuery` at every verb's entry (read, the append condition's
|
|
15
|
+
* guard query, subscribe) — a non-servable query, or one carrying a key the
|
|
16
|
+
* grammar does not define, is a DEFECT in every engine, and an append condition
|
|
17
|
+
* carrying a key `AppendCondition` does not define is refused alongside it;
|
|
18
|
+
* - `ReadLimit` decode on `read`'s `limit` — a negative/non-integer limit is a
|
|
19
|
+
* DEFECT (so `read` keeps `E = never`), validated through the branded schema
|
|
20
|
+
* even at an untyped boundary that reaches the store as `unknown`;
|
|
21
|
+
* - `orDie` on read/subscribe infrastructure errors — `read`/`subscribe` are
|
|
22
|
+
* `E = never` on the contract, so a raw fault is a defect, not a channel value;
|
|
23
|
+
* - the `AppendConditionFailed`-else-die classification on append, matched by
|
|
24
|
+
* `_tag` and never by class identity — ONLY the contract conflict rides the
|
|
25
|
+
* error channel; every other fault is a defect;
|
|
26
|
+
* - `assertAppendNotOwned` on append — an append inside a bracket that already
|
|
27
|
+
* owns one is a DEFECT. Read `AppendIsOwned.ts` for the rule and for why a
|
|
28
|
+
* write-side concept core does not need for its own job is enforced here.
|
|
29
|
+
*
|
|
30
|
+
* The throws are deferred into the returned Effect/Stream (via `Effect.suspend`
|
|
31
|
+
* / `Effect.sync`), so a validation failure surfaces as a DEFECT WHEN THE EFFECT
|
|
32
|
+
* RUNS — never as a synchronous throw at the call site. This gives the contract
|
|
33
|
+
* suite a single trusted choke point and makes a further backend impossible to get
|
|
34
|
+
* subtly wrong: it supplies mechanics, the policy is inherited whole. That is how
|
|
35
|
+
* the third engine, `@kairos-es/store-sqlite`, acquired the policy — it writes not
|
|
36
|
+
* a line of it.
|
|
37
|
+
*/
|
|
38
|
+
import type { Array as Arr, Scope } from 'effect';
|
|
39
|
+
import { Effect, Stream } from 'effect';
|
|
40
|
+
import { AppendCondition } from './AppendCondition.js';
|
|
41
|
+
import type { DcbEventStore, ReadOptions, ReadResponse } from './DcbEventStore.js';
|
|
42
|
+
import type { DcbEvent, Position, SequencedEvent } from './Event.js';
|
|
43
|
+
import type { AppendConditionFailed } from './errors.js';
|
|
44
|
+
import { type Query } from './Query.js';
|
|
45
|
+
/**
|
|
46
|
+
* The MECHANICS half of a store: the same three verbs as the `DcbEventStore`
|
|
47
|
+
* contract, but free to surface infrastructure faults (`E`) on the error
|
|
48
|
+
* channel. `E` is `never` for a pure engine (in-memory) and e.g. `SqlError` for
|
|
49
|
+
* a database engine. `append` may additionally fail with the contract's
|
|
50
|
+
* `AppendConditionFailed`. `makeContractStore` turns this into the contract.
|
|
51
|
+
*/
|
|
52
|
+
export interface RawDcbEventStore<E> {
|
|
53
|
+
readonly read: (query: Query, options?: ReadOptions) => Effect.Effect<ReadResponse, E>;
|
|
54
|
+
readonly append: (events: Arr.NonEmptyReadonlyArray<DcbEvent>, condition?: AppendCondition) => Effect.Effect<Position, AppendConditionFailed | E>;
|
|
55
|
+
readonly subscribe: (query: Query, after?: Position) => Stream.Stream<SequencedEvent, E, Scope.Scope>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Wrap a raw engine's mechanics with the shared contract-enforcement policy,
|
|
59
|
+
* producing a full `DcbEventStore` service value.
|
|
60
|
+
*/
|
|
61
|
+
export declare const makeContractStore: <E>(raw: RawDcbEventStore<E>) => DcbEventStore["Service"];
|
|
62
|
+
//# sourceMappingURL=ContractStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContractStore.d.ts","sourceRoot":"","sources":["../../src/ContractStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,MAAM,EAAqB,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC/E,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AACrD,OAAO,EAAuB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAA;AAKzD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,CACb,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,WAAW,KAClB,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAC3C,SAAS,CAAC,EAAE,eAAe,KACxB,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,CAAC,CAAC,CAAA;IACvD,QAAQ,CAAC,SAAS,EAAE,CAClB,KAAK,EAAE,KAAK,EACZ,KAAK,CAAC,EAAE,QAAQ,KACb,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;CACnD;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,KAAK,gBAAgB,CAAC,CAAC,CAAC,KACvB,aAAa,CAAC,SAAS,CAoGxB,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `DcbEventStore` contract: a `Context.Service` with no bundled implementation.
|
|
3
|
+
* Consumers supply a `Layer` (the in-memory one lives in this package; each SQL
|
|
4
|
+
* engine is a package of its own — `@kairos-es/store-postgres` and
|
|
5
|
+
* `@kairos-es/store-sqlite`).
|
|
6
|
+
*
|
|
7
|
+
* Head semantics (ported from `popo.py:62-65`):
|
|
8
|
+
* - `read` with no `limit` => `head` = the global last position (ignoring the
|
|
9
|
+
* query filter) if the store is non-empty, else `ORIGIN` (0n).
|
|
10
|
+
* - `read` with a `limit` => `head` = the last *returned* event's position
|
|
11
|
+
* (post-filter, post-limit), or `ORIGIN` if none returned.
|
|
12
|
+
*
|
|
13
|
+
* `append` returns the last new position (read-your-writes). `subscribe` is
|
|
14
|
+
* catch-up then live and requires a `Scope` so the live subscription is cleaned
|
|
15
|
+
* up on scope close.
|
|
16
|
+
*/
|
|
17
|
+
import type { Array as Arr, Effect, Scope, Stream } from 'effect';
|
|
18
|
+
import { Context } from 'effect';
|
|
19
|
+
import type { AppendCondition } from './AppendCondition.js';
|
|
20
|
+
import type { DcbEvent, Position, ReadLimit, SequencedEvent } from './Event.js';
|
|
21
|
+
import type { AppendConditionFailed } from './errors.js';
|
|
22
|
+
import type { Query } from './Query.js';
|
|
23
|
+
/** Options for a read: a lower position bound and/or a maximum count. */
|
|
24
|
+
export interface ReadOptions {
|
|
25
|
+
/** Only events strictly after this position (exclusive). Absent = from the start. */
|
|
26
|
+
readonly after?: Position;
|
|
27
|
+
/**
|
|
28
|
+
* Cap the number of events returned (a branded non-negative integer). `0`
|
|
29
|
+
* returns none; absent = all matching events. A negative/non-integer value is
|
|
30
|
+
* a defect at the store boundary — see `ReadLimit`.
|
|
31
|
+
*/
|
|
32
|
+
readonly limit?: ReadLimit;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The result of a read: a stream of matching events (ascending by position)
|
|
36
|
+
* plus the consistency `head`.
|
|
37
|
+
*/
|
|
38
|
+
export interface ReadResponse {
|
|
39
|
+
readonly events: Stream.Stream<SequencedEvent>;
|
|
40
|
+
readonly head: Position;
|
|
41
|
+
}
|
|
42
|
+
declare const DcbEventStore_base: Context.ServiceClass<DcbEventStore, "@kairos-es/core/DcbEventStore", {
|
|
43
|
+
/**
|
|
44
|
+
* Read events matching `query` (empty query = all), optionally bounded by
|
|
45
|
+
* `after`/`limit`. See head semantics in the module docs.
|
|
46
|
+
*/
|
|
47
|
+
readonly read: (query: Query, options?: ReadOptions) => Effect.Effect<ReadResponse>;
|
|
48
|
+
/**
|
|
49
|
+
* Append one or more events atomically. If `condition` is given, the append
|
|
50
|
+
* fails with `AppendConditionFailed` when its query matches any event after
|
|
51
|
+
* `condition.after`. Returns the last new position.
|
|
52
|
+
*/
|
|
53
|
+
readonly append: (events: Arr.NonEmptyReadonlyArray<DcbEvent>, condition?: AppendCondition) => Effect.Effect<Position, AppendConditionFailed>;
|
|
54
|
+
/**
|
|
55
|
+
* Subscribe to matching events: first the backlog after `after` (catch-up),
|
|
56
|
+
* then live events as they are appended. Scoped: the live subscription is
|
|
57
|
+
* released when the scope closes.
|
|
58
|
+
*/
|
|
59
|
+
readonly subscribe: (query: Query, after?: Position) => Stream.Stream<SequencedEvent, never, Scope.Scope>;
|
|
60
|
+
}>;
|
|
61
|
+
export declare class DcbEventStore extends DcbEventStore_base {
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=DcbEventStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DcbEventStore.d.ts","sourceRoot":"","sources":["../../src/DcbEventStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAChC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;IAC9C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CACxB;;IAKG;;;OAGG;mBACY,CACb,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,WAAW,KAClB,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;IAEhC;;;;OAIG;qBACc,CACf,MAAM,EAAE,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAC3C,SAAS,CAAC,EAAE,eAAe,KACxB,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IAEnD;;;;OAIG;wBACiB,CAClB,KAAK,EAAE,KAAK,EACZ,KAAK,CAAC,EAAE,QAAQ,KACb,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;;AA9B1D,qBAAa,aAAc,SAAQ,kBAgCC;CAAG"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core domain types for the DCB event log: branded scalars and the wire
|
|
3
|
+
* `DcbEvent`/`SequencedEvent` shapes.
|
|
4
|
+
*
|
|
5
|
+
* `data` is opaque bytes at the store layer; the codec (`@kairos-es/codec`) turns typed
|
|
6
|
+
* payloads into these bytes. Ordering is by `position` alone; `occurredAt` is
|
|
7
|
+
* producer-supplied domain time and is informational only.
|
|
8
|
+
*/
|
|
9
|
+
import { Schema } from 'effect';
|
|
10
|
+
/**
|
|
11
|
+
* A position in the single global event log: a branded, non-negative `bigint`.
|
|
12
|
+
*
|
|
13
|
+
* The log is 64-bit (`bigserial` in Postgres) and exceeds JS `number` safety at
|
|
14
|
+
* 2^53, so positions are a `bigint`. They are compared, never arithmetic'd.
|
|
15
|
+
* Encoded, a position is a decimal string (JSON-safe, `String(b)`); decoded, a
|
|
16
|
+
* `bigint`. `0n` is the "before the first event" sentinel used by `after` and by
|
|
17
|
+
* an empty store's head.
|
|
18
|
+
*
|
|
19
|
+
* The string-to-`bigint` step is hand-built rather than Effect 4's
|
|
20
|
+
* `Schema.BigIntFromString`, because that schema accepts only `^-?\d+$` and so
|
|
21
|
+
* refuses inputs the encoded side has always admitted — `" 5"`, `"+5"`,
|
|
22
|
+
* `"0x10"`. The decode here is the global `BigInt(s)` behind a blank-string
|
|
23
|
+
* refusal, which is `BigInt.fromString` and is exactly what Effect 3's
|
|
24
|
+
* `Schema.BigInt` ran, so a position string any earlier release decoded still
|
|
25
|
+
* decodes to the same value. The non-negative check then runs on the `bigint`,
|
|
26
|
+
* as it always did.
|
|
27
|
+
*/
|
|
28
|
+
export declare const Position: Schema.brand<Schema.decodeTo<Schema.BigInt, Schema.String, never, never>, "Position">;
|
|
29
|
+
export type Position = typeof Position.Type;
|
|
30
|
+
/** The sentinel position meaning "before the first event". */
|
|
31
|
+
export declare const ORIGIN: Position;
|
|
32
|
+
/**
|
|
33
|
+
* A business identifier attached to an event, following the `key:value`
|
|
34
|
+
* convention (e.g. `student:s1`). Tags replace the per-stream id of classic
|
|
35
|
+
* event sourcing; a query matches an event when the event carries all of an
|
|
36
|
+
* item's tags (superset test).
|
|
37
|
+
*/
|
|
38
|
+
export declare const Tag: Schema.brand<Schema.String, "Tag">;
|
|
39
|
+
export type Tag = typeof Tag.Type;
|
|
40
|
+
/**
|
|
41
|
+
* A reusable tag identity: a `key` plus a `.make(value)` that composes the
|
|
42
|
+
* branded `Tag` `` `${key}:${value}` `` through core's `Tag` schema. Define an
|
|
43
|
+
* identity once (`const CourseId = tagIdentity('course')`) and reuse
|
|
44
|
+
* `CourseId.make(id)` everywhere that entity is tagged. The `key` is exposed for
|
|
45
|
+
* key-aware tooling (e.g. a tag-vocabulary index, a future retag pass).
|
|
46
|
+
*
|
|
47
|
+
* The same constructor authors an event's tags (in a codec `tagsFor` mapper,
|
|
48
|
+
* ADR-0006) and a query's tags, so the `key:value` string a mapper writes and
|
|
49
|
+
* the string a query matches are produced by one function and cannot drift. It
|
|
50
|
+
* lives here beside `Tag` — pure (no clock, no RNG, no effect) — so it is
|
|
51
|
+
* reachable from every tag-authoring site, including core's own query
|
|
52
|
+
* constructors.
|
|
53
|
+
*
|
|
54
|
+
* `key` and `value` are each a single colon-free atom: the `key:value` grammar
|
|
55
|
+
* splits on the first colon, so both are validated to be non-empty and contain
|
|
56
|
+
* no `:` — `key` once at construction, `value` on each `make`. This keeps
|
|
57
|
+
* composition unambiguous and catches the double-prefix footgun loudly:
|
|
58
|
+
* `CourseId.make('course:123')` throws rather than silently yielding
|
|
59
|
+
* `course:course:123`. Raw `Tag.make` remains the escape hatch for a value that
|
|
60
|
+
* must itself contain a colon.
|
|
61
|
+
*/
|
|
62
|
+
export interface TagIdentity {
|
|
63
|
+
readonly key: string;
|
|
64
|
+
readonly make: (value: string) => Tag;
|
|
65
|
+
}
|
|
66
|
+
export declare const tagIdentity: (key: string) => TagIdentity;
|
|
67
|
+
/**
|
|
68
|
+
* A read cap: a branded non-negative integer bounding how many events a `read`
|
|
69
|
+
* returns. `0` returns none; an absent limit returns all matching events.
|
|
70
|
+
*
|
|
71
|
+
* The bound lives in the type, not merely in prose: a negative or non-integer
|
|
72
|
+
* limit is a programming error, so it is a DEFECT at the store boundary — the
|
|
73
|
+
* store validates through this schema and dies rather than surfacing it on the
|
|
74
|
+
* error channel (`read` keeps `E = never`). Constructed at call sites with
|
|
75
|
+
* `ReadLimit.make(n)`.
|
|
76
|
+
*/
|
|
77
|
+
export declare const ReadLimit: Schema.brand<Schema.Int, "ReadLimit">;
|
|
78
|
+
export type ReadLimit = typeof ReadLimit.Type;
|
|
79
|
+
/**
|
|
80
|
+
* An event type name, a branded non-empty string. Versioning is by convention
|
|
81
|
+
* in the name (e.g. `course-registered.v2`); only non-emptiness is enforced.
|
|
82
|
+
*/
|
|
83
|
+
export declare const EventType: Schema.brand<Schema.String, "EventType">;
|
|
84
|
+
export type EventType = typeof EventType.Type;
|
|
85
|
+
/**
|
|
86
|
+
* An event's metadata: a flat bag of strings carrying the circumstances under
|
|
87
|
+
* which the event was RECORDED, never anything the business asserts (ADR-0017).
|
|
88
|
+
* Payload is what happened; metadata is the context of the recording — a
|
|
89
|
+
* `traceparent`, a correlation or causation id, a deployment revision.
|
|
90
|
+
*
|
|
91
|
+
* Values are strings, not `unknown`, for cross-engine parity rather than taste:
|
|
92
|
+
* both SQL engines carry the bag as a JSON document, and `JSON.stringify` drops
|
|
93
|
+
* `undefined`, throws on `bigint` and turns `NaN` into `null`, none of which the
|
|
94
|
+
* in-memory oracle does.
|
|
95
|
+
*
|
|
96
|
+
* The key schema carries no refinement, deliberately: a refinement on a
|
|
97
|
+
* `Schema.Record` key acts as a FILTER on decode, silently dropping a key that
|
|
98
|
+
* fails it rather than failing the decode. Constraining keys, if ever wanted,
|
|
99
|
+
* has to be a validating step of its own.
|
|
100
|
+
*
|
|
101
|
+
* Two engine limits a consumer must know, because the bag is typically filled
|
|
102
|
+
* from outside the application at a request edge. Postgres refuses a NUL
|
|
103
|
+
* character (U+0000) and an unpaired surrogate in any string it stores, so an
|
|
104
|
+
* event whose bag holds one fails to append there while the in-memory and
|
|
105
|
+
* SQLite engines accept it; sanitise untrusted values before they reach the
|
|
106
|
+
* bag. And key ORDER is not preserved: Postgres stores an object's keys in an
|
|
107
|
+
* order of its own, so compare bags as unordered and never rely on the order
|
|
108
|
+
* `Object.keys` returns.
|
|
109
|
+
*
|
|
110
|
+
* Metadata is never part of a `Query` or an `AppendCondition` — a store refuses
|
|
111
|
+
* either when it carries a key the grammar does not define, so a metadata
|
|
112
|
+
* predicate cannot be written and silently ignored — never reaches a codec's
|
|
113
|
+
* `tagsFor`, and no store offers a way to rewrite it once appended. ADR-0017
|
|
114
|
+
* owns why each of those is mechanical rather than preferential.
|
|
115
|
+
*
|
|
116
|
+
* ## The one home of the shape rule
|
|
117
|
+
*
|
|
118
|
+
* The bag is a JSON OBJECT whose every value is a STRING, and this schema is
|
|
119
|
+
* where that rule is stated. At runtime it is checked in ONE place, at write: the
|
|
120
|
+
* codec's `toEvent` decodes every bag through this schema and refuses one that
|
|
121
|
+
* fails on its own error channel, so a value smuggled past the type (through an
|
|
122
|
+
* `any`) stops there. The schema judges a bag's OWN enumerable keys, so an object
|
|
123
|
+
* that keeps its entries anywhere else — a `Map`, a `Date` — reads as the empty
|
|
124
|
+
* bag rather than failing. A store checks no values: an event built by hand and handed
|
|
125
|
+
* to `append` directly carries whatever it carries, and what each engine then does
|
|
126
|
+
* with a non-string value differs — one may store it and read it back differently
|
|
127
|
+
* from another, and a `bigint` makes a SQL engine's append fail. Each
|
|
128
|
+
* SQL engine keeps a container check of its own on read — the column must hold a
|
|
129
|
+
* JSON object — because that defends against a writer outside the library and is
|
|
130
|
+
* about the column, not the values. A VALUE check must never be added on read
|
|
131
|
+
* alone: it would make an event the store accepted unreadable for ever, so a
|
|
132
|
+
* value check belongs at write or nowhere.
|
|
133
|
+
*/
|
|
134
|
+
export declare const Metadata: Schema.$Record<Schema.String, Schema.String>;
|
|
135
|
+
export type Metadata = typeof Metadata.Type;
|
|
136
|
+
/**
|
|
137
|
+
* An event as stored: an opaque payload plus its type, tags, a
|
|
138
|
+
* producer-supplied `uuid`, producer-supplied `occurredAt` domain time, and its
|
|
139
|
+
* `metadata` bag.
|
|
140
|
+
*
|
|
141
|
+
* `data` is `Uint8Array` (opaque bytes); the store does not interpret it.
|
|
142
|
+
* `uuid` is producer-supplied to keep `core` deterministic (no RNG in core).
|
|
143
|
+
* It is checked for UUID SHAPE only (`isGUID`), not for a version nibble: a
|
|
144
|
+
* stricter check would refuse ids an existing log already holds.
|
|
145
|
+
* `occurredAt` is informational only; ordering is by position alone.
|
|
146
|
+
* `metadata` mirrors `tags` rather than `data`: REQUIRED, with an empty bag
|
|
147
|
+
* legal. Required because an absent-versus-empty divergence between engines is
|
|
148
|
+
* invisible to `toEqual`, which ignores `undefined` properties, so an optional
|
|
149
|
+
* field could drift across the three engines with the round-trip suite green.
|
|
150
|
+
* Core only carries it; the codec's `toEvent` is where it is resolved.
|
|
151
|
+
*/
|
|
152
|
+
export declare const DcbEvent: Schema.Struct<{
|
|
153
|
+
readonly type: Schema.brand<Schema.String, "EventType">;
|
|
154
|
+
readonly data: Schema.Uint8Array;
|
|
155
|
+
readonly tags: Schema.$Array<Schema.brand<Schema.String, "Tag">>;
|
|
156
|
+
readonly uuid: Schema.String;
|
|
157
|
+
readonly occurredAt: Schema.DateTimeUtcFromString;
|
|
158
|
+
readonly metadata: Schema.$Record<Schema.String, Schema.String>;
|
|
159
|
+
}>;
|
|
160
|
+
export type DcbEvent = typeof DcbEvent.Type;
|
|
161
|
+
/** A persisted event: the event together with the position it was written at. */
|
|
162
|
+
export declare const SequencedEvent: Schema.Struct<{
|
|
163
|
+
readonly event: Schema.Struct<{
|
|
164
|
+
readonly type: Schema.brand<Schema.String, "EventType">;
|
|
165
|
+
readonly data: Schema.Uint8Array;
|
|
166
|
+
readonly tags: Schema.$Array<Schema.brand<Schema.String, "Tag">>;
|
|
167
|
+
readonly uuid: Schema.String;
|
|
168
|
+
readonly occurredAt: Schema.DateTimeUtcFromString;
|
|
169
|
+
readonly metadata: Schema.$Record<Schema.String, Schema.String>;
|
|
170
|
+
}>;
|
|
171
|
+
readonly position: Schema.brand<Schema.decodeTo<Schema.BigInt, Schema.String, never, never>, "Position">;
|
|
172
|
+
}>;
|
|
173
|
+
export type SequencedEvent = typeof SequencedEvent.Type;
|
|
174
|
+
//# sourceMappingURL=Event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Event.d.ts","sourceRoot":"","sources":["../../src/Event.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,MAAM,EAGP,MAAM,QAAQ,CAAA;AAEf;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ,uFAuBpB,CAAA;AACD,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C,8DAA8D;AAC9D,eAAO,MAAM,MAAM,EAAE,QAA4B,CAAA;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,GAAG,oCAGf,CAAA;AACD,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,IAAI,CAAA;AAEjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,CAAA;CACtC;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,WAiBzC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,uCAGrB,CAAA;AACD,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC,IAAI,CAAA;AAE7C;;;GAGG;AACH,eAAO,MAAM,SAAS,0CAGrB,CAAA;AACD,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC,IAAI,CAAA;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,QAAQ,8CAA8C,CAAA;AACnE,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,QAAQ;;;;;;;EAOnB,CAAA;AACF,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C,iFAAiF;AACjF,eAAO,MAAM,cAAc;;;;;;;;;;EAGzB,CAAA;AACF,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure projections and the composition primitive they fold through.
|
|
3
|
+
*
|
|
4
|
+
* A `Projection<S>` = `{ initialState, query, evolve }`: a query selecting the
|
|
5
|
+
* events it cares about and a pure `evolve` folding an already-*decoded* event
|
|
6
|
+
* into state. Decoding (wire bytes -> typed payload via `Schema`) is not core's
|
|
7
|
+
* job — it needs `Schema` and a `Serializer`, which core must not depend on —
|
|
8
|
+
* so `evolve` folds a `DecodedEvent` (the sequenced envelope plus an opaque
|
|
9
|
+
* `payload`). `composeProjections` merges several projections into one whose
|
|
10
|
+
* state is a named record.
|
|
11
|
+
*
|
|
12
|
+
* There is no builder and no decision-model orchestration here: both are the
|
|
13
|
+
* decode-aware `@kairos-es/codec` package's job (its `projection(...)` builder
|
|
14
|
+
* produces schema-backed projections, and its `modelAtHead` performs the effectful
|
|
15
|
+
* decode then folds through `composeProjections`, which `buildDecisionModel` builds
|
|
16
|
+
* on). Core keeps only the pure fold primitives.
|
|
17
|
+
*
|
|
18
|
+
* `Projection<S>` is *invariant* in `S`, so a heterogeneous record of
|
|
19
|
+
* projections (different state per key) cannot be bounded by
|
|
20
|
+
* `Projection<unknown>`. `AnyProjection` is the single authoritative,
|
|
21
|
+
* state-erased bound that owns that unsoundness in one place (built only via
|
|
22
|
+
* `AnyProjection.of`), so no downstream package writes `Projection<any>`; the
|
|
23
|
+
* precise per-key state is recovered by `StateOf` / `CompositeState`.
|
|
24
|
+
*
|
|
25
|
+
* The fold itself is pure and Effect-free.
|
|
26
|
+
*/
|
|
27
|
+
import type { SequencedEvent } from './Event.js';
|
|
28
|
+
import type { Query } from './Query.js';
|
|
29
|
+
/**
|
|
30
|
+
* An already-decoded event handed to a projection's fold: the sequenced wire
|
|
31
|
+
* envelope (position, type, tags, timings, metadata) plus the decoded `payload`.
|
|
32
|
+
*
|
|
33
|
+
* Core does not decode — it only folds events another layer has already decoded
|
|
34
|
+
* — so `payload` is `unknown` at this layer. `@kairos-es/codec`'s builder emits
|
|
35
|
+
* a projection whose `evolve` narrows `payload` per matched event type.
|
|
36
|
+
*/
|
|
37
|
+
export interface DecodedEvent {
|
|
38
|
+
readonly sequenced: SequencedEvent;
|
|
39
|
+
readonly payload: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A pure projection: the events it reads (`query`), its starting state, and a
|
|
43
|
+
* fold step.
|
|
44
|
+
*
|
|
45
|
+
* `evolve` contract: a projection is only ever invoked with an event that
|
|
46
|
+
* matches its `query`. `composeProjections` enforces this — it gates every
|
|
47
|
+
* sub-projection on its own query before dispatching — so an `evolve` may
|
|
48
|
+
* assume the event matched and need not re-check `matchesSequenced` itself. (It
|
|
49
|
+
* still dispatches by the event's type, which selects the fold; that is fold
|
|
50
|
+
* *selection*, not query gating.) `payload` has already been decoded by the
|
|
51
|
+
* codec layer.
|
|
52
|
+
*/
|
|
53
|
+
export interface Projection<S> {
|
|
54
|
+
readonly initialState: S;
|
|
55
|
+
readonly query: Query;
|
|
56
|
+
readonly evolve: (state: S, event: DecodedEvent) => S;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A state-erased view of a `Projection<S>`: the single authoritative bound for
|
|
60
|
+
* a *heterogeneous* record of projections. `Projection<S>` is invariant in `S`
|
|
61
|
+
* (S is both a parameter and the result of `evolve`), so `Record<string,
|
|
62
|
+
* Projection<unknown>>` rejects a record whose keys fold different state types.
|
|
63
|
+
* Rather than leak `Projection<any>` into every package that holds such a
|
|
64
|
+
* record, this type hides the invariant `evolve` behind an erased
|
|
65
|
+
* `(unknown) => unknown` surface and carries `S` only through the covariant
|
|
66
|
+
* `initialState`, so `Record<string, AnyProjection>` is sound with no `any`
|
|
67
|
+
* anywhere. Build one only via `AnyProjection.of`; recover the precise per-key
|
|
68
|
+
* `S` with `StateOf` / `CompositeState`. The `evolve` contract is the same as
|
|
69
|
+
* `Projection`'s: it is only ever called with an event matching `query`
|
|
70
|
+
* (`composeProjections` gates first), so an implementation may assume the match.
|
|
71
|
+
*/
|
|
72
|
+
export interface AnyProjection<out S = unknown> {
|
|
73
|
+
readonly initialState: S;
|
|
74
|
+
readonly query: Query;
|
|
75
|
+
readonly evolve: (state: unknown, event: DecodedEvent) => unknown;
|
|
76
|
+
}
|
|
77
|
+
export declare const AnyProjection: {
|
|
78
|
+
/**
|
|
79
|
+
* View a typed `Projection<S>` as an opaque `AnyProjection<S>`. This is the
|
|
80
|
+
* one place the invariant `evolve` is widened to the erased surface, so the
|
|
81
|
+
* unsoundness of a heterogeneous projection record is discharged here and
|
|
82
|
+
* nowhere else. `composeProjections` calls `evolve` with each projection's
|
|
83
|
+
* own state, so the widening is sound at runtime.
|
|
84
|
+
*/
|
|
85
|
+
readonly of: <S>(projection: Projection<S>) => AnyProjection<S>;
|
|
86
|
+
};
|
|
87
|
+
/** Recover the state type a projection folds to. */
|
|
88
|
+
export type StateOf<P> = P extends AnyProjection<infer S> ? S : never;
|
|
89
|
+
/**
|
|
90
|
+
* The record of named sub-states a composed projection (or decision model)
|
|
91
|
+
* folds to: each key's precise state type, recovered from its `AnyProjection`.
|
|
92
|
+
*/
|
|
93
|
+
export type CompositeState<T extends Record<string, AnyProjection>> = {
|
|
94
|
+
readonly [K in keyof T]: StateOf<T[K]>;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* OR-merge a list of queries into one. The empty list is the wildcard.
|
|
98
|
+
*
|
|
99
|
+
* The same merge `composeProjections` folds its record through, reachable
|
|
100
|
+
* WITHOUT a record — which is what a caller accumulating queries rather than
|
|
101
|
+
* projections needs, and the codec's read ledger is one. Without it that caller
|
|
102
|
+
* either hand-rolls the merge, or fabricates a projection per query to reach this
|
|
103
|
+
* one; both were tried and the first is a TRAP. An empty `items` is the WILDCARD
|
|
104
|
+
* and is absorbing under OR, while spreading an empty list contributes nothing, so
|
|
105
|
+
* a hand-rolled union silently narrows a match-everything query to its siblings —
|
|
106
|
+
* in the unsafe direction, and with nothing to notice.
|
|
107
|
+
*
|
|
108
|
+
* Seeding the fold with the wildcard would swallow everything for the same reason,
|
|
109
|
+
* so the list is reduced with no seed and the empty case answers directly.
|
|
110
|
+
*/
|
|
111
|
+
export declare const unionQueries: (queries: ReadonlyArray<Query>) => Query;
|
|
112
|
+
/**
|
|
113
|
+
* Compose named projections into one whose state is a record of sub-states and
|
|
114
|
+
* whose query is the OR-merge of all sub-queries. Each sub-projection only
|
|
115
|
+
* folds events matching its own query — the guarantee that `Projection.evolve`'s
|
|
116
|
+
* contract lets sub-projections rely on (so they need not re-gate themselves).
|
|
117
|
+
*
|
|
118
|
+
* The fold always writes each *matching* sub-result into a fresh record and
|
|
119
|
+
* returns it (no `changed` short-circuit, no `evolved !== prev` reference
|
|
120
|
+
* check): an in-place-mutating fold that returns the same reference would
|
|
121
|
+
* otherwise be silently dropped. At decision-model scale the extra copy is nil.
|
|
122
|
+
*/
|
|
123
|
+
export declare const composeProjections: <T extends Record<string, AnyProjection>>(projections: T) => Projection<CompositeState<T>>;
|
|
124
|
+
//# sourceMappingURL=Projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Projection.d.ts","sourceRoot":"","sources":["../../src/Projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAWpC;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,KAAK,CAAC,CAAA;CACtD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO;IAC5C,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAA;CAClE;AAED,eAAO,MAAM,aAAa;IACxB;;;;;;OAMG;kBACE,CAAC,cAAc,UAAU,CAAC,CAAC,CAAC,KAAG,aAAa,CAAC,CAAC,CAAC;CAE5C,CAAA;AAEV,oDAAoD;AACpD,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAErE;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI;IACpE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACvC,CAAA;AAqBD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS,aAAa,CAAC,KAAK,CAAC,KAAG,KAGT,CAAA;AAEpD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EACxE,aAAa,CAAC,KACb,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CA0B9B,CAAA"}
|