@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,163 @@
1
+ import { Effect, Stream } from 'effect';
2
+ import { deserialiseThenDecode } from "./deserialiseThenDecode.js";
3
+ import { CodecError } from "./errors.js";
4
+ import { Serializer } from "./Serializer.js";
5
+ /**
6
+ * Merge every slice's `type -> Schema` map into one decode lookup.
7
+ *
8
+ * A record of slices is the unit an application composes, but decode is keyed
9
+ * by the wire event's runtime `type` string, so the per-slice maps have to
10
+ * collapse into one flat table first. A type reachable from two slices is
11
+ * therefore a collision the merge has to rule on, and it rules by REFERENCE
12
+ * EQUALITY: the same `Schema` arriving twice is absorbed, two *different*
13
+ * `Schema`s under one type string are refused with a throw.
14
+ *
15
+ * **Why reference equality is the discriminator.** The legitimate case — the one
16
+ * the old last-writer-wins was written for — is two slices that `.on` the same
17
+ * `EventDef`, which is how one event feeds two views. `event()` builds its
18
+ * `Schema.Struct` once and every def hands out that one object, so those two
19
+ * slices hold the identical reference and whichever entry survives the merge
20
+ * decodes identically; `decode.test.ts` asserts that under both key orderings
21
+ * rather than leaving it as a claim. Nothing weaker is sound and nothing
22
+ * stronger is needed: two distinct defs can be structurally identical today and
23
+ * diverge on the next commit, so structural comparison would license exactly the
24
+ * pairing that is about to go wrong.
25
+ *
26
+ * **Why the other case cannot stay last-writer-wins.** Two distinct defs can
27
+ * share a `type` string — a copied def, or two versions accidentally given one
28
+ * name — and then the surviving schema is whichever slice `Object.keys` visits
29
+ * last, an ordering the record does not express and its author never chose.
30
+ * Every event of that type is then decoded against the wrong def for one of the
31
+ * slices, and where the two shapes overlap that failure is SILENT rather than a
32
+ * `CodecError`: `Schema.decodeUnknown` defaults `onExcessProperty` to `'ignore'`
33
+ * (`effect@3.22`'s `ParseResult.ts` runs its excess-key loop only under
34
+ * `'error'` or `'preserve'`), so a field the other def declared is dropped, not
35
+ * reported, and the fold downstream is handed a payload quietly missing it.
36
+ *
37
+ * **Why a throw, and why here rather than at the read side's wiring gate.**
38
+ * `makeRegistry` already treats a duplicated type as a construction-time defect
39
+ * and throws (ADR-0005, `totality.test.ts`); this is the same mistake reached
40
+ * through the slice path, so it gets the same mechanism and the same opening
41
+ * words rather than a second convention for one rule. It belongs to the codec
42
+ * because BOTH consumers need it and only one of them is on the read side: the
43
+ * write path reaches this through `buildDecisionModel` and never goes near the read
44
+ * side's wiring gate. Neither consumer can be mid-stream when it fires —
45
+ * the write path merges before its first `store.read`, and the read side
46
+ * merges after that gate and before it forks the pipeline — and inside
47
+ * their `Effect.gen`s a throw becomes a DEFECT rather than an escaping exception
48
+ * (`effect@3.22`'s `fiberRuntime.ts` run loop converts a thrown value with
49
+ * `core.die`), which is the classification a mis-wiring already has on both
50
+ * sides of the library.
51
+ *
52
+ * The sentence names BOTH slices. The two halves of this mistake are in
53
+ * different entries of the record, and a message naming only the one that
54
+ * happened to be visited second sends its author to look in the wrong place.
55
+ *
56
+ * The cast re-attaches `R`. Indexing `T` by a plain `string` key erases each
57
+ * slice to the `SliceProjection` *constraint*, whose requirement parameter
58
+ * defaults to `unknown`, so the concrete per-slice `R` is not recoverable from
59
+ * the value side. `RequirementOf<T>` recovers it from the *type* side, and the
60
+ * assertion is sound because a slice's own requirement is always a member of
61
+ * that union. Doing it once here is why no call site needs a cast.
62
+ *
63
+ * Module-private, and deliberately so: see `decodeSlices` for why the merge is
64
+ * not a step a caller is trusted to take.
65
+ */
66
+ const mergeSliceSchemas = projections => {
67
+ const schemas = new Map();
68
+ // Which slice contributed each entry, kept in lockstep with `schemas` purely
69
+ // so the refusal can name both sides of a collision. Its presence is also what
70
+ // the check below reads, rather than a second `schemas.has`, so the narrowing
71
+ // that makes the earlier key a `string` falls out of the check itself.
72
+ const contributedBy = new Map();
73
+ for (const key of Object.keys(projections)) {
74
+ const slice = projections[key];
75
+ for (const [type, schema] of slice.schemas) {
76
+ const owner = contributedBy.get(type);
77
+ if (owner !== undefined && schemas.get(type) !== schema) {
78
+ throw new Error(`duplicate event type '${type}' in slices '${owner}' and '${key}': ` + 'they hold different Schemas for one type, so decode would use ' + 'whichever slice merged last. Two slices may share an event type ' + 'only by sharing one EventDef.');
79
+ }
80
+ schemas.set(type, schema);
81
+ contributedBy.set(type, key);
82
+ }
83
+ }
84
+ return schemas;
85
+ };
86
+ /**
87
+ * The decode stage: look the event's `type` up in `schemas`, run the shared
88
+ * `deserialiseThenDecode` over the wire bytes, and emit a `DecodedEvent` (the
89
+ * sequenced envelope plus its typed payload) for the pure fold downstream. The
90
+ * lookup and the envelope are what this stage adds; the two-step in the middle,
91
+ * and the two `reason` strings it fails with, belong to that helper.
92
+ *
93
+ * Curried on `schemas` so a caller builds the lookup once — per decision model,
94
+ * per read model — and reuses the stage across every stream it drives.
95
+ *
96
+ * `Serializer` is read from context *inside* the stage (once, when the stream
97
+ * starts, via `Stream.unwrap`) rather than being pre-yielded and closed over by
98
+ * the caller. That keeps the serialisation dependency wholly inside this
99
+ * module: a consumer cannot hand in a different serialiser than the one its
100
+ * layer provides, so encode (`toEvent`) and decode provably read the same
101
+ * `Serializer` — the invariant `Serializer.ts` rests on. The obligation is
102
+ * still visible, because `Serializer` surfaces in the returned stream's `R`;
103
+ * it just is not satisfiable by hand.
104
+ *
105
+ * The stage is transparent to the input stream's own `E` and `R`: a bounded
106
+ * `read` stream (`never`, `never`) and a live `subscribe` stream (`never`,
107
+ * `Scope`) both flow through unchanged, gaining only `CodecError` and
108
+ * `Serializer`.
109
+ *
110
+ * Module-private: `decodeSlices` is the public shape. Keeping the schema-map
111
+ * form here rather than inlining it into `decodeSlices` is what makes the
112
+ * "build the lookup once, reuse the stage" property explicit in the types.
113
+ */
114
+ const decodeEvents = schemas => events => Stream.unwrap(Effect.map(Serializer, serializer => events.pipe(Stream.mapEffect(sequenced => {
115
+ const schema = schemas.get(sequenced.event.type);
116
+ if (schema === undefined) {
117
+ return Effect.fail(new CodecError({
118
+ reason: `no schema for event type '${sequenced.event.type}'`
119
+ }));
120
+ }
121
+ return deserialiseThenDecode(serializer, schema, sequenced.event.data).pipe(Effect.map(payload => ({
122
+ sequenced,
123
+ payload
124
+ })));
125
+ }))));
126
+ /**
127
+ * The public decode stage: give it the slice record, get back a `Stream`
128
+ * combinator that decodes every event those slices can handle.
129
+ *
130
+ * **Why one call rather than two.** The lookup and the stage were briefly two
131
+ * public symbols — merge the record, then hand the map to the stage — and every
132
+ * consumer in the repo composed them in exactly that order and did nothing else
133
+ * with the intermediate map. A two-step protocol with one legal composition is
134
+ * not flexibility, it is a way to get it wrong: a caller can merge the wrong
135
+ * record, or a record that is not the one its `Query` was composed from, and
136
+ * that second mistake is silent. In the read side it would decode a live
137
+ * subscription against schemas the subscription never selects — every event
138
+ * arriving would fail `no schema for event type`, framed as a data problem when
139
+ * it is a wiring one. Taking the slices directly removes the intermediate value
140
+ * a caller could substitute, so the schemas provably come from the same record
141
+ * the query does.
142
+ *
143
+ * **Still curried, deliberately.** The merge is a pure derivation of the slice
144
+ * record, so both consumers build the stage ONCE — per decision model, per read
145
+ * model — and reuse it across every stream they drive. Rebuilding the table per
146
+ * stream (let alone per restart) would only burn allocations, and the currying
147
+ * is what makes "built once" the natural way to call this rather than a
148
+ * discipline.
149
+ *
150
+ * **It can REFUSE the record**, and does so the moment it is called rather than
151
+ * once events are flowing: a type reachable from two slices under two DIFFERENT
152
+ * `Schema`s throws, in the same words `makeRegistry` uses. See
153
+ * `mergeSliceSchemas` for the rule, why reference equality is its discriminator
154
+ * and why a throw is the right mechanism. Both in-repo consumers call this
155
+ * before there is a stream to fail, so inside their `Effect.gen`s the refusal is
156
+ * a defect at construction.
157
+ *
158
+ * `RequirementOf<T>` surfaces in the result's `R`, so a slice whose `Schema`
159
+ * carries a requirement (an application-authored decryption transform, ADR-0004)
160
+ * propagates it to the caller with no cast at the call site.
161
+ */
162
+ export const decodeSlices = slices => decodeEvents(mergeSliceSchemas(slices));
163
+ //# sourceMappingURL=decode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.js","names":["Effect","Stream","deserialiseThenDecode","CodecError","Serializer","mergeSliceSchemas","projections","schemas","Map","contributedBy","key","Object","keys","slice","type","schema","owner","get","undefined","Error","set","decodeEvents","events","unwrap","map","serializer","pipe","mapEffect","sequenced","event","fail","reason","data","payload","decodeSlices","slices"],"sources":["../../src/decode.ts"],"sourcesContent":[null],"mappings":"AAiDA,SAASA,MAAM,EAAeC,MAAM,QAAQ,QAAQ;AAKpD,SAASC,qBAAqB,QAAQ,4BAAyB;AAC/D,SAASC,UAAU,QAAQ,aAAU;AAErC,SAASC,UAAU,QAAQ,iBAAc;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAMC,iBAAiB,GACrBC,WAAc,IAC4D;EAC1E,MAAMC,OAAO,GAAG,IAAIC,GAAG,EAGpB;EACH;EACA;EACA;EACA;EACA,MAAMC,aAAa,GAAG,IAAID,GAAG,EAAkB;EAC/C,KAAK,MAAME,GAAG,IAAIC,MAAM,CAACC,IAAI,CAACN,WAAW,CAAC,EAAE;IAC1C,MAAMO,KAAK,GAAGP,WAAW,CAACI,GAAG,CAAC;IAC9B,KAAK,MAAM,CAACI,IAAI,EAAEC,MAAM,CAAC,IAAIF,KAAK,CAACN,OAAO,EAAE;MAC1C,MAAMS,KAAK,GAAGP,aAAa,CAACQ,GAAG,CAACH,IAAI,CAAC;MACrC,IAAIE,KAAK,KAAKE,SAAS,IAAIX,OAAO,CAACU,GAAG,CAACH,IAAI,CAAC,KAAKC,MAAM,EAAE;QACvD,MAAM,IAAII,KAAK,CACb,yBAAyBL,IAAI,gBAAgBE,KAAK,UAAUN,GAAG,KAAK,GAClE,gEAAgE,GAChE,kEAAkE,GAClE,+BAA+B,CAClC;MACH;MACAH,OAAO,CAACa,GAAG,CACTN,IAAI,EACJC,MAA2D,CAC5D;MACDN,aAAa,CAACW,GAAG,CAACN,IAAI,EAAEJ,GAAG,CAAC;IAC9B;EACF;EACA,OAAOH,OAAO;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAMc,YAAY,GACXd,OAAiE,IAEpEe,MAA2C,IAE3CrB,MAAM,CAACsB,MAAM,CACXvB,MAAM,CAACwB,GAAG,CAACpB,UAAU,EAAGqB,UAAU,IAChCH,MAAM,CAACI,IAAI,CACTzB,MAAM,CAAC0B,SAAS,CAAEC,SAAS,IAAI;EAC7B,MAAMb,MAAM,GAAGR,OAAO,CAACU,GAAG,CAACW,SAAS,CAACC,KAAK,CAACf,IAAI,CAAC;EAChD,IAAIC,MAAM,KAAKG,SAAS,EAAE;IACxB,OAAOlB,MAAM,CAAC8B,IAAI,CAChB,IAAI3B,UAAU,CAAC;MACb4B,MAAM,EAAE,6BAA6BH,SAAS,CAACC,KAAK,CAACf,IAAI;KAC1D,CAAC,CACH;EACH;EACA,OAAOZ,qBAAqB,CAC1BuB,UAAU,EACVV,MAAM,EACNa,SAAS,CAACC,KAAK,CAACG,IAAI,CACrB,CAACN,IAAI,CACJ1B,MAAM,CAACwB,GAAG,CAAES,OAAO,KAAoB;IAAEL,SAAS;IAAEK;EAAO,CAAE,CAAC,CAAC,CAChE;AACH,CAAC,CAAC,CACH,CACF,CACF;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,OAAO,MAAMC,YAAY,GACvBC,MAAS,IAOLd,YAAY,CAAChB,iBAAiB,CAAC8B,MAAM,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,13 @@
1
+ import { Effect, Schema } from 'effect';
2
+ import { CodecError } from "./errors.js";
3
+ export const deserialiseThenDecode = (serializer, schema, bytes) => Effect.try({
4
+ try: () => serializer.deserialise(bytes),
5
+ catch: cause => new CodecError({
6
+ reason: 'deserialise',
7
+ cause
8
+ })
9
+ }).pipe(Effect.flatMap(raw => Schema.decodeUnknown(schema)(raw).pipe(Effect.mapError(cause => new CodecError({
10
+ reason: 'decode',
11
+ cause
12
+ })))));
13
+ //# sourceMappingURL=deserialiseThenDecode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deserialiseThenDecode.js","names":["Effect","Schema","CodecError","deserialiseThenDecode","serializer","schema","bytes","try","deserialise","catch","cause","reason","pipe","flatMap","raw","decodeUnknown","mapError"],"sources":["../../src/deserialiseThenDecode.ts"],"sourcesContent":[null],"mappings":"AAyDA,SAASA,MAAM,EAAEC,MAAM,QAAQ,QAAQ;AACvC,SAASC,UAAU,QAAQ,aAAU;AAGrC,OAAO,MAAMC,qBAAqB,GAAGA,CACnCC,UAA2C,EAC3CC,MAA0C,EAC1CC,KAAiB,KAEjBN,MAAM,CAACO,GAAG,CAAC;EACTA,GAAG,EAAEA,CAAA,KAAMH,UAAU,CAACI,WAAW,CAACF,KAAK,CAAC;EACxCG,KAAK,EAAGC,KAAK,IAAK,IAAIR,UAAU,CAAC;IAAES,MAAM,EAAE,aAAa;IAAED;EAAK,CAAE;CAClE,CAAC,CAACE,IAAI,CACLZ,MAAM,CAACa,OAAO,CAAEC,GAAG,IACjBb,MAAM,CAACc,aAAa,CAACV,MAAM,CAAC,CAACS,GAAG,CAAC,CAACF,IAAI,CACpCZ,MAAM,CAACgB,QAAQ,CAAEN,KAAK,IAAK,IAAIR,UAAU,CAAC;EAAES,MAAM,EAAE,QAAQ;EAAED;AAAK,CAAE,CAAC,CAAC,CACxE,CACF,CACF","ignoreList":[]}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Tagged errors for the codec. Decode failures are values on the error channel,
3
+ * never defects.
4
+ *
5
+ * `CodecError` covers every serialisation/`Schema` failure on the codec path
6
+ * (deserialise threw, `Schema.decode`/`encode` produced a `ParseError`, a
7
+ * wrong/absent encryption key surfaced through a `Schema` transform, or a
8
+ * `tagsFor` mapper threw while deriving an event's tags). It carries a
9
+ * human-readable `reason` and the underlying `cause` (exact-optional).
10
+ * `UnknownEventType` is raised only on the general (registry) path, when a wire
11
+ * event's runtime type string is outside the registered set — the home of
12
+ * ADR-0005 decode-totality.
13
+ */
14
+ import { Data } from 'effect';
15
+ /** A serialisation or `Schema` decode/encode failure on the codec path. */
16
+ export class CodecError extends /*#__PURE__*/Data.TaggedError('CodecError') {
17
+ /**
18
+ * `reason` again, as the `Error` field that says why — so the fault renders
19
+ * ITSELF.
20
+ *
21
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, so
22
+ * without this a `CodecError` stringifies to `'CodecError'`, and the sentence
23
+ * naming the schema, the event type or the missing key that failed reaches a log
24
+ * line only where the code doing the logging knows to reach in for `reason`.
25
+ * Filled, `Error.prototype.toString` yields `'CodecError: <reason>'`, which is
26
+ * what `Cause.pretty` (it reads `message` off anything `instanceof Error`) and the
27
+ * read side's projection supervisor both report — neither of them holding a table
28
+ * of this class's fields to keep in step with it.
29
+ *
30
+ * This class is on the package's PUBLISHED surface, so be exact about what the
31
+ * field moves. Everything that renders the error for a HUMAN: `String(err)`,
32
+ * `Cause.pretty` and therefore a failed fibre's `cause` annotation on every
33
+ * shipped logger, and an unhandled failure's report. Nothing that renders it for
34
+ * a MACHINE: `Data.Error` overrides `YieldableError`'s `toJSON` with
35
+ * `{ ...plainArgs, ...this }` — own enumerables plus the constructor args, and a
36
+ * prototype accessor is neither — so `JSON.stringify(err)` still emits the same
37
+ * `reason`/`_tag` and a structured log of a raw `CodecError` keeps its shape.
38
+ *
39
+ * A GETTER rather than a second field, so `reason` stays the single declaration.
40
+ * It is visible because `Data.TaggedError`'s constructor is
41
+ * `super(args?.message, …)` followed by `Object.assign(this, args)` (verified
42
+ * against `effect@3.22`'s `Data.ts`): the args above carry no `message`, so
43
+ * nothing shadows this prototype accessor with an own property.
44
+ */
45
+ get message() {
46
+ return this.reason;
47
+ }
48
+ }
49
+ /** A wire event whose runtime type string is outside the registry's known set. */
50
+ export class UnknownEventType extends /*#__PURE__*/Data.TaggedError('UnknownEventType') {
51
+ /**
52
+ * The offending type, in a sentence saying what the registry did with it — so the
53
+ * fault renders ITSELF, exactly as `CodecError` above does.
54
+ *
55
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, so without
56
+ * this a decode that met an unregistered type stringifies to `'UnknownEventType'`
57
+ * — the one datum that makes the failure actionable, the type string itself,
58
+ * reaching a log line only where the logging code knows to reach in for `type`.
59
+ * Filled, `Error.prototype.toString` yields the tag followed by
60
+ * `event type 'nope.v1' is not in the registry`, which is what `Cause.pretty`
61
+ * prints and what an operator greps for. It matters here in particular because
62
+ * this class is the home of ADR-0005 decode-totality: a wire event outside the
63
+ * registered set is the expected consequence of a deployment reading a log a newer
64
+ * writer has extended, and the first question is always WHICH type — which the tag
65
+ * alone cannot answer.
66
+ *
67
+ * The wording deliberately differs from `CodecError`'s. That one says there is no
68
+ * schema for a type, and is raised on the TYPED path, where a caller named the
69
+ * type and the schema map it supplied has no entry for it. This one is the general
70
+ * (registry) path, where the type arrived off the WIRE and the registry is what
71
+ * does not know it. Distinct sentences keep the two greppable apart, which a
72
+ * shared phrasing would cost for nothing.
73
+ *
74
+ * A GETTER rather than a second field, so `type` stays the single declaration.
75
+ * What filling this field reaches, what it deliberately leaves untouched, and why
76
+ * a prototype accessor is visible at all are argued once on `CodecError` above and
77
+ * not restated here — it is the same package, the same published surface and the
78
+ * same mechanism, and two copies of that paragraph would be two things to keep in
79
+ * step. The read side's four getters point at one home for the identical reason.
80
+ */
81
+ get message() {
82
+ return `event type '${this.type}' is not in the registry`;
83
+ }
84
+ }
85
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":["Data","CodecError","TaggedError","message","reason","UnknownEventType","type"],"sources":["../../src/errors.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;AAaA,SAASA,IAAI,QAAQ,QAAQ;AAE7B;AACA,OAAM,MAAOC,UAAW,sBAAQD,IAAI,CAACE,WAAW,CAAC,YAAY,CAG3D;EACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4BA,IAAaC,OAAOA,CAAA;IAClB,OAAO,IAAI,CAACC,MAAM;EACpB;;AAGF;AACA,OAAM,MAAOC,gBAAiB,sBAAQL,IAAI,CAACE,WAAW,CAAC,kBAAkB,CAEvE;EACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BA,IAAaC,OAAOA,CAAA;IAClB,OAAO,eAAe,IAAI,CAACG,IAAI,0BAA0B;EAC3D","ignoreList":[]}
@@ -0,0 +1,19 @@
1
+ import { Schema } from 'effect';
2
+ /**
3
+ * Define an event from a versioned `type` literal, its payload `fields`, and its
4
+ * required `tagsFor` mapper. Builds `Schema.Struct(fields)` and brands the type
5
+ * literal *once* (the single sanctioned cast). The returned def is typed so
6
+ * `def.type` is a branded literal assignable to core's `EventType` with no
7
+ * consumer cast, `A`/`I`/`R` are inferred from the struct, and `tagsFor`
8
+ * receives the def's decoded payload type.
9
+ */
10
+ export const event = (type, fields, tagsFor) => {
11
+ const schema = Schema.Struct(fields);
12
+ return {
13
+ // The single EventType brand, applied once inside event(); no consumer casts.
14
+ type: type,
15
+ schema,
16
+ tagsFor
17
+ };
18
+ };
19
+ //# sourceMappingURL=event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.js","names":["Schema","event","type","fields","tagsFor","schema","Struct"],"sources":["../../src/event.ts"],"sourcesContent":[null],"mappings":"AA+BA,SAASA,MAAM,QAAQ,QAAQ;AA+B/B;;;;;;;;AAQA,OAAO,MAAMC,KAAK,GAAGA,CAInBC,IAAU,EACVC,MAAc,EACdC,OAA4C,KAK1C;EACF,MAAMC,MAAM,GAAGL,MAAM,CAACM,MAAM,CAACH,MAAM,CAAC;EACpC,OAAO;IACL;IACAD,IAAI,EAAEA,IAAwB;IAC9BG,MAAM;IACND;GACD;AACH,CAAC","ignoreList":[]}
@@ -0,0 +1,63 @@
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? })` produces a wire event (encode
21
+ * -> serialise, tags derived from `def.tagsFor`, clock + uuid from services).
22
+ * Serialisation is a pluggable `Serializer` (JSON
23
+ * default). Encryption, when needed, is an *application*-authored `Schema`
24
+ * transform on a payload field; the codec only propagates its requirement and
25
+ * error channels — it owns no cipher and imports no `node:crypto`.
26
+ *
27
+ * `Decider` + `runDecider(decider, command)` close that path into the write
28
+ * side's whole command cycle: build the decision model, decide, append under the
29
+ * read's own condition. They live HERE, beside the `buildDecisionModel` they
30
+ * drive, rather than in `@kairos-es/testing` where they began — that package
31
+ * peers `@effect/vitest`, and a production command handler must not name it (see
32
+ * `decider.ts` for the full argument). `decider(projections, decide)` builds one
33
+ * with all four type arguments inferred, so a command's slice record and its
34
+ * error union are written once, in the two function bodies, rather than restated
35
+ * as an annotation above them.
36
+ *
37
+ * `modelAtHead(slices)` is that same read-decode-fold WITHOUT the
38
+ * `AppendCondition`: `{ state, head }`, one slice record materialised at read
39
+ * time with no table, no checkpoint and no runner, current as of the `head` its
40
+ * read returned — which is a stronger currency claim than a maintained view's
41
+ * checkpoint can make. `buildDecisionModel` is expressed over it, so the write
42
+ * path's decision model and a read-time materialisation share one body rather
43
+ * than two that could drift (see `decisionModel.ts` for both arguments).
44
+ *
45
+ * `decodeSlices(slices)` is the decode stage `modelAtHead` folds through,
46
+ * exported because the read side needs the *same* decode over a live
47
+ * `subscribe` stream. Exposing it keeps that single implementation shared
48
+ * rather than reimplemented downstream. It is deliberately ONE symbol taking
49
+ * the slice record, not a merge-then-decode pair: the pair had exactly one
50
+ * legal composition, and its only degree of freedom was to decode against a
51
+ * record other than the one the query came from (see `decode.ts`).
52
+ */
53
+ export { decider, runDecider } from "./decider.js";
54
+ export { buildDecisionModel, modelAtHead } from "./decisionModel.js";
55
+ export { decodeSlices } from "./decode.js";
56
+ export { CodecError, UnknownEventType } from "./errors.js";
57
+ export { event } from "./event.js";
58
+ export { projection } from "./projection.js";
59
+ export { makeRegistry } from "./registry.js";
60
+ export { Serializer, SerializerDefault } from "./Serializer.js";
61
+ export { toEvent } from "./toEvent.js";
62
+ export { Uuid, UuidLive, UuidTest } from "./Uuid.js";
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["decider","runDecider","buildDecisionModel","modelAtHead","decodeSlices","CodecError","UnknownEventType","event","projection","makeRegistry","Serializer","SerializerDefault","toEvent","Uuid","UuidLive","UuidTest"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAuBA,OAAO,EAAEC,UAAU,QAAQ,cAAW;AAC7D,SACEC,kBAAkB,EAClBC,WAAW,QAEN,oBAAiB;AACxB,SAASC,YAAY,QAAQ,aAAU;AACvC,SAASC,UAAU,EAAEC,gBAAgB,QAAQ,aAAU;AACvD,SAA0CC,KAAK,QAAsB,YAAS;AAC9E,SAEEC,UAAU,QAEL,iBAAc;AACrB,SAASC,YAAY,QAAuB,eAAY;AACxD,SAASC,UAAU,EAAEC,iBAAiB,QAAQ,iBAAc;AAC5D,SAASC,OAAO,QAAQ,cAAW;AACnC,SAASC,IAAI,EAAEC,QAAQ,EAAEC,QAAQ,QAAQ,WAAQ","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "sideEffects": []
4
+ }
@@ -0,0 +1,110 @@
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<unknown, 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 { EventType } from '@kairos-es/core';
21
+ /**
22
+ * Start a slice for `initialState`. Returns a builder with no defs yet
23
+ * (`R = never`); each `.on(def, fold)` widens `R` by the def's requirement.
24
+ */
25
+ export const projection = initialState => {
26
+ // Persistent builder: `make` closes over the accumulated maps, and each
27
+ // `.on` copies them into a fresh builder rather than mutating in place, so
28
+ // no two derived builders ever share mutable state (reusing an intermediate
29
+ // builder can't cross-contaminate slices).
30
+ //
31
+ // `make` is generic in the accumulated requirement `R` — the parameter the
32
+ // builder is *about*. Pinning it to `never` and casting the result back would
33
+ // make the recursion typecheck by discarding the very thing each `.on` adds;
34
+ // threading `R` means the implementation states the same widening the
35
+ // `ProjectionBuilder` interface promises, and `on` returns without a cast.
36
+ const make = (schemas, folds) => ({
37
+ on: (def, fold) => {
38
+ // A repeated type WITHIN one chain is unambiguously a mistake, so this is
39
+ // a plain `has` rather than the reference-equality rule `mergeSliceSchemas`
40
+ // applies ACROSS slices. There, two entries for one type are the intended
41
+ // way to feed one event into two views, and the schemas being one object is
42
+ // what makes the collision harmless. Here there is only ever one fold per
43
+ // type — `evolve` dispatches by type into a single map — so a second `.on`
44
+ // cannot mean "as well as", only "instead of", and it silently discards
45
+ // the author's first fold along with its schema. The asymmetry is the
46
+ // point: the same `has` up there would outlaw the legitimate case.
47
+ if (schemas.has(def.type)) {
48
+ throw new Error(`duplicate event type '${def.type}' in slice: .on was called twice ` + 'for it, and the second fold would silently replace the first. ' + 'Handle both cases in one fold.');
49
+ }
50
+ // The entry type is annotated `R | R2` rather than inferred, and that
51
+ // annotation is what carries the requirement forward — `build` reads `R`
52
+ // straight off this map, so `RequirementOf` ultimately reads it from here.
53
+ // Widening the copied `R` entries into it costs nothing: `Schema` is
54
+ // declared `<in out A, in out I, out R>` upstream, hence covariant in its
55
+ // requirement.
56
+ const nextSchemas = new Map(schemas);
57
+ // Erasing `A`/`I` is the one unavoidable cast, and `as unknown as` is what
58
+ // it takes: `Schema` is INVARIANT in both, so `Schema<A, I, R2>` and
59
+ // `Schema<unknown, unknown, R2>` are unrelated in either direction. It is
60
+ // sound because the payload is only ever read back through `evolve`, which
61
+ // re-narrows by event type via the matching `fold`. The target names `R2`
62
+ // and not `never` because `R2` is what the def actually requires — an
63
+ // `Encrypted` field's `CipherKey` is not nothing — so the cast erases only
64
+ // what it has to and states the rest truthfully.
65
+ nextSchemas.set(def.type, def.schema);
66
+ const nextFolds = new Map(folds);
67
+ // Contravariance in the payload parameter, so this one is a cast too: the
68
+ // map holds folds over `unknown`, and `evolve` supplies the payload the
69
+ // matching def decoded, which is the `A` this fold was written against.
70
+ nextFolds.set(def.type, fold);
71
+ return make(nextSchemas, nextFolds);
72
+ },
73
+ build: options => {
74
+ // Internal-layer Query assembly from plain data (see core's Projection.ts
75
+ // note): a type-only-plus-optional-tags item the public surface withholds.
76
+ const types = [...schemas.keys()].map(t => EventType.make(t));
77
+ const query = {
78
+ items: [{
79
+ types,
80
+ tags: options?.tags ?? []
81
+ }]
82
+ };
83
+ return {
84
+ initialState,
85
+ query,
86
+ // Defensive copy: hand out a fresh Map, not the builder's own. `schemas`
87
+ // is typed `ReadonlyMap`, but that is a compile-time guard only — a
88
+ // caller who casts it back to `Map` could otherwise mutate the slice's
89
+ // live decode table after `build`, desyncing it from `query` (whose
90
+ // event-types were snapshotted from these keys just above). The copy is
91
+ // cheap (a handful of keys) and makes the slice genuinely immutable.
92
+ schemas: new Map(schemas),
93
+ // Erased evolve: dispatch by the sequenced event's type to the matching
94
+ // fold over the decoded payload. Per the `AnyProjection.evolve` contract
95
+ // (core), a projection is only ever handed events matching its `query` —
96
+ // `composeProjections` gates each sub before dispatch — so this needs no
97
+ // second `matchesSequenced` check; the `fold === undefined` fallback is
98
+ // the honest total-function guard (evolve is typed over any DecodedEvent).
99
+ evolve: (state, decoded) => {
100
+ const fold = folds.get(decoded.sequenced.event.type);
101
+ return fold === undefined ? state : fold(state, decoded.payload);
102
+ }
103
+ };
104
+ }
105
+ });
106
+ // Explicit `never`: an empty `Map` literal gives inference nothing to read the
107
+ // requirement off, and "no defs yet" is exactly `never`, not `unknown`.
108
+ return make(new Map(), new Map());
109
+ };
110
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection.js","names":["EventType","projection","initialState","make","schemas","folds","on","def","fold","has","type","Error","nextSchemas","Map","set","schema","nextFolds","build","options","types","keys","map","t","query","items","tags","evolve","state","decoded","get","sequenced","event","undefined","payload"],"sources":["../../src/projection.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;AAmBA,SAGEA,SAAS,QAGJ,iBAAiB;AAoCxB;;;;AAIA,OAAO,MAAMC,UAAU,GAAOC,YAAe,IAAiC;EAC5E;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAMC,IAAI,GAAGA,CACXC,OAAgE,EAChEC,KAA6D,MAChC;IAC7BC,EAAE,EAAEA,CACFC,GAAuB,EACvBC,IAAiC,KACD;MAChC;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA,IAAIJ,OAAO,CAACK,GAAG,CAACF,GAAG,CAACG,IAAI,CAAC,EAAE;QACzB,MAAM,IAAIC,KAAK,CACb,yBAAyBJ,GAAG,CAACG,IAAI,mCAAmC,GAClE,gEAAgE,GAChE,gCAAgC,CACnC;MACH;MACA;MACA;MACA;MACA;MACA;MACA;MACA,MAAME,WAAW,GAAG,IAAIC,GAAG,CAGzBT,OAAO,CAAC;MACV;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACAQ,WAAW,CAACE,GAAG,CACbP,GAAG,CAACG,IAAI,EACRH,GAAG,CAACQ,MAAwD,CAC7D;MACD,MAAMC,SAAS,GAAG,IAAIH,GAAG,CAACR,KAAK,CAAC;MAChC;MACA;MACA;MACAW,SAAS,CAACF,GAAG,CAACP,GAAG,CAACG,IAAI,EAAEF,IAAyC,CAAC;MAClE,OAAOL,IAAI,CAASS,WAAW,EAAEI,SAAS,CAAC;IAC7C,CAAC;IACDC,KAAK,EAAGC,OAAO,IAAI;MACjB;MACA;MACA,MAAMC,KAAK,GAAG,CAAC,GAAGf,OAAO,CAACgB,IAAI,EAAE,CAAC,CAACC,GAAG,CAAEC,CAAC,IAAKtB,SAAS,CAACG,IAAI,CAACmB,CAAC,CAAC,CAAC;MAC/D,MAAMC,KAAK,GAAU;QACnBC,KAAK,EAAE,CAAC;UAAEL,KAAK;UAAEM,IAAI,EAAEP,OAAO,EAAEO,IAAI,IAAI;QAAE,CAAE;OAC7C;MACD,OAAO;QACLvB,YAAY;QACZqB,KAAK;QACL;QACA;QACA;QACA;QACA;QACA;QACAnB,OAAO,EAAE,IAAIS,GAAG,CAACT,OAAO,CAAC;QACzB;QACA;QACA;QACA;QACA;QACA;QACAsB,MAAM,EAAEA,CAACC,KAAK,EAAEC,OAAqB,KAAI;UACvC,MAAMpB,IAAI,GAAGH,KAAK,CAACwB,GAAG,CAACD,OAAO,CAACE,SAAS,CAACC,KAAK,CAACrB,IAAI,CAAC;UACpD,OAAOF,IAAI,KAAKwB,SAAS,GAAGL,KAAK,GAAGnB,IAAI,CAACmB,KAAU,EAAEC,OAAO,CAACK,OAAO,CAAC;QACvE;OACD;IACH;GACD,CAAC;EACF;EACA;EACA,OAAO9B,IAAI,CAAQ,IAAIU,GAAG,EAAE,EAAE,IAAIA,GAAG,EAAE,CAAC;AAC1C,CAAC","ignoreList":[]}
@@ -0,0 +1,34 @@
1
+ import { Effect } from 'effect';
2
+ import { deserialiseThenDecode } from "./deserialiseThenDecode.js";
3
+ import { UnknownEventType } from "./errors.js";
4
+ import { Serializer } from "./Serializer.js";
5
+ export const makeRegistry = defs => {
6
+ const map = new Map();
7
+ for (const def of defs) {
8
+ if (map.has(def.type)) {
9
+ throw new Error(`duplicate event type '${def.type}' in registry`);
10
+ }
11
+ map.set(def.type, def.schema);
12
+ }
13
+ return {
14
+ decode: event => Effect.gen(function* () {
15
+ const schema = map.get(event.type);
16
+ if (schema === undefined) {
17
+ return yield* Effect.fail(new UnknownEventType({
18
+ type: event.type
19
+ }));
20
+ }
21
+ const serializer = yield* Serializer;
22
+ const payload = yield* deserialiseThenDecode(serializer, schema, event.data);
23
+ return {
24
+ type: event.type,
25
+ payload
26
+ };
27
+ // The schema map erases `R` to `unknown`; the declared `R` is recovered
28
+ // from `Defs` at the type level, so the gen block's inferred requirement
29
+ // (`unknown`) is re-stated as `Serializer | R` here, which is this
30
+ // module's only cast.
31
+ })
32
+ };
33
+ };
34
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","names":["Effect","deserialiseThenDecode","UnknownEventType","Serializer","makeRegistry","defs","map","Map","def","has","type","Error","set","schema","decode","event","gen","get","undefined","fail","serializer","payload","data"],"sources":["../../src/registry.ts"],"sourcesContent":[null],"mappings":"AA2BA,SAASA,MAAM,QAAqB,QAAQ;AAC5C,SAASC,qBAAqB,QAAQ,4BAAyB;AAC/D,SAA0BC,gBAAgB,QAAQ,aAAU;AAE5D,SAASC,UAAU,QAAQ,iBAAc;AAqBzC,OAAO,MAAMC,YAAY,GACvBC,IAAU,IAC2B;EACrC,MAAMC,GAAG,GAAG,IAAIC,GAAG,EAAoD;EACvE,KAAK,MAAMC,GAAG,IAAIH,IAAI,EAAE;IACtB,IAAIC,GAAG,CAACG,GAAG,CAACD,GAAG,CAACE,IAAI,CAAC,EAAE;MACrB,MAAM,IAAIC,KAAK,CAAC,yBAAyBH,GAAG,CAACE,IAAI,eAAe,CAAC;IACnE;IACAJ,GAAG,CAACM,GAAG,CAACJ,GAAG,CAACE,IAAI,EAAEF,GAAG,CAACK,MAAM,CAAC;EAC/B;EACA,OAAO;IACLC,MAAM,EAAGC,KAAK,IACZf,MAAM,CAACgB,GAAG,CAAC,aAAS;MAClB,MAAMH,MAAM,GAAGP,GAAG,CAACW,GAAG,CAACF,KAAK,CAACL,IAAI,CAAC;MAClC,IAAIG,MAAM,KAAKK,SAAS,EAAE;QACxB,OAAO,OAAOlB,MAAM,CAACmB,IAAI,CAAC,IAAIjB,gBAAgB,CAAC;UAAEQ,IAAI,EAAEK,KAAK,CAACL;QAAI,CAAE,CAAC,CAAC;MACvE;MACA,MAAMU,UAAU,GAAG,OAAOjB,UAAU;MACpC,MAAMkB,OAAO,GAAG,OAAOpB,qBAAqB,CAC1CmB,UAAU,EACVP,MAAM,EACNE,KAAK,CAACO,IAAI,CACX;MACD,OAAO;QAAEZ,IAAI,EAAEK,KAAK,CAACL,IAAI;QAAEW;MAAO,CAAE;MACpC;MACA;MACA;MACA;IACF,CAAC;GAKJ;AACH,CAAC","ignoreList":[]}
@@ -0,0 +1,44 @@
1
+ import { DateTime, Effect, Schema } from 'effect';
2
+ import { CodecError } from "./errors.js";
3
+ import { Serializer } from "./Serializer.js";
4
+ import { Uuid } from "./Uuid.js";
5
+ /**
6
+ * Encode `payload` under `def` and stamp it into a wire `DcbEvent`, deriving its
7
+ * `tags` from `def.tagsFor(payload)` (a throwing mapper surfaces as a
8
+ * `CodecError`, not a defect). The tags are stamped exactly as the mapper
9
+ * returns them — no dedup or reordering. `type` and `tags` are cleartext
10
+ * wire-envelope fields (the query surface), so a `tagsFor` mapper may only key
11
+ * off cleartext payload fields (ADR-0006). `uuid` / `occurredAt` are
12
+ * exact-optional overrides; absent, they come from the `Uuid` service and the
13
+ * Effect clock.
14
+ */
15
+ export const toEvent = (def, payload, options) => Effect.gen(function* () {
16
+ // Derive tags first. A mapper is app code that can throw (e.g. `Tag.make`
17
+ // rejects an ill-formed `key:value`), and the codec's contract is that such
18
+ // failures are values on the `CodecError` channel, never defects (errors.ts).
19
+ // Deriving before the uuid/clock effects also means a bad tag fails without
20
+ // consuming the `Uuid` sequence.
21
+ const tags = yield* Effect.try({
22
+ try: () => def.tagsFor(payload),
23
+ catch: cause => new CodecError({
24
+ reason: 'tags',
25
+ cause
26
+ })
27
+ });
28
+ const serializer = yield* Serializer;
29
+ const encoded = yield* Schema.encode(def.schema)(payload).pipe(Effect.mapError(cause => new CodecError({
30
+ reason: 'encode',
31
+ cause
32
+ })));
33
+ const data = serializer.serialise(encoded);
34
+ const uuid = options?.uuid ?? (yield* (yield* Uuid).generate);
35
+ const occurredAt = options?.occurredAt ?? (yield* DateTime.now);
36
+ return {
37
+ type: def.type,
38
+ data,
39
+ tags,
40
+ uuid,
41
+ occurredAt
42
+ };
43
+ });
44
+ //# sourceMappingURL=toEvent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toEvent.js","names":["DateTime","Effect","Schema","CodecError","Serializer","Uuid","toEvent","def","payload","options","gen","tags","try","tagsFor","catch","cause","reason","serializer","encoded","encode","schema","pipe","mapError","data","serialise","uuid","generate","occurredAt","now","type"],"sources":["../../src/toEvent.ts"],"sourcesContent":[null],"mappings":"AAcA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,MAAM,QAAQ,QAAQ;AACjD,SAASC,UAAU,QAAQ,aAAU;AAErC,SAASC,UAAU,QAAQ,iBAAc;AACzC,SAASC,IAAI,QAAQ,WAAQ;AAE7B;;;;;;;;;;AAUA,OAAO,MAAMC,OAAO,GAAGA,CACrBC,GAAsB,EACtBC,OAAU,EACVC,OAGC,KAEDR,MAAM,CAACS,GAAG,CAAC,aAAS;EAClB;EACA;EACA;EACA;EACA;EACA,MAAMC,IAAI,GAAG,OAAOV,MAAM,CAACW,GAAG,CAAC;IAC7BA,GAAG,EAAEA,CAAA,KAAML,GAAG,CAACM,OAAO,CAACL,OAAO,CAAC;IAC/BM,KAAK,EAAGC,KAAK,IAAK,IAAIZ,UAAU,CAAC;MAAEa,MAAM,EAAE,MAAM;MAAED;IAAK,CAAE;GAC3D,CAAC;EACF,MAAME,UAAU,GAAG,OAAOb,UAAU;EACpC,MAAMc,OAAO,GAAG,OAAOhB,MAAM,CAACiB,MAAM,CAACZ,GAAG,CAACa,MAAM,CAAC,CAACZ,OAAO,CAAC,CAACa,IAAI,CAC5DpB,MAAM,CAACqB,QAAQ,CAAEP,KAAK,IAAK,IAAIZ,UAAU,CAAC;IAAEa,MAAM,EAAE,QAAQ;IAAED;EAAK,CAAE,CAAC,CAAC,CACxE;EACD,MAAMQ,IAAI,GAAGN,UAAU,CAACO,SAAS,CAACN,OAAO,CAAC;EAC1C,MAAMO,IAAI,GAAGhB,OAAO,EAAEgB,IAAI,KAAK,OAAO,CAAC,OAAOpB,IAAI,EAAEqB,QAAQ,CAAC;EAC7D,MAAMC,UAAU,GAAGlB,OAAO,EAAEkB,UAAU,KAAK,OAAO3B,QAAQ,CAAC4B,GAAG,CAAC;EAC/D,OAAO;IACLC,IAAI,EAAEtB,GAAG,CAACsB,IAAI;IACdN,IAAI;IACJZ,IAAI;IACJc,IAAI;IACJE;GACD;AACH,CAAC,CAAC","ignoreList":[]}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@kairos-es/codec",
3
+ "version": "0.0.0",
4
+ "description": "Schema-first codec with encryption seam for kairos-es",
5
+ "license": "BSD-3-Clause",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/neverbland/kairos-es.git",
9
+ "directory": "packages/codec"
10
+ },
11
+ "sideEffects": [],
12
+ "homepage": "https://github.com/neverbland/kairos-es",
13
+ "peerDependencies": {
14
+ "effect": "^3.22.0",
15
+ "@kairos-es/core": "^0.0.0"
16
+ },
17
+ "main": "./dist/cjs/index.js",
18
+ "module": "./dist/esm/index.js",
19
+ "types": "./dist/dts/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/dts/index.d.ts",
23
+ "import": "./dist/esm/index.js",
24
+ "default": "./dist/cjs/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "typesVersions": {
29
+ "*": {}
30
+ }
31
+ }