@kairos-es/codec 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.
Files changed (88) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +46 -0
  3. package/dist/cjs/Serializer.js +34 -0
  4. package/dist/cjs/Serializer.js.map +1 -0
  5. package/dist/cjs/Uuid.js +41 -0
  6. package/dist/cjs/Uuid.js.map +1 -0
  7. package/dist/cjs/decider.js +152 -0
  8. package/dist/cjs/decider.js.map +1 -0
  9. package/dist/cjs/decisionModel.js +155 -0
  10. package/dist/cjs/decisionModel.js.map +1 -0
  11. package/dist/cjs/decode.js +170 -0
  12. package/dist/cjs/decode.js.map +1 -0
  13. package/dist/cjs/deserialiseThenDecode.js +20 -0
  14. package/dist/cjs/deserialiseThenDecode.js.map +1 -0
  15. package/dist/cjs/errors.js +94 -0
  16. package/dist/cjs/errors.js.map +1 -0
  17. package/dist/cjs/event.js +26 -0
  18. package/dist/cjs/event.js.map +1 -0
  19. package/dist/cjs/index.js +112 -0
  20. package/dist/cjs/index.js.map +1 -0
  21. package/dist/cjs/projection.js +118 -0
  22. package/dist/cjs/projection.js.map +1 -0
  23. package/dist/cjs/registry.js +41 -0
  24. package/dist/cjs/registry.js.map +1 -0
  25. package/dist/cjs/toEvent.js +51 -0
  26. package/dist/cjs/toEvent.js.map +1 -0
  27. package/dist/dts/Serializer.d.ts +27 -0
  28. package/dist/dts/Serializer.d.ts.map +1 -0
  29. package/dist/dts/Uuid.d.ts +19 -0
  30. package/dist/dts/Uuid.d.ts.map +1 -0
  31. package/dist/dts/decider.d.ts +157 -0
  32. package/dist/dts/decider.d.ts.map +1 -0
  33. package/dist/dts/decisionModel.d.ts +129 -0
  34. package/dist/dts/decisionModel.d.ts.map +1 -0
  35. package/dist/dts/decode.d.ts +92 -0
  36. package/dist/dts/decode.d.ts.map +1 -0
  37. package/dist/dts/deserialiseThenDecode.d.ts +62 -0
  38. package/dist/dts/deserialiseThenDecode.d.ts.map +1 -0
  39. package/dist/dts/errors.d.ts +79 -0
  40. package/dist/dts/errors.d.ts.map +1 -0
  41. package/dist/dts/event.d.ts +71 -0
  42. package/dist/dts/event.d.ts.map +1 -0
  43. package/dist/dts/index.d.ts +63 -0
  44. package/dist/dts/index.d.ts.map +1 -0
  45. package/dist/dts/projection.d.ts +56 -0
  46. package/dist/dts/projection.d.ts.map +1 -0
  47. package/dist/dts/registry.d.ts +46 -0
  48. package/dist/dts/registry.d.ts.map +1 -0
  49. package/dist/dts/toEvent.d.ts +34 -0
  50. package/dist/dts/toEvent.d.ts.map +1 -0
  51. package/dist/esm/Serializer.js +26 -0
  52. package/dist/esm/Serializer.js.map +1 -0
  53. package/dist/esm/Uuid.js +33 -0
  54. package/dist/esm/Uuid.js.map +1 -0
  55. package/dist/esm/decider.js +143 -0
  56. package/dist/esm/decider.js.map +1 -0
  57. package/dist/esm/decisionModel.js +146 -0
  58. package/dist/esm/decisionModel.js.map +1 -0
  59. package/dist/esm/decode.js +163 -0
  60. package/dist/esm/decode.js.map +1 -0
  61. package/dist/esm/deserialiseThenDecode.js +13 -0
  62. package/dist/esm/deserialiseThenDecode.js.map +1 -0
  63. package/dist/esm/errors.js +85 -0
  64. package/dist/esm/errors.js.map +1 -0
  65. package/dist/esm/event.js +19 -0
  66. package/dist/esm/event.js.map +1 -0
  67. package/dist/esm/index.js +63 -0
  68. package/dist/esm/index.js.map +1 -0
  69. package/dist/esm/package.json +4 -0
  70. package/dist/esm/projection.js +110 -0
  71. package/dist/esm/projection.js.map +1 -0
  72. package/dist/esm/registry.js +34 -0
  73. package/dist/esm/registry.js.map +1 -0
  74. package/dist/esm/toEvent.js +44 -0
  75. package/dist/esm/toEvent.js.map +1 -0
  76. package/package.json +31 -0
  77. package/src/Serializer.ts +37 -0
  78. package/src/Uuid.ts +36 -0
  79. package/src/decider.ts +219 -0
  80. package/src/decisionModel.ts +182 -0
  81. package/src/decode.ts +256 -0
  82. package/src/deserialiseThenDecode.ts +76 -0
  83. package/src/errors.ts +91 -0
  84. package/src/event.ts +90 -0
  85. package/src/index.ts +71 -0
  86. package/src/projection.ts +163 -0
  87. package/src/registry.ts +87 -0
  88. package/src/toEvent.ts +63 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * A `Decider`: the write side's unit of decision. It pairs the projections a
3
+ * command handler reads (its decision model) with an effectful `decide` step
4
+ * that turns the folded state plus a command into the events to append.
5
+ *
6
+ * This is PRODUCTION code and it lives beside the `buildDecisionModel` it
7
+ * drives. It began in `@kairos-es/testing` because the Given/When/Then surface
8
+ * was its first consumer, but nothing in it is test apparatus: every import
9
+ * resolves to this package or to `@kairos-es/core`, and a test framework is
10
+ * never one of them. That home was a real hazard rather than a tidiness
11
+ * complaint — `@kairos-es/testing` peers `@effect/vitest`, so a command handler
12
+ * typed against `Decider` (as this repo's own worked example is, in `src/` and
13
+ * not `test/`) named a package whose sibling module imports `expect` and `it` at
14
+ * top level, and a handler that actually RAN a command would have
15
+ * runtime-imported `runDecider` from it, loading a test framework into a
16
+ * production module graph. `@kairos-es/read/testing` documents that same hazard
17
+ * as the reason a subpath may add no peer; the difference here is that this is
18
+ * not a testing surface at all, so the fix is a home rather than a rule.
19
+ *
20
+ * `@kairos-es/testing` now imports these two symbols like any other consumer,
21
+ * and NO compatibility re-export is left behind there: nothing is published yet,
22
+ * so a re-export would be permanent clutter bought for no consumer, and it would
23
+ * preserve in that package's public surface exactly the coupling the move
24
+ * exists to remove. That is the argument ADR-0008 made when `retryOnConflict`
25
+ * moved to the package that owns the error it matches, and it holds here for the
26
+ * same reason — a write-side consumer depending on the wrong package is
27
+ * backwards, whichever direction the dependency points.
28
+ *
29
+ * `projections` is a *function of the command* because a DCB decision model is
30
+ * parametrised by the command's own identifiers (e.g. narrow to
31
+ * `course:{command.courseId}`), which keeps the consistency boundary — and the
32
+ * `failIfEventsMatch` guard the read produces — as tight as the decision
33
+ * actually needs. This mirrors dcb-events, where the command handler calls
34
+ * `buildDecisionModel(store, { ...projections built from command.data })`.
35
+ *
36
+ * `runDecider` runs the DCB command cycle against whatever `DcbEventStore` is in
37
+ * context: build the decision model for the command (a real `read` + decode +
38
+ * fold via `buildDecisionModel`), decide, then `append` the new events under the
39
+ * append condition the read produced. That IS the optimistic-concurrency
40
+ * contract, in production and in a scenario alike — which is also why the
41
+ * Given/When/Then surface built on it exercises the real fold and the real store
42
+ * rather than a mock. It is a free function rather than a `Context.Tag` service,
43
+ * so it is still not the "production command-runner service method" a retrying
44
+ * `Layer` would need (ADR-0008): the correct retry remains
45
+ * `retryOnConflict(runDecider(...))`, wrapping the whole cycle so a fresh read
46
+ * re-derives the condition.
47
+ *
48
+ * The `decide` step produces its events effectfully (they are encoded via
49
+ * `toEvent`, so it requires `Serializer | Uuid | R`) and may fail on its own
50
+ * error channel (a domain rule violation, e.g. "course already exists"); the
51
+ * append may fail with `AppendConditionFailed` if a concurrent writer moved the
52
+ * boundary, and decode may fail with `CodecError`. All surface on the error
53
+ * channel, so a caller handles all three the same way — and
54
+ * `@kairos-es/testing`'s `.thenError(tag)` can assert any of them.
55
+ */
56
+ import type { AppendConditionFailed, CompositeState, DcbEvent, Position } from '@kairos-es/core';
57
+ import { DcbEventStore } from '@kairos-es/core';
58
+ import type { Array as Arr } from 'effect';
59
+ import { Effect } from 'effect';
60
+ import { type RequirementOf } from './decisionModel.js';
61
+ import type { CodecError } from './errors.js';
62
+ import type { SliceProjection } from './projection.js';
63
+ import type { Serializer } from './Serializer.js';
64
+ import type { Uuid } from './Uuid.js';
65
+ /**
66
+ * A decision unit: the projections whose folded state a command is decided
67
+ * against, and the `decide` step producing the events to append.
68
+ *
69
+ * `decide` may fail with a domain error `E` (a tagged error is idiomatic, so
70
+ * `@kairos-es/testing`'s `.thenError` can match its `_tag`). It receives the
71
+ * composed decision-model `state` and the `command`, and returns the non-empty
72
+ * batch of new events — effectfully, because each event is produced via
73
+ * `toEvent` (encode -> serialise), which requires `Serializer | Uuid | R`.
74
+ *
75
+ * The projection record is bounded by this package's `SliceProjection` (the
76
+ * schema-backed, state-erased bound for a heterogeneous record); the
77
+ * precise per-key state is recovered by core's shared `CompositeState`. The
78
+ * requirement `R` is the union carried by the record's slices' schemas.
79
+ */
80
+ export interface Decider<P extends Record<string, SliceProjection>, Command, E = never, R = never> {
81
+ readonly projections: (command: Command) => P;
82
+ readonly decide: (state: CompositeState<P>, command: Command) => Effect.Effect<Arr.NonEmptyReadonlyArray<DcbEvent>, E, Serializer | Uuid | R>;
83
+ }
84
+ /**
85
+ * Build a `Decider` with all four of its type arguments INFERRED: the slice
86
+ * record `P` from what `projections` returns, the error union `E` and the extra
87
+ * requirement `R` from what `decide` returns. The command type is the one thing
88
+ * a caller still writes, once, as the annotation on the `projections` parameter
89
+ * — `Command` occurs only in parameter positions, so nothing else could supply
90
+ * it, and it is domain information (the command this decision is parametrised
91
+ * by) rather than plumbing.
92
+ *
93
+ * It adds no capability: the interface stays directly inhabitable and this
94
+ * returns exactly the object literal a caller would have written. What it
95
+ * changes is what an author must SPELL. An object literal annotated
96
+ * `Decider<P, Command, E>` has to state the whole slice record — a
97
+ * `ReturnType<typeof someProjection>` per key, a slice's state type being
98
+ * inferred by `projection().on().build()` and so having no name to write — plus
99
+ * the union of every error its decide branches can fail with. Dropping the
100
+ * annotation instead is not open to the author: without it `decide`'s `state`
101
+ * and `command` parameters have no contextual type and are implicitly `any`. So
102
+ * the cost landed hardest on the worked example, where some thirty lines of type
103
+ * plumbing sat between the reader and the three decisions that file exists to
104
+ * teach.
105
+ *
106
+ * `Eff` captures decide's WHOLE return type as one type parameter instead of
107
+ * inferring `E` and `R` from an `Effect<…, E, Serializer | Uuid | R>` shaped
108
+ * target, and that is the part that had to be got right rather than merely
109
+ * written down. A real `decide` returns a UNION of effects — one `Effect.fail`
110
+ * per rule it enforces, plus the produce path — and TypeScript infers a type
111
+ * parameter from a union source by choosing one constituent's candidate, not by
112
+ * unioning them. Against the shaped target `E` therefore came out as whichever
113
+ * branch won, and every other branch was then rejected against it. Inference to
114
+ * a NAKED type variable takes the union whole, which is why `Eff` is the
115
+ * parameter and `Effect.Effect.Error` / `Effect.Effect.Context` recover the two
116
+ * channels afterwards: those do distribute across a union of effects.
117
+ *
118
+ * The constraint on `Eff` is load-bearing twice over. It is what still refuses a
119
+ * `decide` that could produce an EMPTY batch, the one guarantee the shaped
120
+ * target used to give and the one `append` relies on. It is also what
121
+ * contextually types the produce path's `(event) => [event]` as a one-element
122
+ * TUPLE; unconstrained, that widens to `DcbEvent[]`, which is not a
123
+ * `NonEmptyReadonlyArray` and would fail at the call site.
124
+ *
125
+ * The single `as` is narrow and unavoidable: the declared return is a
126
+ * conditional over an unresolved `Eff`, so while `Eff` is still a type variable
127
+ * the checker cannot relate `(…) => Eff` to `(…) => Effect<…,
128
+ * Effect.Error<Eff>, …>`. The value is the two functions it was handed, so
129
+ * there is nothing for the cast to be wrong about.
130
+ *
131
+ * `R` excludes `Serializer | Uuid` because that is what `R` MEANS on `Decider`:
132
+ * the interface writes decide's requirement as `Serializer | Uuid | R`, so `R`
133
+ * is what the produce side needs BEYOND the codec's own two services. Leaving
134
+ * them in would state the same requirement twice and make an ordinary decider
135
+ * read as though it carried an extra one, though the set `runDecider` ends up
136
+ * asking for is identical either way. The exclusion cannot swallow a caller's
137
+ * own service by accident: a `Context.Tag`'s requirement type carries its key as
138
+ * a literal `Id` field, so only a tag declared under `@kairos-es/codec/Uuid` or
139
+ * `@kairos-es/codec/Serializer` matches — and a tag under that key IS that
140
+ * service, context being keyed by the same string at runtime.
141
+ */
142
+ export declare const decider: <P extends Record<string, SliceProjection>, Command, Eff extends Effect.Effect<Arr.NonEmptyReadonlyArray<DcbEvent>, unknown, unknown>>(projections: (command: Command) => P, decide: (state: CompositeState<P>, command: Command) => Eff) => Decider<P, Command, Effect.Effect.Error<Eff>, Exclude<Effect.Effect.Context<Eff>, Serializer | Uuid>>;
143
+ /**
144
+ * Run the full DCB command cycle for a decider against the ambient
145
+ * `DcbEventStore`: build the decision model (real read + decode + fold), decide,
146
+ * and append under the read's append condition. Returns the last new position.
147
+ *
148
+ * The requirement is `DcbEventStore | Serializer | Uuid | R |
149
+ * RequirementOf<P>`: `Serializer` enters through both the decode
150
+ * (`buildDecisionModel`) and produce (`toEvent`) sides, `Uuid` through produce,
151
+ * the slices' `RequirementOf<P>` through decode, and `R` through produce. (For a
152
+ * def whose `Schema` carries a requirement, `RequirementOf<P>` and `R` are the
153
+ * same tag.) Any `decide` failure, a decode `CodecError`, or an
154
+ * `AppendConditionFailed` propagates on the error channel unchanged.
155
+ */
156
+ export declare const runDecider: <P extends Record<string, SliceProjection>, Command, E, R>(decider: Decider<P, Command, E, R>, command: Command) => Effect.Effect<Position, E | CodecError | AppendConditionFailed, DcbEventStore | Serializer | Uuid | R | RequirementOf<P>>;
157
+ //# sourceMappingURL=decider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decider.d.ts","sourceRoot":"","sources":["../../src/decider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACd,QAAQ,EACR,QAAQ,EACT,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAElC;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,OAAO,CACtB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,CAAC,GAAG,KAAK,EACT,CAAC,GAAG,KAAK;IAET,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,CAAA;IAC7C,QAAQ,CAAC,MAAM,EAAE,CACf,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EACxB,OAAO,EAAE,OAAO,KACb,MAAM,CAAC,MAAM,CAChB,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EACnC,CAAC,EACD,UAAU,GAAG,IAAI,GAAG,CAAC,CACtB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,eAAO,MAAM,OAAO,GAClB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,GAAG,SAAS,MAAM,CAAC,MAAM,CACvB,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EACnC,OAAO,EACP,OAAO,CACR,EAED,aAAa,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,EACpC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,KAAK,GAAG,KAC1D,OAAO,CACR,CAAC,EACD,OAAO,EACP,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EACxB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC,CAOrD,CAAA;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GACrB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,CAAC,EACD,CAAC,EAED,SAAS,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAClC,SAAS,OAAO,KACf,MAAM,CAAC,MAAM,CACd,QAAQ,EACR,CAAC,GAAG,UAAU,GAAG,qBAAqB,EACtC,aAAa,GAAG,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAStD,CAAA"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Read-and-fold over a slice record: read the composed query, decode each event
3
+ * through the matching def's `Schema` in an effectful `Stream.mapEffect` stage,
4
+ * then fold *purely* through core's `composeProjections`.
5
+ *
6
+ * TWO exports sit over that ONE body, the second expressed over the first.
7
+ * `modelAtHead` stops where the fold does, at `{ state, head }` — a
8
+ * materialisation of the record with no table, no checkpoint and no runner.
9
+ * `buildDecisionModel` adds the only part that is genuinely decision-specific:
10
+ * the `AppendCondition` derived from the same query and that same head (the DCB
11
+ * optimistic-concurrency contract). It is core's old
12
+ * `buildDecisionModel(Record<string, AnyProjection>)` surface, moved here
13
+ * because decode needs `Schema` + the `Serializer`.
14
+ *
15
+ * The fold stays pure and in core; only the decode stage is effectful, and that
16
+ * stage lives in `decode.ts` — `decodeSlices` — so the read side folds a live
17
+ * `subscribe` stream through the *same* decode this module folds a bounded
18
+ * `read` stream through.
19
+ *
20
+ * The result requirement is exactly `DcbEventStore | Serializer | R`, where `R`
21
+ * is the union of the record's slices' requirements — inferred with no cast,
22
+ * `R` being carried covariantly by each slice's `schemas` map value type.
23
+ * `Serializer` is not yielded here: `decodeSlices` reads it from context itself,
24
+ * so the requirement arrives through the decode stage rather than through a
25
+ * binding this module owns (see `decode.ts` for why).
26
+ */
27
+ import { type AppendCondition, type CompositeState, DcbEventStore, type Position } from '@kairos-es/core';
28
+ import { Effect } from 'effect';
29
+ import type { CodecError } from './errors.js';
30
+ import type { SliceProjection } from './projection.js';
31
+ import type { Serializer } from './Serializer.js';
32
+ /** The union of requirements `R` carried by a record of `SliceProjection`s. */
33
+ export type RequirementOf<T extends Record<string, SliceProjection>> = T[keyof T] extends SliceProjection<infer _S, infer R> ? R : never;
34
+ /**
35
+ * Read the events for the composed slices' query, decode each through its def's
36
+ * `Schema`, fold into per-key state, and return `{ state, head }`: one slice
37
+ * record materialised at read time, with no table, no checkpoint and no runner.
38
+ *
39
+ * ## Why `head` comes back, and not bare `state`
40
+ *
41
+ * `head` is the consistency statement, and without it the state is a figure with
42
+ * no as-of — which is why the as-of is in the name. The fold stops at the
43
+ * position the read returned and is maintained no further, so `head` is the
44
+ * whole of what a caller can say about this state's currency: it is what lets
45
+ * the write path turn it into an `AppendCondition` (see `buildDecisionModel`),
46
+ * and what lets a read-time caller report, cache or compare a state against a
47
+ * position rather than against a wall clock.
48
+ *
49
+ * It is also the inversion worth naming out loud, because intuition runs the
50
+ * other way. A MAINTAINED view is consistent as of its CHECKPOINT, which trails
51
+ * the log's head by however far behind its runner is; this fold is consistent as
52
+ * of the head itself. So of two materialisations of one slice record, the one
53
+ * with no table, no checkpoint and no runner is the MOST current, and the table
54
+ * beside it is the stale one. Reach for a maintained view for the queries a
55
+ * single fold cannot serve — across many identities, ordered, joined, paginated
56
+ * — and never because it is fresher, because it is not.
57
+ *
58
+ * `head` is LOG-GLOBAL: it is the head the store's `read` returned, so it moves
59
+ * for an event no slice here selects. `test/decisionModel.test.ts` pins that
60
+ * behaviour EXPLICITLY, over four appended events one of which the composed query
61
+ * excludes by tag; `test/modelAtHead.test.ts` pins it incidentally, its own head
62
+ * assertion landing on that same excluded event's position. Two guards, one of them
63
+ * deliberate — reach for the explicit one when the property itself is what changes.
64
+ * Log-global neither tightens nor loosens the derived guard — there can be no
65
+ * event matching the query between the query's last match and the log head, so a
66
+ * query-filtered head would refuse exactly the same appends — which is precisely
67
+ * why the store's own figure is the one to carry: one fact about the log, not a
68
+ * per-query derivation, and the honest as-of for a state folded from everything
69
+ * the query selected up to it.
70
+ *
71
+ * ## Why this lives in the codec and not in `@kairos-es/read`
72
+ *
73
+ * In `@kairos-es/read` it would be the only export touching none of that
74
+ * package's machinery: no checkpoint, no `EventLogDurability` (R2 is vacuous
75
+ * where there is no checkpoint to outlive the log), no rung of that package's
76
+ * construction gate, and none of its test apparatus. The vocabulary is already
77
+ * this package's — ADR-0004 names the typed slice path's clients as "a decision
78
+ * model / materialisation / command" — and the body is `buildDecisionModel`'s,
79
+ * which is here for the reason the module doc gives.
80
+ */
81
+ export declare const modelAtHead: <T extends Record<string, SliceProjection>>(projections: T) => Effect.Effect<{
82
+ readonly state: CompositeState<T>;
83
+ readonly head: Position;
84
+ }, CodecError, DcbEventStore | Serializer | RequirementOf<T>>;
85
+ /**
86
+ * The write side's decision model: `modelAtHead` plus the `AppendCondition` that
87
+ * reuses the read query as `failIfEventsMatch` and the read head as `after` (the
88
+ * DCB optimistic-concurrency contract).
89
+ *
90
+ * It is expressed OVER `modelAtHead` rather than beside it, and that is the point
91
+ * of the pair. Two functions reading, decoding and folding a slice record almost
92
+ * identically would need two sets of tests and would be free to drift on the
93
+ * decode stage, on the cast recovering `CompositeState<T>`, or on which record
94
+ * the query is composed from. Re-expressing makes this a decomposition instead
95
+ * of a near-duplicate: `modelAtHead` is the shared part, this function is the
96
+ * condition and nothing else, and each is shorter than the one function was.
97
+ *
98
+ * `composeProjections` therefore runs TWICE for one command — once inside
99
+ * `modelAtHead` for the read, once here for `failIfEventsMatch` — and that is
100
+ * deliberate rather than waste. It is a pure derivation of the record, so the two
101
+ * queries cannot disagree however either call site changes. The alternative,
102
+ * returning the query from `modelAtHead`, would put a third field on a READ-TIME
103
+ * API that no read-time caller wants, to save one pure merge over a handful of
104
+ * slices; a surface paying for a body's convenience is the worse trade.
105
+ *
106
+ * ## How to attribute the read, the decode and the fold — the rule, stated once
107
+ *
108
+ * Because this function is `Effect.map` over `modelAtHead`, prose about the codec
109
+ * splits into two kinds and only one of them may name this symbol:
110
+ *
111
+ * - Naming this function as the ENTRY POINT a caller invokes, or as the path
112
+ * something is reached THROUGH, is correct — "the `buildDecisionModel` fold", "the
113
+ * write path reaches decode through `buildDecisionModel`". Transitive reach is real
114
+ * reach, and this is the symbol a command actually calls.
115
+ * - Naming it as the function that CONTAINS the read, the decode or the fold is
116
+ * FALSE. All three are `modelAtHead`'s; this body adds an `AppendCondition` and
117
+ * nothing else.
118
+ *
119
+ * Worth writing down because the distinction is invisible from a call site and the
120
+ * mistake is systematic rather than careless: every such claim was TRUE before
121
+ * `modelAtHead` was extracted from here, so a doc written against the older shape
122
+ * reads perfectly and is wrong. The correction is to name `modelAtHead`, never to
123
+ * soften the sentence.
124
+ */
125
+ export declare const buildDecisionModel: <T extends Record<string, SliceProjection>>(projections: T) => Effect.Effect<{
126
+ readonly state: CompositeState<T>;
127
+ readonly appendCondition: AppendCondition;
128
+ }, CodecError, DcbEventStore | Serializer | RequirementOf<T>>;
129
+ //# sourceMappingURL=decisionModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decisionModel.d.ts","sourceRoot":"","sources":["../../src/decisionModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,aAAa,EACb,KAAK,QAAQ,EACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAA;AAEvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,+EAA+E;AAC/E,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,IACjE,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,eAAe,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACnE,aAAa,CAAC,KACb,MAAM,CAAC,MAAM,CACd;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,EAC9D,UAAU,EACV,aAAa,GAAG,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,CAyB3C,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAC1E,aAAa,CAAC,KACb,MAAM,CAAC,MAAM,CACd;IACE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAA;IACjC,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAA;CAC1C,EACD,UAAU,EACV,aAAa,GAAG,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,CAU1C,CAAA"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The shared decode stage: the wire-bytes -> typed-payload half of the codec
3
+ * seam, lifted out of `buildDecisionModel` into a `Stream` combinator so every
4
+ * consumer of a slice record decodes through *one* implementation.
5
+ *
6
+ * Two consumers, one stage. `modelAtHead` folds a bounded `read` stream into
7
+ * decision state, which is how `buildDecisionModel` gets one; the read side folds
8
+ * an unbounded `subscribe` stream into a materialised view. They differ in
9
+ * everything *downstream* of decode (bounded vs live, a pure fold vs a
10
+ * transactional commit) and in nothing upstream of it — same slice record, same `type -> Schema` lookup, same
11
+ * `Serializer`, same failure taxonomy — so decode is exactly where the seam
12
+ * belongs. Duplicating it would let the two paths drift: a read side with its
13
+ * own copy could silently accept an event the decision path rejects, or reword
14
+ * a `CodecError` reason, and nothing would catch it.
15
+ *
16
+ * Why a `Stream` combinator rather than an `Effect` over a materialised array:
17
+ * decode must stay back-pressured. `Stream.mapEffect` with no `concurrency`
18
+ * option decodes one event at a time, pulling only as fast as the consumer
19
+ * folds, and that is what lets a projection catch up over a backlog far larger
20
+ * than memory.
21
+ *
22
+ * Be precise about what adding `concurrency` here would and would not cost,
23
+ * because the obvious reason is the wrong one. Effect v3's `mapEffect` is
24
+ * order-PRESERVING even when concurrent — reordering needs the separate
25
+ * `unordered: true` option (verified against the installed
26
+ * `effect@3.22`'s `Stream.d.ts`, and probed at runtime) — so a bare
27
+ * `concurrency` would NOT hand the fold its events out of position order. What
28
+ * it would actually cost is the back-pressure above, and it would run
29
+ * application-authored `Schema` transforms (an ADR-0004 decryption hook, say)
30
+ * concurrently, which their authors have no reason to expect. `unordered: true`
31
+ * is the option that would be an outright correctness bug against an
32
+ * order-sensitive fold. Sequential is the right default for both reasons; the
33
+ * order argument alone does not carry it.
34
+ *
35
+ * Why these failures are values and not defects: corrupt bytes, an absent
36
+ * schema and a `Schema` mismatch are *data* problems an application can
37
+ * legitimately handle (quarantine the event, fail the command, alert), so they
38
+ * belong on the error channel as `CodecError` — see `errors.ts`. The three
39
+ * `reason` strings are observable surface; treat them as fixed, not cosmetic.
40
+ *
41
+ * Two of those three are no longer this module's to reword. `deserialise` and
42
+ * `decode` are fixed in `deserialiseThenDecode`, the two-step this stage shares
43
+ * with the general (registry) path, precisely so that a claim about surface has
44
+ * ONE implementation behind it rather than a copy per path. The absent-schema
45
+ * reason stays here, because the two paths deliberately disagree about what a
46
+ * lookup miss means — `UnknownEventType` there, ADR-0005 — and that helper's doc
47
+ * carries the argument.
48
+ */
49
+ import type { DecodedEvent, SequencedEvent } from '@kairos-es/core';
50
+ import { Stream } from 'effect';
51
+ import type { RequirementOf } from './decisionModel.js';
52
+ import { CodecError } from './errors.js';
53
+ import type { SliceProjection } from './projection.js';
54
+ import { Serializer } from './Serializer.js';
55
+ /**
56
+ * The public decode stage: give it the slice record, get back a `Stream`
57
+ * combinator that decodes every event those slices can handle.
58
+ *
59
+ * **Why one call rather than two.** The lookup and the stage were briefly two
60
+ * public symbols — merge the record, then hand the map to the stage — and every
61
+ * consumer in the repo composed them in exactly that order and did nothing else
62
+ * with the intermediate map. A two-step protocol with one legal composition is
63
+ * not flexibility, it is a way to get it wrong: a caller can merge the wrong
64
+ * record, or a record that is not the one its `Query` was composed from, and
65
+ * that second mistake is silent. In the read side it would decode a live
66
+ * subscription against schemas the subscription never selects — every event
67
+ * arriving would fail `no schema for event type`, framed as a data problem when
68
+ * it is a wiring one. Taking the slices directly removes the intermediate value
69
+ * a caller could substitute, so the schemas provably come from the same record
70
+ * the query does.
71
+ *
72
+ * **Still curried, deliberately.** The merge is a pure derivation of the slice
73
+ * record, so both consumers build the stage ONCE — per decision model, per read
74
+ * model — and reuse it across every stream they drive. Rebuilding the table per
75
+ * stream (let alone per restart) would only burn allocations, and the currying
76
+ * is what makes "built once" the natural way to call this rather than a
77
+ * discipline.
78
+ *
79
+ * **It can REFUSE the record**, and does so the moment it is called rather than
80
+ * once events are flowing: a type reachable from two slices under two DIFFERENT
81
+ * `Schema`s throws, in the same words `makeRegistry` uses. See
82
+ * `mergeSliceSchemas` for the rule, why reference equality is its discriminator
83
+ * and why a throw is the right mechanism. Both in-repo consumers call this
84
+ * before there is a stream to fail, so inside their `Effect.gen`s the refusal is
85
+ * a defect at construction.
86
+ *
87
+ * `RequirementOf<T>` surfaces in the result's `R`, so a slice whose `Schema`
88
+ * carries a requirement (an application-authored decryption transform, ADR-0004)
89
+ * propagates it to the caller with no cast at the call site.
90
+ */
91
+ export declare const decodeSlices: <T extends Record<string, SliceProjection>>(slices: T) => (<E, R>(events: Stream.Stream<SequencedEvent, E, R>) => Stream.Stream<DecodedEvent, E | CodecError, R | RequirementOf<T> | Serializer>);
92
+ //# sourceMappingURL=decode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/decode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACnE,OAAO,EAAuB,MAAM,EAAE,MAAM,QAAQ,CAAA;AAIpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AA0JzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACpE,QAAQ,CAAC,KACR,CAAC,CAAC,CAAC,EAAE,CAAC,EACP,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,KACxC,MAAM,CAAC,MAAM,CAChB,YAAY,EACZ,CAAC,GAAG,UAAU,EACd,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAClC,CAA4C,CAAA"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The two-step every decode in this package performs, in one implementation:
3
+ * deserialise the wire bytes through the `Serializer` its caller already holds,
4
+ * then `Schema.decodeUnknown` the result, each half's failure wrapped as the
5
+ * `CodecError` whose `reason` says WHICH half failed.
6
+ *
7
+ * **What it is the single home of.** The two `reason` strings `'deserialise'`
8
+ * and `'decode'`, and their ORDER. Why those strings are observable surface —
9
+ * fixed rather than cosmetic, because an application quarantines an event,
10
+ * alerts or fails a command off them — is argued in `decode.ts`'s module doc and
11
+ * is not restated here. What this module adds is that the argument had TWO
12
+ * implementations standing behind it: the slice stage (`decodeEvents`) and the
13
+ * general/registry path (`makeRegistry`'s `decode`) each wrote the pair out, in
14
+ * one package, and only the slice copy was pinned by a test. A reworded `reason`
15
+ * on the registry path would have been caught by nothing. Sharing the step
16
+ * DELETES that drift channel rather than alarming it, which is why this exists
17
+ * as a function and not as a second test.
18
+ *
19
+ * **The order is contract, not accident.** Deserialise has to run first because
20
+ * `Schema.decodeUnknown` has nothing to read until it has, and the observable
21
+ * consequence is that the two reasons PARTITION the causes an operator acts on:
22
+ * rotten bytes are always `'deserialise'` and never `'decode'`, so a storage or
23
+ * producer problem never arrives dressed as schema drift. Swapping the labels
24
+ * would not break a decode; it would send every incident to the wrong team.
25
+ *
26
+ * **Why a `Serializer` VALUE rather than reading the tag here.** The two callers
27
+ * acquire it at deliberately different moments and this step must not flatten
28
+ * that. `decodeEvents` reads it ONCE when the stream starts (`Stream.unwrap`), so
29
+ * a backlog of a million events does not re-read context per event; the
30
+ * registry's `decode` is a per-event `Effect` and yields it inside its own
31
+ * `Effect.gen`. Taking the resolved service preserves both, and keeps
32
+ * `Serializer` off this signature's `R`, so the requirement stays declared where
33
+ * the acquisition actually happens — which is where a reader goes looking for it.
34
+ *
35
+ * **Why the LOOKUP is not folded in**, though both callers do one immediately
36
+ * before calling this: the two paths disagree about what a miss MEANS, and the
37
+ * disagreement is deliberate. The slice path fails `CodecError` with
38
+ * `no schema for event type '<type>'` — a caller named the type and the record it
39
+ * supplied has no entry for it, so the wiring is wrong. The registry path fails
40
+ * `UnknownEventType` — the type arrived off the WIRE and the registry does not
41
+ * know it, which is ADR-0005 decode-totality and the one fault a rolling
42
+ * deployment is EXPECTED to meet. `errors.ts` argues why those two sentences stay
43
+ * greppable apart. Folding the lookup in would mean a per-caller miss
44
+ * constructor: a second seam, bought to share the one thing the two paths
45
+ * genuinely do not have in common.
46
+ *
47
+ * `R` is the SCHEMA's requirement, propagated untouched — an ADR-0004 decryption
48
+ * transform reaches the caller's `R` through here with no cast. The three
49
+ * parameters are positional because their types are mutually unassignable, so a
50
+ * transposed call is a compile error rather than a silent inversion.
51
+ *
52
+ * PACKAGE-INTERNAL: exported to its two callers and no further, deliberately
53
+ * absent from `index.ts`. It is a shared step, not a capability on offer — a
54
+ * consumer holding bytes and a `Schema` wants `decodeSlices` or a `Registry`,
55
+ * both of which do the lookup this one refuses to.
56
+ */
57
+ import type { Context } from 'effect';
58
+ import { Effect, Schema } from 'effect';
59
+ import { CodecError } from './errors.js';
60
+ import type { Serializer } from './Serializer.js';
61
+ export declare const deserialiseThenDecode: <R>(serializer: Context.Tag.Service<Serializer>, schema: Schema.Schema<unknown, unknown, R>, bytes: Uint8Array) => Effect.Effect<unknown, CodecError, R>;
62
+ //# sourceMappingURL=deserialiseThenDecode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deserialiseThenDecode.d.ts","sourceRoot":"","sources":["../../src/deserialiseThenDecode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACrC,YAAY,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,EAC3C,QAAQ,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,EAC1C,OAAO,UAAU,KAChB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC,CAUpC,CAAA"}
@@ -0,0 +1,79 @@
1
+ declare const CodecError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
2
+ readonly _tag: "CodecError";
3
+ } & Readonly<A>;
4
+ /** A serialisation or `Schema` decode/encode failure on the codec path. */
5
+ export declare class CodecError extends CodecError_base<{
6
+ readonly reason: string;
7
+ readonly cause?: unknown;
8
+ }> {
9
+ /**
10
+ * `reason` again, as the `Error` field that says why — so the fault renders
11
+ * ITSELF.
12
+ *
13
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, so
14
+ * without this a `CodecError` stringifies to `'CodecError'`, and the sentence
15
+ * naming the schema, the event type or the missing key that failed reaches a log
16
+ * line only where the code doing the logging knows to reach in for `reason`.
17
+ * Filled, `Error.prototype.toString` yields `'CodecError: <reason>'`, which is
18
+ * what `Cause.pretty` (it reads `message` off anything `instanceof Error`) and the
19
+ * read side's projection supervisor both report — neither of them holding a table
20
+ * of this class's fields to keep in step with it.
21
+ *
22
+ * This class is on the package's PUBLISHED surface, so be exact about what the
23
+ * field moves. Everything that renders the error for a HUMAN: `String(err)`,
24
+ * `Cause.pretty` and therefore a failed fibre's `cause` annotation on every
25
+ * shipped logger, and an unhandled failure's report. Nothing that renders it for
26
+ * a MACHINE: `Data.Error` overrides `YieldableError`'s `toJSON` with
27
+ * `{ ...plainArgs, ...this }` — own enumerables plus the constructor args, and a
28
+ * prototype accessor is neither — so `JSON.stringify(err)` still emits the same
29
+ * `reason`/`_tag` and a structured log of a raw `CodecError` keeps its shape.
30
+ *
31
+ * A GETTER rather than a second field, so `reason` stays the single declaration.
32
+ * It is visible because `Data.TaggedError`'s constructor is
33
+ * `super(args?.message, …)` followed by `Object.assign(this, args)` (verified
34
+ * against `effect@3.22`'s `Data.ts`): the args above carry no `message`, so
35
+ * nothing shadows this prototype accessor with an own property.
36
+ */
37
+ get message(): string;
38
+ }
39
+ declare const UnknownEventType_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
40
+ readonly _tag: "UnknownEventType";
41
+ } & Readonly<A>;
42
+ /** A wire event whose runtime type string is outside the registry's known set. */
43
+ export declare class UnknownEventType extends UnknownEventType_base<{
44
+ readonly type: string;
45
+ }> {
46
+ /**
47
+ * The offending type, in a sentence saying what the registry did with it — so the
48
+ * fault renders ITSELF, exactly as `CodecError` above does.
49
+ *
50
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, so without
51
+ * this a decode that met an unregistered type stringifies to `'UnknownEventType'`
52
+ * — the one datum that makes the failure actionable, the type string itself,
53
+ * reaching a log line only where the logging code knows to reach in for `type`.
54
+ * Filled, `Error.prototype.toString` yields the tag followed by
55
+ * `event type 'nope.v1' is not in the registry`, which is what `Cause.pretty`
56
+ * prints and what an operator greps for. It matters here in particular because
57
+ * this class is the home of ADR-0005 decode-totality: a wire event outside the
58
+ * registered set is the expected consequence of a deployment reading a log a newer
59
+ * writer has extended, and the first question is always WHICH type — which the tag
60
+ * alone cannot answer.
61
+ *
62
+ * The wording deliberately differs from `CodecError`'s. That one says there is no
63
+ * schema for a type, and is raised on the TYPED path, where a caller named the
64
+ * type and the schema map it supplied has no entry for it. This one is the general
65
+ * (registry) path, where the type arrived off the WIRE and the registry is what
66
+ * does not know it. Distinct sentences keep the two greppable apart, which a
67
+ * shared phrasing would cost for nothing.
68
+ *
69
+ * A GETTER rather than a second field, so `type` stays the single declaration.
70
+ * What filling this field reaches, what it deliberately leaves untouched, and why
71
+ * a prototype accessor is visible at all are argued once on `CodecError` above and
72
+ * not restated here — it is the same package, the same published surface and the
73
+ * same mechanism, and two copies of that paragraph would be two things to keep in
74
+ * step. The read side's four getters point at one home for the identical reason.
75
+ */
76
+ get message(): string;
77
+ }
78
+ export {};
79
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":";;;AAeA,2EAA2E;AAC3E,qBAAa,UAAW,SAAQ,gBAA+B;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED,kFAAkF;AAClF,qBAAa,gBAAiB,SAAQ,sBAAqC;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB,CAAC;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Event definitions: a tagged `Schema` built through `event(type, fields,
3
+ * tagsFor)`. Each def is a `Schema<Payload, Wire, 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. `Schema` is invariant in
44
+ * `A`/`I`, so `EventDef` cannot be `out A`/`out I`.
45
+ */
46
+ export interface EventDef<A = unknown, I = unknown, R = never> {
47
+ readonly type: EventType;
48
+ readonly schema: Schema.Schema<A, I, R>;
49
+ readonly tagsFor: TagsFor<A>;
50
+ }
51
+ /**
52
+ * The heterogeneous bound (mirrors `Schema.Schema.Any`) for arrays/records of
53
+ * mixed defs, where the precise `A`/`I` differ per element. `tagsFor` erases to
54
+ * `(a: any) => …`, which is harmless: only the write path (`toEvent`) calls it,
55
+ * and the registry/decode path never does (stored events already carry tags).
56
+ */
57
+ export type EventDefAny = EventDef<any, any, unknown>;
58
+ /**
59
+ * Define an event from a versioned `type` literal, its payload `fields`, and its
60
+ * required `tagsFor` mapper. Builds `Schema.Struct(fields)` and brands the type
61
+ * literal *once* (the single sanctioned cast). The returned def is typed so
62
+ * `def.type` is a branded literal assignable to core's `EventType` with no
63
+ * consumer cast, `A`/`I`/`R` are inferred from the struct, and `tagsFor`
64
+ * receives the def's decoded payload type.
65
+ */
66
+ export declare const event: <const Type extends string, Fields extends Schema.Struct.Fields>(type: Type, fields: Fields, tagsFor: TagsFor<Schema.Struct.Type<Fields>>) => {
67
+ readonly type: Type & EventType;
68
+ readonly schema: Schema.Struct<Fields>;
69
+ readonly tagsFor: TagsFor<Schema.Struct.Type<Fields>>;
70
+ };
71
+ //# 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;;;;;GAKG;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,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IACvC,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"}