@kairos-es/codec 1.0.0-alpha.4 → 1.0.0-alpha.5
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 +2 -2
- package/dist/cjs/MetadataSource.js +1 -1
- package/dist/cjs/MetadataSource.js.map +1 -1
- package/dist/cjs/Serializer.js +3 -3
- package/dist/cjs/Serializer.js.map +1 -1
- package/dist/cjs/Uuid.js +6 -3
- package/dist/cjs/Uuid.js.map +1 -1
- package/dist/cjs/decider.js +1 -1
- package/dist/cjs/decisionModel.js +1 -1
- package/dist/cjs/decisionModel.js.map +1 -1
- package/dist/cjs/decode.js +5 -5
- package/dist/cjs/decode.js.map +1 -1
- package/dist/cjs/deserialiseThenDecode.js +1 -1
- package/dist/cjs/deserialiseThenDecode.js.map +1 -1
- package/dist/cjs/errors.js +6 -4
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/event.js.map +1 -1
- package/dist/cjs/precondition.js +2 -2
- package/dist/cjs/precondition.js.map +1 -1
- package/dist/cjs/projection.js +9 -10
- package/dist/cjs/projection.js.map +1 -1
- package/dist/cjs/readLedger.js +3 -3
- package/dist/cjs/readLedger.js.map +1 -1
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/toEvent.js +4 -4
- package/dist/cjs/toEvent.js.map +1 -1
- package/dist/dts-cjs/MetadataSource.d.ts +8 -0
- package/dist/{dts → dts-cjs}/MetadataSource.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/Serializer.d.ts +3 -3
- package/dist/{dts → dts-cjs}/Serializer.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/Uuid.d.ts +6 -3
- package/dist/{dts → dts-cjs}/Uuid.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/decider.d.ts +7 -7
- package/dist/{dts → dts-cjs}/decider.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/decision.d.ts +2 -2
- package/dist/{dts → dts-cjs}/decode.d.ts +3 -3
- package/dist/{dts → dts-cjs}/decode.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/deserialiseThenDecode.d.ts +3 -3
- package/dist/{dts → dts-cjs}/deserialiseThenDecode.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/errors.d.ts +4 -3
- package/dist/dts-cjs/errors.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/event.d.ts +18 -8
- package/dist/dts-cjs/event.d.ts.map +1 -0
- package/dist/dts-cjs/package.json +3 -0
- package/dist/{dts → dts-cjs}/precondition.d.ts +2 -2
- package/dist/{dts → dts-cjs}/projection.d.ts +2 -2
- package/dist/dts-cjs/projection.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/readLedger.d.ts +4 -4
- package/dist/{dts → dts-cjs}/readLedger.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/toEvent.d.ts +2 -2
- package/dist/dts-esm/MetadataSource.d.ts +8 -0
- package/dist/dts-esm/MetadataSource.d.ts.map +1 -0
- package/dist/dts-esm/Serializer.d.ts +27 -0
- package/dist/dts-esm/Serializer.d.ts.map +1 -0
- package/dist/dts-esm/Uuid.d.ts +22 -0
- package/dist/dts-esm/Uuid.d.ts.map +1 -0
- package/dist/dts-esm/decider.d.ts +206 -0
- package/dist/dts-esm/decider.d.ts.map +1 -0
- package/dist/dts-esm/decision.d.ts +161 -0
- package/dist/dts-esm/decision.d.ts.map +1 -0
- package/dist/dts-esm/decisionModel.d.ts +149 -0
- package/dist/dts-esm/decisionModel.d.ts.map +1 -0
- package/dist/dts-esm/decode.d.ts +92 -0
- package/dist/dts-esm/decode.d.ts.map +1 -0
- package/dist/dts-esm/deserialiseThenDecode.d.ts +62 -0
- package/dist/dts-esm/deserialiseThenDecode.d.ts.map +1 -0
- package/dist/dts-esm/errors.d.ts +80 -0
- package/dist/dts-esm/errors.d.ts.map +1 -0
- package/dist/dts-esm/event.d.ts +81 -0
- package/dist/dts-esm/event.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +134 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/precondition.d.ts +244 -0
- package/dist/dts-esm/precondition.d.ts.map +1 -0
- package/dist/dts-esm/projection.d.ts +56 -0
- package/dist/dts-esm/projection.d.ts.map +1 -0
- package/dist/dts-esm/readLedger.d.ts +263 -0
- package/dist/dts-esm/readLedger.d.ts.map +1 -0
- package/dist/dts-esm/registry.d.ts +46 -0
- package/dist/dts-esm/registry.d.ts.map +1 -0
- package/dist/dts-esm/toEvent.d.ts +45 -0
- package/dist/dts-esm/toEvent.d.ts.map +1 -0
- package/dist/esm/MetadataSource.js +1 -1
- package/dist/esm/MetadataSource.js.map +1 -1
- package/dist/esm/Serializer.js +3 -3
- package/dist/esm/Serializer.js.map +1 -1
- package/dist/esm/Uuid.js +6 -3
- package/dist/esm/Uuid.js.map +1 -1
- package/dist/esm/decider.js +1 -1
- package/dist/esm/decisionModel.js +1 -1
- package/dist/esm/decisionModel.js.map +1 -1
- package/dist/esm/decode.js +5 -5
- package/dist/esm/decode.js.map +1 -1
- package/dist/esm/deserialiseThenDecode.js +1 -1
- package/dist/esm/deserialiseThenDecode.js.map +1 -1
- package/dist/esm/errors.js +6 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/event.js.map +1 -1
- package/dist/esm/precondition.js +2 -2
- package/dist/esm/precondition.js.map +1 -1
- package/dist/esm/projection.js +9 -10
- package/dist/esm/projection.js.map +1 -1
- package/dist/esm/readLedger.js +3 -3
- package/dist/esm/readLedger.js.map +1 -1
- package/dist/esm/registry.js.map +1 -1
- package/dist/esm/toEvent.js +4 -4
- package/dist/esm/toEvent.js.map +1 -1
- package/package.json +12 -7
- package/src/MetadataSource.ts +2 -2
- package/src/Serializer.ts +4 -4
- package/src/Uuid.ts +7 -4
- package/src/decider.ts +9 -9
- package/src/decision.ts +1 -1
- package/src/decisionModel.ts +1 -1
- package/src/decode.ts +12 -18
- package/src/deserialiseThenDecode.ts +5 -5
- package/src/errors.ts +6 -4
- package/src/event.ts +19 -9
- package/src/precondition.ts +3 -3
- package/src/projection.ts +15 -20
- package/src/readLedger.ts +7 -7
- package/src/registry.ts +1 -1
- package/src/toEvent.ts +4 -4
- package/dist/dts/MetadataSource.d.ts +0 -8
- package/dist/dts/errors.d.ts.map +0 -1
- package/dist/dts/event.d.ts.map +0 -1
- package/dist/dts/projection.d.ts.map +0 -1
- /package/dist/{dts → dts-cjs}/decision.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/decisionModel.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/decisionModel.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/index.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/index.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/precondition.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/registry.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/registry.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/toEvent.d.ts.map +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event definitions: a tagged `Schema` built through `event(type, fields,
|
|
3
|
+
* tagsFor)`. Each def is a `Schema.Codec<Payload, Wire, R, R>` plus its branded versioned
|
|
4
|
+
* `type` (ADR-0005) captured *beside* the payload, and a required `tagsFor`
|
|
5
|
+
* mapper deriving the event's tags from its decoded payload (ADR-0006).
|
|
6
|
+
*
|
|
7
|
+
* The single `EventType` brand is applied once, here, so no consumer ever casts
|
|
8
|
+
* a type string: `def.type` is already assignable to core's `EventType`. The
|
|
9
|
+
* payload `A`, wire encoding `I`, and requirement `R` are inferred from the
|
|
10
|
+
* `Schema.Struct` of `fields` — including any requirement a field's `Schema`
|
|
11
|
+
* transform carries (e.g. an application `Encrypted(...)` field contributes its
|
|
12
|
+
* `CipherKey` to `R`).
|
|
13
|
+
*
|
|
14
|
+
* `tagsFor` is the only way to tag an event: `toEvent` derives the wire tags by
|
|
15
|
+
* calling it, and there is no `tags` option to override them. Its
|
|
16
|
+
* `NonEmptyReadonlyArray<Tag>` return makes "every event carries at least one
|
|
17
|
+
* tag" a compile-time guarantee (returning `[]` is a type error at the def
|
|
18
|
+
* site), and its input being only this event's payload guarantees `tag ⊆ event
|
|
19
|
+
* data` structurally. An event with no natural business identity returns an
|
|
20
|
+
* explicit app-defined `system:<name>` tag, never an empty array.
|
|
21
|
+
*
|
|
22
|
+
* A mapper must be a *pure* function of its payload and key off fields in their
|
|
23
|
+
* schema-canonical form (normalising in the mapper to match a field's schema),
|
|
24
|
+
* so the tag it derives is stable under the schema round-trip: a later retag
|
|
25
|
+
* re-derives tags from decoded history, and `tagsFor(decode(encode(p)))` must
|
|
26
|
+
* equal `tagsFor(p)` for that to be sound. Plain scalar fields satisfy this
|
|
27
|
+
* trivially; a field whose schema canonicalises on decode (trim, lowercase,
|
|
28
|
+
* normalise) needs the mapper to apply the same normalisation (ADR-0006).
|
|
29
|
+
*/
|
|
30
|
+
import type { EventType, Tag } from '@kairos-es/core';
|
|
31
|
+
import type { Array as Arr } from 'effect';
|
|
32
|
+
import { Schema } from 'effect';
|
|
33
|
+
/**
|
|
34
|
+
* A tag mapper: derives an event's tags from its decoded payload, returning at
|
|
35
|
+
* least one `Tag`. Named so a standalone mapper can be typed and shared (e.g.
|
|
36
|
+
* across two versioned defs of one logical event) and so the signature is
|
|
37
|
+
* written once, not per use site.
|
|
38
|
+
*/
|
|
39
|
+
export type TagsFor<A> = (payload: A) => Arr.NonEmptyReadonlyArray<Tag>;
|
|
40
|
+
/**
|
|
41
|
+
* An event definition: its branded `type`, the `Schema` mapping its typed
|
|
42
|
+
* payload `A` to its wire encoding `I` (requiring `R`), and the `tagsFor` mapper
|
|
43
|
+
* that derives the event's tags from a decoded payload.
|
|
44
|
+
*
|
|
45
|
+
* `R` is ONE requirement for both directions, which is why the schema is a
|
|
46
|
+
* `Schema.Codec<A, I, R, R>` rather than leaving decoding and encoding services
|
|
47
|
+
* apart. A def is decoded on the read path and encoded by `toEvent`, so a
|
|
48
|
+
* consumer providing a def's services provides them for both; an application
|
|
49
|
+
* encryption transform needs its key either way. A struct whose two directions
|
|
50
|
+
* differ still fits, the codec being covariant in both, and `R` is then their
|
|
51
|
+
* union.
|
|
52
|
+
*
|
|
53
|
+
* `tagsFor` CONSUMES an `A`, so `EventDef` is invariant in `A` whatever the
|
|
54
|
+
* schema's own variance, and is therefore not declared `out A`.
|
|
55
|
+
*/
|
|
56
|
+
export interface EventDef<A = unknown, I = unknown, R = never> {
|
|
57
|
+
readonly type: EventType;
|
|
58
|
+
readonly schema: Schema.Codec<A, I, R, R>;
|
|
59
|
+
readonly tagsFor: TagsFor<A>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The heterogeneous bound for arrays/records of mixed defs, where the precise
|
|
63
|
+
* `A`/`I` differ per element. `tagsFor` erases to `(a: any) => …`, which is
|
|
64
|
+
* harmless: only the write path (`toEvent`) calls it, and the registry/decode
|
|
65
|
+
* path never does (stored events already carry tags).
|
|
66
|
+
*/
|
|
67
|
+
export type EventDefAny = EventDef<any, any, unknown>;
|
|
68
|
+
/**
|
|
69
|
+
* Define an event from a versioned `type` literal, its payload `fields`, and its
|
|
70
|
+
* required `tagsFor` mapper. Builds `Schema.Struct(fields)` and brands the type
|
|
71
|
+
* literal *once* (the single sanctioned cast). The returned def is typed so
|
|
72
|
+
* `def.type` is a branded literal assignable to core's `EventType` with no
|
|
73
|
+
* consumer cast, `A`/`I`/`R` are inferred from the struct, and `tagsFor`
|
|
74
|
+
* receives the def's decoded payload type.
|
|
75
|
+
*/
|
|
76
|
+
export declare const event: <const Type extends string, Fields extends Schema.Struct.Fields>(type: Type, fields: Fields, tagsFor: TagsFor<Schema.Struct.Type<Fields>>) => {
|
|
77
|
+
readonly type: Type & EventType;
|
|
78
|
+
readonly schema: Schema.Struct<Fields>;
|
|
79
|
+
readonly tagsFor: TagsFor<Schema.Struct.Type<Fields>>;
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/event.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;;GAKG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,GAAG,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAA;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,KAAK;IAC3D,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IACzC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAC7B;AAED;;;;;GAKG;AAEH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,GAChB,KAAK,CAAC,IAAI,SAAS,MAAM,EACzB,MAAM,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,EAEnC,MAAM,IAAI,EACV,QAAQ,MAAM,EACd,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAC3C;IACD,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;CAStD,CAAA"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kairos-es/codec — the schema-first codec seam, mapping typed domain events
|
|
3
|
+
* to and from the store's opaque wire bytes, with an application-owned
|
|
4
|
+
* encryption seam (ADR-0004).
|
|
5
|
+
*
|
|
6
|
+
* Two entry points:
|
|
7
|
+
* - The *typed slice path* (primary): `event(type, fields, tagsFor)` defines a
|
|
8
|
+
* tagged `Schema` plus a required mapper deriving the event's tags from its
|
|
9
|
+
* payload (ADR-0006); `projection(init).on(def, fold)...build()` composes the
|
|
10
|
+
* specific defs a decision model consumes, inferring its state and requirement
|
|
11
|
+
* `R`; `modelAtHead(slices)` reads, decodes through each def's `Schema`, and folds
|
|
12
|
+
* purely, and `buildDecisionModel(slices)` adds the `AppendCondition` to it. A handler for an un-imported event is a compile error, and
|
|
13
|
+
* a mistyped type cannot silently match nothing.
|
|
14
|
+
* - The *general path* (minimal): `makeRegistry(defs)` decodes an arbitrary wire
|
|
15
|
+
* event by its runtime type string; an unknown type is a typed
|
|
16
|
+
* `UnknownEventType` (ADR-0005 decode-totality). The payload stays `unknown`
|
|
17
|
+
* on this path: the type is discovered at runtime, so no def was named to
|
|
18
|
+
* narrow it (see `registry.ts`).
|
|
19
|
+
*
|
|
20
|
+
* `toEvent(def, payload, { uuid?, occurredAt?, metadata? })` produces a wire
|
|
21
|
+
* event (encode -> serialise, tags derived from `def.tagsFor`, clock + uuid from
|
|
22
|
+
* services, metadata from the call or the optional ambient `MetadataSource`).
|
|
23
|
+
* Serialisation is a pluggable `Serializer` (JSON
|
|
24
|
+
* default). Encryption, when needed, is an *application*-authored `Schema`
|
|
25
|
+
* transform on a payload field; the codec only propagates its requirement and
|
|
26
|
+
* error channels — it owns no cipher and imports no `node:crypto`.
|
|
27
|
+
*
|
|
28
|
+
* `Decider` + `runDecider(decider, command)` close that path into the write
|
|
29
|
+
* side's whole command cycle: build the decision model, decide, append under the
|
|
30
|
+
* read's own condition. They live HERE, beside the `buildDecisionModel` they
|
|
31
|
+
* drive, rather than in `@kairos-es/testing` where they began — that package
|
|
32
|
+
* peers `@effect/vitest`, and a production command handler must not name it (see
|
|
33
|
+
* `decider.ts` for the full argument). `decider(projections, decide)` builds one
|
|
34
|
+
* with all four type arguments inferred, so a command's slice record and its
|
|
35
|
+
* error union are written once, in the two function bodies, rather than restated
|
|
36
|
+
* as an annotation above them.
|
|
37
|
+
*
|
|
38
|
+
* A decision may also answer `Unchanged` — "already done, nothing to append" —
|
|
39
|
+
* built with `Decision.unchanged(reason?)` and recognised ONLY by
|
|
40
|
+
* `isUnchanged`, by its `UnchangedTypeId`. It is a success, not an error and not
|
|
41
|
+
* an empty batch: every runner returns it in place of a `Position` and appends
|
|
42
|
+
* nothing. `Decider`'s fifth type parameter carries the widening and defaults to
|
|
43
|
+
* the non-empty batch, and each runner's result is `DecisionResult<O>`, so a
|
|
44
|
+
* decider or body that cannot answer `Unchanged` keeps a plain `Position` — an
|
|
45
|
+
* untyped one and one generic in its success type included — and its caller is
|
|
46
|
+
* never asked to handle it. `decision.ts` owns the argument, and the one type
|
|
47
|
+
* query that does read differently.
|
|
48
|
+
*
|
|
49
|
+
* `transact(precondition)(body)` is the layered write path, for a decision whose
|
|
50
|
+
* second query cannot be written until an earlier read has been folded. It opens
|
|
51
|
+
* an ambient read ledger, evaluates the precondition inside it, hands that
|
|
52
|
+
* precondition's output to the body as a service, and appends what the body
|
|
53
|
+
* returned under the union of every query either of them read, at the earliest
|
|
54
|
+
* head observed. No condition reaches the caller. `unsafeAppend(events, reason)`
|
|
55
|
+
* is the one sanctioned way out. `precondition.ts` owns the argument for the
|
|
56
|
+
* bracket and everything on it, and `readLedger.ts` for the ledger the seam rests
|
|
57
|
+
* on — including why the derivation itself is deliberately not on this surface.
|
|
58
|
+
*
|
|
59
|
+
* ## ⚠ `runDecider` and `decideOnly` are TWO DISPOSITIONS OF ONE CYCLE
|
|
60
|
+
*
|
|
61
|
+
* They are read as a pair or not at all, because each refuses exactly where the
|
|
62
|
+
* other is the answer:
|
|
63
|
+
*
|
|
64
|
+
* | | outside a bracket | inside a bracket |
|
|
65
|
+
* | ------------ | ---------------------------------------- | ----------------------------------- |
|
|
66
|
+
* | `runDecider` | ✅ the ordinary single-hop case | ⛔ a DEFECT, refused at the store |
|
|
67
|
+
* | `decideOnly` | ⛔ unrunnable, and `tsc` says so | ✅ the layered case |
|
|
68
|
+
*
|
|
69
|
+
* Both carry `Unchanged` the same way, which is what keeps them one pair:
|
|
70
|
+
* `runDecider` returns it having appended nothing, and `decideOnly` passes it to
|
|
71
|
+
* the body unaltered, where `transact` returns it having appended nothing —
|
|
72
|
+
* and, having no write to guard, without refusing an empty bracket as
|
|
73
|
+
* `NoReadsRecorded`.
|
|
74
|
+
*
|
|
75
|
+
* ⚠ *Unrunnable* rather than *unwritable*, and the difference is measured. A bare
|
|
76
|
+
* `decideOnly(d, cmd)` outside a bracket COMPILES, and so does an exported helper
|
|
77
|
+
* over it: its `ReadLedger` requirement simply propagates, as any unmet requirement
|
|
78
|
+
* does. The refusal lands where the context is DISCHARGED — an `Effect.provide`, a
|
|
79
|
+
* `runPromise`, an annotation asking for `R = never` — which may be another file.
|
|
80
|
+
* So `pnpm check` catches it without anything running, which is the whole gain over
|
|
81
|
+
* the runtime defect this replaced; what it does not do is underline the call.
|
|
82
|
+
*
|
|
83
|
+
* Neither is expressed over the other, and that is forced rather than chosen: a
|
|
84
|
+
* definition of `runDecider` as *"`decideOnly`, then append"* would have to call
|
|
85
|
+
* `decideOnly` outside a bracket, which does not typecheck, and the append it then
|
|
86
|
+
* made inside one would be refused. The two refusals are the same fact seen from
|
|
87
|
+
* either side — an append inside a bracket belongs to the bracket — so any
|
|
88
|
+
* expression of one over the other has to defeat one of them.
|
|
89
|
+
*
|
|
90
|
+
* ⚠ The two refusals arrive at DIFFERENT MOMENTS, which is the part a caller acts
|
|
91
|
+
* on. `runDecider` inside a bracket is refused when it RUNS, at core's shared append
|
|
92
|
+
* decorator — so the same refusal also meets a raw `store.append` and a runner one
|
|
93
|
+
* call away in a helper. `decideOnly` outside a bracket is refused by `tsc`, at the
|
|
94
|
+
* discharge point above, by naming `ReadLedger` in its requirement channel. Why each
|
|
95
|
+
* was given the instrument it was is a DECISION and belongs to the record that took
|
|
96
|
+
* it: ADR-0014's `Status`. A lint rule reaches the first case earlier still, while it
|
|
97
|
+
* is being written; `biome-plugins/README.md` in the source repository owns what a
|
|
98
|
+
* rule there can and cannot see.
|
|
99
|
+
*
|
|
100
|
+
* The remedy is one identifier away in each direction, which is the whole reason a
|
|
101
|
+
* refusal is affordable at all. `decider.ts` and `precondition.ts` carry the two
|
|
102
|
+
* halves.
|
|
103
|
+
*
|
|
104
|
+
* `modelAtHead(slices)` is that same read-decode-fold WITHOUT the
|
|
105
|
+
* `AppendCondition`: `{ state, head }`, one slice record materialised at read
|
|
106
|
+
* time with no table, no checkpoint and no runner, current as of the `head` its
|
|
107
|
+
* read returned — which is a stronger currency claim than a maintained view's
|
|
108
|
+
* checkpoint can make. `buildDecisionModel` is expressed over it, so the write
|
|
109
|
+
* path's decision model and a read-time materialisation share one body rather
|
|
110
|
+
* than two that could drift (see `decisionModel.ts` for both arguments).
|
|
111
|
+
*
|
|
112
|
+
* `decodeSlices(slices)` is the decode stage `modelAtHead` folds through,
|
|
113
|
+
* exported because the read side needs the *same* decode over a live
|
|
114
|
+
* `subscribe` stream. Exposing it keeps that single implementation shared
|
|
115
|
+
* rather than reimplemented downstream. It is deliberately ONE symbol taking
|
|
116
|
+
* the slice record, not a merge-then-decode pair: the pair had exactly one
|
|
117
|
+
* legal composition, and its only degree of freedom was to decode against a
|
|
118
|
+
* record other than the one the query came from (see `decode.ts`).
|
|
119
|
+
*/
|
|
120
|
+
export { type Decider, decider, runDecider } from './decider.js';
|
|
121
|
+
export { Decision, type DecisionOutput, type DecisionResult, isUnchanged, Unchanged, UnchangedTypeId, } from './decision.js';
|
|
122
|
+
export { buildDecisionModel, modelAtHead, type RequirementOf, } from './decisionModel.js';
|
|
123
|
+
export { decodeSlices } from './decode.js';
|
|
124
|
+
export { CodecError, UnknownEventType } from './errors.js';
|
|
125
|
+
export { type EventDef, type EventDefAny, event, type TagsFor } from './event.js';
|
|
126
|
+
export { MetadataSource } from './MetadataSource.js';
|
|
127
|
+
export { decideOnly, type Precondition, transact, unsafeAppend, } from './precondition.js';
|
|
128
|
+
export { type ProjectionBuilder, projection, type SliceProjection, } from './projection.js';
|
|
129
|
+
export { type CloseHop, type Hop, NoReadsRecorded, ReadLedger, TaglessSliceInLedger, } from './readLedger.js';
|
|
130
|
+
export { makeRegistry, type Registry } from './registry.js';
|
|
131
|
+
export { Serializer, SerializerDefault } from './Serializer.js';
|
|
132
|
+
export { toEvent } from './toEvent.js';
|
|
133
|
+
export { Uuid, UuidLive, UuidTest } from './Uuid.js';
|
|
134
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EACL,QAAQ,EACR,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,WAAW,EACX,SAAS,EACT,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EACL,UAAU,EACV,KAAK,YAAY,EACjB,QAAQ,EACR,YAAY,GACb,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,iBAAiB,EACtB,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,GAAG,EACR,eAAe,EACf,UAAU,EACV,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,YAAY,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bracket that owns the append, the precondition value it takes, and the two
|
|
3
|
+
* functions that make a decision inside it writable.
|
|
4
|
+
*
|
|
5
|
+
* ## What the bracket is for, and why no condition reaches the caller
|
|
6
|
+
*
|
|
7
|
+
* `transact(precondition)(body)` evaluates a guard, hands its output to the body
|
|
8
|
+
* as a service, and appends whatever the body produced under the union of every
|
|
9
|
+
* read EITHER of them performed. A caller writes no condition, no union, no floor
|
|
10
|
+
* and no `min`.
|
|
11
|
+
*
|
|
12
|
+
* That is not an ergonomic preference. Owning the append does not make a wrong
|
|
13
|
+
* condition a type error — it makes one UNWRITABLE, which is the stronger
|
|
14
|
+
* guarantee: a returned condition is a condition a caller can decline to use, and
|
|
15
|
+
* a decision that looks guarded and is not is the exact failure this seam exists
|
|
16
|
+
* to close. It is what Axon Framework 5 does, where the condition is married to
|
|
17
|
+
* the events at prepare-commit and the caller never holds one.
|
|
18
|
+
*
|
|
19
|
+
* One thing is deliberately NOT borrowed from that prior art. There, an append
|
|
20
|
+
* with no prior read leaves the condition unset and the append unguarded, so the
|
|
21
|
+
* method name reads as unconditional while the behaviour is not. `NoReadsRecorded`
|
|
22
|
+
* closes that door here and must not be lost.
|
|
23
|
+
*
|
|
24
|
+
* ⚠ The name says *declare, then do*. It is NOT a database transaction: an append
|
|
25
|
+
* under a condition is not two-phase, and nothing rolls back.
|
|
26
|
+
*
|
|
27
|
+
* ## A body may answer `Unchanged`, and then there is nothing to guard
|
|
28
|
+
*
|
|
29
|
+
* The body's success is a `DecisionOutput`: a non-empty batch, or `Unchanged` —
|
|
30
|
+
* "already done". On `Unchanged` the bracket appends nothing, derives no
|
|
31
|
+
* condition and returns the `Unchanged`, and the result type is
|
|
32
|
+
* `DecisionResult<O>`, so a body that cannot answer it keeps a plain `Position`.
|
|
33
|
+
* `decideOnly` passes a decider's `Unchanged` through to the body unaltered.
|
|
34
|
+
*
|
|
35
|
+
* It is NOT refused as `NoReadsRecorded`, even from a bracket that read nothing,
|
|
36
|
+
* and that is not a hole in the refusal. `NoReadsRecorded` exists because an
|
|
37
|
+
* append with no prior read would go out UNGUARDED while looking guarded. An
|
|
38
|
+
* `Unchanged` appends nothing, so there is no write for a condition to guard and
|
|
39
|
+
* nothing for the refusal to protect. The same empty bracket returning EVENTS is
|
|
40
|
+
* still refused, exactly as before. `decision.ts` owns what `Unchanged` is.
|
|
41
|
+
*
|
|
42
|
+
* ## Order is the design, and it is the one line here that must not move
|
|
43
|
+
*
|
|
44
|
+
* The precondition is evaluated INSIDE the bracket and never before it. Evaluated
|
|
45
|
+
* before, its reads are not part of what it protects — the ledger does not exist
|
|
46
|
+
* yet, so its hops are lost and the write is guarded only by the business read.
|
|
47
|
+
* The check would look exactly as it does now and would guard nothing.
|
|
48
|
+
*
|
|
49
|
+
* ## Why the precondition's output crosses as a SERVICE
|
|
50
|
+
*
|
|
51
|
+
* A body that needs authorisation names the tag in its requirement channel, so
|
|
52
|
+
* `transact` must provide it, so the body CANNOT BE RUN without a precondition.
|
|
53
|
+
* That is a PRESENCE check, and presence is the one direction the type system can
|
|
54
|
+
* decide here — `Exclude<R, Id>` in the return type is where it is spent. The
|
|
55
|
+
* alternative, a curried parameter, buys nothing equivalent: a body is free to
|
|
56
|
+
* ignore a parameter.
|
|
57
|
+
*
|
|
58
|
+
* ## Composition is a combinator, not a list
|
|
59
|
+
*
|
|
60
|
+
* `transact` takes exactly ONE precondition. A variadic argument asserts
|
|
61
|
+
* INDEPENDENCE, and the interesting case is DEPENDENT — *may act for this
|
|
62
|
+
* organisation*, then *may sign in that organisation* — which a flat list cannot
|
|
63
|
+
* express and which is the very shape this change exists for. A dependent pair
|
|
64
|
+
* needs nothing anyway: a precondition is an ordinary `Effect` evaluated inside
|
|
65
|
+
* the open ledger, so two compose with `Effect.gen` and both sets of reads land in
|
|
66
|
+
* the same condition. An INDEPENDENT set wants a combinator over a RECORD,
|
|
67
|
+
* evaluated SEQUENTIALLY — fail-fast is the point, and concurrent reads are
|
|
68
|
+
* precisely the case where the earliest-head reduction stops being redundant. That
|
|
69
|
+
* combinator is purely additive and is not shipped here.
|
|
70
|
+
*
|
|
71
|
+
* ## Conflict retry goes OUTSIDE the bracket, never inside
|
|
72
|
+
*
|
|
73
|
+
* `retryOnConflict(transact(p)(body))`, never a retry within the body. The bracket
|
|
74
|
+
* takes an opaque `Effect`, so no type can express that rule and the lint gate is
|
|
75
|
+
* what enforces it: in the source repository, `biome-plugins/noRetryInBracket.grit`
|
|
76
|
+
* owns why and `test/retryInBracket.test.ts` executes it.
|
|
77
|
+
*
|
|
78
|
+
* ## ⚠⚠ THE ONE MISTAKE NOTHING CATCHES: a view read inside a bracket
|
|
79
|
+
*
|
|
80
|
+
* **A projection read, a raw SQL read, or any read that does not fold through
|
|
81
|
+
* `modelAtHead`, performed inside a bracket, contributes NO HOP.** Its query is
|
|
82
|
+
* absent from the derived union and its head is absent from the floor, so the
|
|
83
|
+
* decision rests on data the condition does not guard — and the bracket looks
|
|
84
|
+
* guarded, which is the exact failure this seam exists to close, wearing the fix's
|
|
85
|
+
* own name.
|
|
86
|
+
*
|
|
87
|
+
* **Nothing will tell you — not here and not in your own tree.** Not the type
|
|
88
|
+
* system: a requirement channel is ADDITIVE, so it can demand a capability's
|
|
89
|
+
* presence and never its absence. Not `NoReadsRecorded`: that fires on an EMPTY
|
|
90
|
+
* bracket and never on an under-populated one, so a bracket holding one genuine fold
|
|
91
|
+
* beside a view read passes it. And not a lint rule — one was written for exactly
|
|
92
|
+
* this, kept for a while and deleted, because a syntactic gate has no call graph and
|
|
93
|
+
* no types, so it could only name a receiver by convention, and because a lint
|
|
94
|
+
* plugin runs only where its own config is and so never travels with the library.
|
|
95
|
+
* `biome-plugins/README.md` in the source repository records the test that rule
|
|
96
|
+
* failed.
|
|
97
|
+
*
|
|
98
|
+
* So this is a rule a reader keeps, and knowing it is the guarantee: **inside a
|
|
99
|
+
* bracket, read through `modelAtHead`. If a read must
|
|
100
|
+
* consult a materialised view, do it OUTSIDE the bracket** — where it is ordinary
|
|
101
|
+
* code, because its answer is then not part of what the append is guarded on.
|
|
102
|
+
* ADR-0014's *decision read* rule is the test for which one a given read is.
|
|
103
|
+
*/
|
|
104
|
+
import type { AppendConditionFailed, DcbEvent, Position } from '@kairos-es/core';
|
|
105
|
+
import { AppendIsOwned, DcbEventStore } from '@kairos-es/core';
|
|
106
|
+
import type { Array as Arr } from 'effect';
|
|
107
|
+
import { Context, Effect } from 'effect';
|
|
108
|
+
import type { Decider } from './decider.js';
|
|
109
|
+
import { type DecisionOutput, type DecisionResult } from './decision.js';
|
|
110
|
+
import { type RequirementOf } from './decisionModel.js';
|
|
111
|
+
import type { CodecError } from './errors.js';
|
|
112
|
+
import type { SliceProjection } from './projection.js';
|
|
113
|
+
import { NoReadsRecorded, ReadLedger, type TaglessSliceInLedger } from './readLedger.js';
|
|
114
|
+
import type { Serializer } from './Serializer.js';
|
|
115
|
+
import type { Uuid } from './Uuid.js';
|
|
116
|
+
/**
|
|
117
|
+
* A named, reusable guard plus the tag its output is handed across.
|
|
118
|
+
*
|
|
119
|
+
* A VALUE and not a function of the command, deliberately: the point of the seam
|
|
120
|
+
* is that a guard is written once, centrally, and referenced by however many
|
|
121
|
+
* commands need it. A command supplies its own arguments when it BUILDS the value
|
|
122
|
+
* (`mayActFor(userId, orgId, verb)`), and what `transact` receives is the built
|
|
123
|
+
* thing.
|
|
124
|
+
*
|
|
125
|
+
* `Service` is fully generic and the library never reads it. Every consumer-side
|
|
126
|
+
* authorisation shape — a plain resolved identifier, a branded proof, a gate with
|
|
127
|
+
* a private minting closure — is application code this genericity accommodates
|
|
128
|
+
* without the library knowing anything about it. That is a requirement rather than
|
|
129
|
+
* an incidental property: hard-coding it would foreclose a consumer's later
|
|
130
|
+
* choice, and it costs nothing to keep.
|
|
131
|
+
*
|
|
132
|
+
* ⚠ `evaluate` is an `Effect`, so it is a FIELD OF AN OBJECT LITERAL and its
|
|
133
|
+
* expression is evaluated when the `Precondition` VALUE is built — before the
|
|
134
|
+
* bracket opens and before any cheap check inside it runs. Anything a builder does
|
|
135
|
+
* eagerly there (parsing an argument, resolving an identifier) happens outside the
|
|
136
|
+
* ordering guarantee above, and a throw there is an uncatchable defect on the very
|
|
137
|
+
* path meant to deny the input cheaply. Wrap such work in `Effect.suspend`.
|
|
138
|
+
*/
|
|
139
|
+
export interface Precondition<Id, Service, E, R> {
|
|
140
|
+
readonly tag: Context.Key<Id, Service>;
|
|
141
|
+
readonly evaluate: Effect.Effect<Service, E, R>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The DCB command cycle MINUS the append: read the decision model, decode, fold,
|
|
145
|
+
* decide, and RETURN the events for the bracket to append — or the decider's
|
|
146
|
+
* `Unchanged`, passed through as it came, for the bracket to append nothing.
|
|
147
|
+
*
|
|
148
|
+
* Bracket-only, and the refusal is STATIC — see the section below for the mechanism
|
|
149
|
+
* and, more importantly, for where the error actually lands, which is not where a
|
|
150
|
+
* reader first expects. Forced through anyway, being outside a bracket is a DEFECT
|
|
151
|
+
* rather than an error-channel value: it is not a data-dependent outcome a caller
|
|
152
|
+
* could handle, and the events produced would be dropped with nothing to append them.
|
|
153
|
+
*
|
|
154
|
+
* It is also what makes the nesting refusal liveable. A command runner inside a
|
|
155
|
+
* bracket is refused at the store, and this is the function a builder reaches for
|
|
156
|
+
* instead — one identifier away. A refusal is only affordable when the fix is that
|
|
157
|
+
* close.
|
|
158
|
+
*
|
|
159
|
+
* `modelAtHead` and NOT `buildDecisionModel`: this function must not derive a
|
|
160
|
+
* condition at all. The bracket owns that, and owning it in ONE place is what
|
|
161
|
+
* stops a caller appending under the last hop's condition alone.
|
|
162
|
+
*
|
|
163
|
+
* ## Bracket-only is a REQUIREMENT, so `tsc` refuses it rather than a run
|
|
164
|
+
*
|
|
165
|
+
* `ReadLedger` is named in the requirement channel and `transact` is the only thing
|
|
166
|
+
* that provides it, so a program that calls this outside a bracket cannot be RUN,
|
|
167
|
+
* and `tsc` is what says so — nothing has to execute for a caller to be told.
|
|
168
|
+
*
|
|
169
|
+
* ⚠ Measured, because the obvious stronger reading is wrong: the call site itself
|
|
170
|
+
* COMPILES. A bare `decideOnly(d, cmd)` outside a bracket is fine, and so is an
|
|
171
|
+
* exported helper over it; the requirement just propagates, as any unmet requirement
|
|
172
|
+
* does. What fails is the DISCHARGE — `Effect.provide`, `runPromise`, an annotation
|
|
173
|
+
* asking for `R = never` — possibly in another file. That is still a static refusal
|
|
174
|
+
* and it is the whole gain over the runtime defect this replaced. It is not a
|
|
175
|
+
* squiggle under the mistake.
|
|
176
|
+
*
|
|
177
|
+
* That is a PRESENCE check, and presence is the direction the type system can
|
|
178
|
+
* decide here — the same direction `transact`'s `Exclude<R, Id>` already spends on
|
|
179
|
+
* the precondition's output. ADR-0014's 2026-08-29 amendment carries the rule that
|
|
180
|
+
* follows: look for a presence check before reaching for a defect. This function
|
|
181
|
+
* shipped a defect first, and the check was available all along.
|
|
182
|
+
*
|
|
183
|
+
* ⚠ Two lines carry it and their JOBS differ. The RETURN ANNOTATION is the surface a
|
|
184
|
+
* consumer is judged against, and what the discharge above meets. The
|
|
185
|
+
* `yield* ReadLedger` keeps the requirement in the INFERRED type, so it survives the
|
|
186
|
+
* annotation being edited, and it dies at run time for the audience no type reaches
|
|
187
|
+
* — a JavaScript consumer runs no compile step — rather than handing back events
|
|
188
|
+
* with no bracket to append them. Nothing here READS the ledger; `modelAtHead` does
|
|
189
|
+
* the recording, ambiently, through `Effect.serviceOption`.
|
|
190
|
+
*
|
|
191
|
+
* ⚠ Do not try to write down which check catches which deletion. It was tried twice
|
|
192
|
+
* and was wrong twice, and `test/appendOwnership.test.ts` in the source repository
|
|
193
|
+
* says why the second failure means there should be no third attempt. Plant the
|
|
194
|
+
* mutation instead.
|
|
195
|
+
*/
|
|
196
|
+
export declare const decideOnly: <P extends Record<string, SliceProjection>, Command, E, R, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>>(decider: Decider<P, Command, E, R, O>, command: Command) => Effect.Effect<O, E | CodecError | TaglessSliceInLedger, ReadLedger | DcbEventStore | Serializer | Uuid | R | RequirementOf<P>>;
|
|
197
|
+
/**
|
|
198
|
+
* The sanctioned exception to the nesting refusal: append these events under NO
|
|
199
|
+
* condition, and say in the identifier and in a required `reason` why that is
|
|
200
|
+
* deliberate.
|
|
201
|
+
*
|
|
202
|
+
* It covers three cases and no others: deliberate narrowing, a genuinely
|
|
203
|
+
* unconditional creation, and a second append inside one bracket. The `reason` is
|
|
204
|
+
* ceremony on purpose, and ADR-0014 decision 6 owns why a hatch has to cost more
|
|
205
|
+
* than the safe path — that argument is a DECISION and belongs to the record that
|
|
206
|
+
* took it, so it is not restated here. How biome turns a required argument into a
|
|
207
|
+
* review signal is the source repository's `biome-plugins/unsafeAppendReported.grit`.
|
|
208
|
+
*
|
|
209
|
+
* ⚠ It BYPASSES the derived condition rather than composing with it. A caller who
|
|
210
|
+
* genuinely needs *the union, minus one event type* must restate the whole
|
|
211
|
+
* condition here. If that turns up in practice, a composing override is the
|
|
212
|
+
* additive answer and this hatch is not it.
|
|
213
|
+
*
|
|
214
|
+
* `Effect.updateContext` over
|
|
215
|
+
* `Context.omit(AppendIsOwned)` is what lets it through the store's ownership
|
|
216
|
+
* refusal — the one route, and `AppendIsOwned.ts` owns why removing the marker is
|
|
217
|
+
* the shape rather than a second tag or a sanctioning flag.
|
|
218
|
+
*/
|
|
219
|
+
export declare const unsafeAppend: (events: Arr.NonEmptyReadonlyArray<DcbEvent>, reason: string) => Effect.Effect<Position, AppendConditionFailed, DcbEventStore>;
|
|
220
|
+
/**
|
|
221
|
+
* Open a bracket over ONE decision: evaluate the precondition inside it, hand its
|
|
222
|
+
* output to the body under the precondition's tag, and append what the body
|
|
223
|
+
* produced under the union of every read either of them performed.
|
|
224
|
+
*
|
|
225
|
+
* Curried so the precondition reads as the thing the command DECLARES and the body
|
|
226
|
+
* reads as the thing it DOES.
|
|
227
|
+
*
|
|
228
|
+
* The ownership marker is provided ALONGSIDE the ledger and to the body only, so
|
|
229
|
+
* any append inside the body — a nested bracket, a command runner, a raw
|
|
230
|
+
* `store.append` — is refused at the store as a defect, while the bracket's own
|
|
231
|
+
* closing append below runs outside that scope and is unaffected. That is why the
|
|
232
|
+
* refusal is on APPENDING rather than on opening a bracket: a command runner
|
|
233
|
+
* appends without opening one, and its events usually match the union the bracket
|
|
234
|
+
* is about to guard on, so the bracket's own close would be refused by a condition
|
|
235
|
+
* its own body invalidated.
|
|
236
|
+
*
|
|
237
|
+
* The body may succeed with `Unchanged` (see the module documentation), and the
|
|
238
|
+
* result is then `DecisionResult<O>`: `Position` for a body that cannot answer
|
|
239
|
+
* it, `Position | Unchanged` for one that can. `O` is last and defaulted, so an
|
|
240
|
+
* explicit `<E, R>` written against the narrower signature still means what it
|
|
241
|
+
* did.
|
|
242
|
+
*/
|
|
243
|
+
export declare const transact: <Id, Service, PE, PR>(precondition: Precondition<Id, Service, PE, PR>) => <E, R, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>>(body: Effect.Effect<O, E, R>) => Effect.Effect<DecisionResult<O>, E | PE | NoReadsRecorded | AppendConditionFailed, Exclude<Exclude<R, Id>, ReadLedger | AppendIsOwned> | Exclude<PR, ReadLedger | AppendIsOwned> | DcbEventStore>;
|
|
244
|
+
//# sourceMappingURL=precondition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"precondition.d.ts","sourceRoot":"","sources":["../../src/precondition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAS,MAAM,QAAQ,CAAA;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,cAAc,EAEpB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAGL,eAAe,EACf,UAAU,EACV,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAElC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,eAAO,MAAM,UAAU,GACrB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,CAAC,EACD,CAAC,EACD,CAAC,SAAS,cAAc,GAAG,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAE9D,SAAS,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACrC,SAAS,OAAO,KACf,MAAM,CAAC,MAAM,CACd,CAAC,EACD,CAAC,GAAG,UAAU,GAAG,oBAAoB,EACrC,UAAU,GAAG,aAAa,GAAG,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAOnE,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,YAAY,GACvB,QAAQ,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAc3C,QAAQ,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,qBAAqB,EAAE,aAAa,CAe5D,CAAA;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,QAAQ,GAClB,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,cAAc,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,MACpE,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,cAAc,GAAG,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EACnE,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC3B,MAAM,CAAC,MAAM,CACd,cAAc,CAAC,CAAC,CAAC,EACjB,CAAC,GAAG,EAAE,GAAG,eAAe,GAAG,qBAAqB,EAC9C,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,aAAa,CAAC,GACnD,OAAO,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,CAAC,GACvC,aAAa,CAuDb,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed slice path (primary): a schema-backed projection and the fluent
|
|
3
|
+
* builder that produces it.
|
|
4
|
+
*
|
|
5
|
+
* A `SliceProjection<S, R>` is a core `AnyProjection<S>` (directly composable —
|
|
6
|
+
* its `evolve` folds a `DecodedEvent`) enriched with the `type -> Schema` map
|
|
7
|
+
* `decodeSlices` merges and decodes through. That map's value type carries `R`
|
|
8
|
+
* covariantly (`Schema.Decoder<unknown, R>`), so the union of the defs'
|
|
9
|
+
* requirements flows to the decision model with no cast (and no phantom marker).
|
|
10
|
+
* Because `evolve` is already erased, a built slice drops straight into
|
|
11
|
+
* `composeProjections` — no `AnyProjection.of` ceremony.
|
|
12
|
+
*
|
|
13
|
+
* `projection(initialState).on(def, fold)...build()` infers the state `S` and
|
|
14
|
+
* the union `R` of the `.on`-ed defs' requirements, derives the read `Query`'s
|
|
15
|
+
* event-types from the defs' branded type literals (no `as EventType`), and
|
|
16
|
+
* hands each `fold` its decoded, typed payload. A handler for an un-imported
|
|
17
|
+
* event is therefore a compile error, and a mistyped type cannot silently match
|
|
18
|
+
* nothing.
|
|
19
|
+
*/
|
|
20
|
+
import { type AnyProjection, type Tag } from '@kairos-es/core';
|
|
21
|
+
import type { Schema } from 'effect';
|
|
22
|
+
import type { EventDef } from './event.js';
|
|
23
|
+
/**
|
|
24
|
+
* A schema-backed, directly-composable projection: an `AnyProjection<S>` plus
|
|
25
|
+
* the `type -> Schema` map for decode. The map's value type carries the union
|
|
26
|
+
* `R` of its defs' requirements covariantly, so `RequirementOf` recovers `R`
|
|
27
|
+
* (and the `out R` annotation keeps union-widening sound) with no phantom marker.
|
|
28
|
+
*/
|
|
29
|
+
export interface SliceProjection<out S = unknown, out R = unknown> extends AnyProjection<S> {
|
|
30
|
+
readonly schemas: ReadonlyMap<string, Schema.Decoder<unknown, R>>;
|
|
31
|
+
}
|
|
32
|
+
/** The fluent builder: accumulate `.on(def, fold)` steps, then `.build()`. */
|
|
33
|
+
export interface ProjectionBuilder<S, R = never> {
|
|
34
|
+
/**
|
|
35
|
+
* Fold events of `def`'s type: `fold` receives the decoded, typed `payload`.
|
|
36
|
+
* Adds `def`'s requirement `R2` to the accumulated `R`.
|
|
37
|
+
*
|
|
38
|
+
* Throws if this chain has already `.on`-ed that event type — see the guard in
|
|
39
|
+
* `projection` for why a repeat is always a mistake here, where across two
|
|
40
|
+
* slices it need not be.
|
|
41
|
+
*/
|
|
42
|
+
on<A, I, R2>(def: EventDef<A, I, R2>, fold: (state: S, payload: A) => S): ProjectionBuilder<S, R | R2>;
|
|
43
|
+
/**
|
|
44
|
+
* Materialise the slice. The query is a single item over the `.on`-ed defs'
|
|
45
|
+
* types (branded via `EventType.make`) and `options.tags` (default none).
|
|
46
|
+
*/
|
|
47
|
+
build(options?: {
|
|
48
|
+
readonly tags?: ReadonlyArray<Tag>;
|
|
49
|
+
}): SliceProjection<S, R>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Start a slice for `initialState`. Returns a builder with no defs yet
|
|
53
|
+
* (`R = never`); each `.on(def, fold)` widens `R` by the def's requirement.
|
|
54
|
+
*/
|
|
55
|
+
export declare const projection: <S>(initialState: S) => ProjectionBuilder<S, never>;
|
|
56
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EACL,KAAK,aAAa,EAIlB,KAAK,GAAG,EACT,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEvC;;;;;GAKG;AACH,MAAM,WAAW,eAAe,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,CAAC,CAAC,GAAG,OAAO,CAC/D,SAAQ,aAAa,CAAC,CAAC,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAA;CAClE;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK;IAC7C;;;;;;;OAOG;IACH,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EACT,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EACvB,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,GAChC,iBAAiB,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAA;IAC/B;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,CAAA;KAAE,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CAC/E;AAED;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,cAAc,CAAC,KAAG,iBAAiB,CAAC,CAAC,EAAE,KAAK,CA4FzE,CAAA"}
|