@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.
- package/LICENSE +28 -0
- package/README.md +46 -0
- package/dist/cjs/Serializer.js +34 -0
- package/dist/cjs/Serializer.js.map +1 -0
- package/dist/cjs/Uuid.js +41 -0
- package/dist/cjs/Uuid.js.map +1 -0
- package/dist/cjs/decider.js +152 -0
- package/dist/cjs/decider.js.map +1 -0
- package/dist/cjs/decisionModel.js +155 -0
- package/dist/cjs/decisionModel.js.map +1 -0
- package/dist/cjs/decode.js +170 -0
- package/dist/cjs/decode.js.map +1 -0
- package/dist/cjs/deserialiseThenDecode.js +20 -0
- package/dist/cjs/deserialiseThenDecode.js.map +1 -0
- package/dist/cjs/errors.js +94 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/event.js +26 -0
- package/dist/cjs/event.js.map +1 -0
- package/dist/cjs/index.js +112 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/projection.js +118 -0
- package/dist/cjs/projection.js.map +1 -0
- package/dist/cjs/registry.js +41 -0
- package/dist/cjs/registry.js.map +1 -0
- package/dist/cjs/toEvent.js +51 -0
- package/dist/cjs/toEvent.js.map +1 -0
- package/dist/dts/Serializer.d.ts +27 -0
- package/dist/dts/Serializer.d.ts.map +1 -0
- package/dist/dts/Uuid.d.ts +19 -0
- package/dist/dts/Uuid.d.ts.map +1 -0
- package/dist/dts/decider.d.ts +157 -0
- package/dist/dts/decider.d.ts.map +1 -0
- package/dist/dts/decisionModel.d.ts +129 -0
- package/dist/dts/decisionModel.d.ts.map +1 -0
- package/dist/dts/decode.d.ts +92 -0
- package/dist/dts/decode.d.ts.map +1 -0
- package/dist/dts/deserialiseThenDecode.d.ts +62 -0
- package/dist/dts/deserialiseThenDecode.d.ts.map +1 -0
- package/dist/dts/errors.d.ts +79 -0
- package/dist/dts/errors.d.ts.map +1 -0
- package/dist/dts/event.d.ts +71 -0
- package/dist/dts/event.d.ts.map +1 -0
- package/dist/dts/index.d.ts +63 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/projection.d.ts +56 -0
- package/dist/dts/projection.d.ts.map +1 -0
- package/dist/dts/registry.d.ts +46 -0
- package/dist/dts/registry.d.ts.map +1 -0
- package/dist/dts/toEvent.d.ts +34 -0
- package/dist/dts/toEvent.d.ts.map +1 -0
- package/dist/esm/Serializer.js +26 -0
- package/dist/esm/Serializer.js.map +1 -0
- package/dist/esm/Uuid.js +33 -0
- package/dist/esm/Uuid.js.map +1 -0
- package/dist/esm/decider.js +143 -0
- package/dist/esm/decider.js.map +1 -0
- package/dist/esm/decisionModel.js +146 -0
- package/dist/esm/decisionModel.js.map +1 -0
- package/dist/esm/decode.js +163 -0
- package/dist/esm/decode.js.map +1 -0
- package/dist/esm/deserialiseThenDecode.js +13 -0
- package/dist/esm/deserialiseThenDecode.js.map +1 -0
- package/dist/esm/errors.js +85 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/event.js +19 -0
- package/dist/esm/event.js.map +1 -0
- package/dist/esm/index.js +63 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/projection.js +110 -0
- package/dist/esm/projection.js.map +1 -0
- package/dist/esm/registry.js +34 -0
- package/dist/esm/registry.js.map +1 -0
- package/dist/esm/toEvent.js +44 -0
- package/dist/esm/toEvent.js.map +1 -0
- package/package.json +31 -0
- package/src/Serializer.ts +37 -0
- package/src/Uuid.ts +36 -0
- package/src/decider.ts +219 -0
- package/src/decisionModel.ts +182 -0
- package/src/decode.ts +256 -0
- package/src/deserialiseThenDecode.ts +76 -0
- package/src/errors.ts +91 -0
- package/src/event.ts +90 -0
- package/src/index.ts +71 -0
- package/src/projection.ts +163 -0
- package/src/registry.ts +87 -0
- 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,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
|
+
}
|