@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
|
+
/**
|
|
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 {
|
|
21
|
+
type AnyProjection,
|
|
22
|
+
type DecodedEvent,
|
|
23
|
+
EventType,
|
|
24
|
+
type Query,
|
|
25
|
+
type Tag,
|
|
26
|
+
} from '@kairos-es/core'
|
|
27
|
+
import type { Schema } from 'effect'
|
|
28
|
+
import type { EventDef } from './event'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A schema-backed, directly-composable projection: an `AnyProjection<S>` plus
|
|
32
|
+
* the `type -> Schema` map for decode. The map's value type carries the union
|
|
33
|
+
* `R` of its defs' requirements covariantly, so `RequirementOf` recovers `R`
|
|
34
|
+
* (and the `out R` annotation keeps union-widening sound) with no phantom marker.
|
|
35
|
+
*/
|
|
36
|
+
export interface SliceProjection<out S = unknown, out R = unknown>
|
|
37
|
+
extends AnyProjection<S> {
|
|
38
|
+
readonly schemas: ReadonlyMap<string, Schema.Schema<unknown, unknown, R>>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The fluent builder: accumulate `.on(def, fold)` steps, then `.build()`. */
|
|
42
|
+
export interface ProjectionBuilder<S, R = never> {
|
|
43
|
+
/**
|
|
44
|
+
* Fold events of `def`'s type: `fold` receives the decoded, typed `payload`.
|
|
45
|
+
* Adds `def`'s requirement `R2` to the accumulated `R`.
|
|
46
|
+
*
|
|
47
|
+
* Throws if this chain has already `.on`-ed that event type — see the guard in
|
|
48
|
+
* `projection` for why a repeat is always a mistake here, where across two
|
|
49
|
+
* slices it need not be.
|
|
50
|
+
*/
|
|
51
|
+
on<A, I, R2>(
|
|
52
|
+
def: EventDef<A, I, R2>,
|
|
53
|
+
fold: (state: S, payload: A) => S,
|
|
54
|
+
): ProjectionBuilder<S, R | R2>
|
|
55
|
+
/**
|
|
56
|
+
* Materialise the slice. The query is a single item over the `.on`-ed defs'
|
|
57
|
+
* types (branded via `EventType.make`) and `options.tags` (default none).
|
|
58
|
+
*/
|
|
59
|
+
build(options?: { readonly tags?: ReadonlyArray<Tag> }): SliceProjection<S, R>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Start a slice for `initialState`. Returns a builder with no defs yet
|
|
64
|
+
* (`R = never`); each `.on(def, fold)` widens `R` by the def's requirement.
|
|
65
|
+
*/
|
|
66
|
+
export const projection = <S>(initialState: S): ProjectionBuilder<S, never> => {
|
|
67
|
+
// Persistent builder: `make` closes over the accumulated maps, and each
|
|
68
|
+
// `.on` copies them into a fresh builder rather than mutating in place, so
|
|
69
|
+
// no two derived builders ever share mutable state (reusing an intermediate
|
|
70
|
+
// builder can't cross-contaminate slices).
|
|
71
|
+
//
|
|
72
|
+
// `make` is generic in the accumulated requirement `R` — the parameter the
|
|
73
|
+
// builder is *about*. Pinning it to `never` and casting the result back would
|
|
74
|
+
// make the recursion typecheck by discarding the very thing each `.on` adds;
|
|
75
|
+
// threading `R` means the implementation states the same widening the
|
|
76
|
+
// `ProjectionBuilder` interface promises, and `on` returns without a cast.
|
|
77
|
+
const make = <R>(
|
|
78
|
+
schemas: ReadonlyMap<string, Schema.Schema<unknown, unknown, R>>,
|
|
79
|
+
folds: ReadonlyMap<string, (state: S, payload: unknown) => S>,
|
|
80
|
+
): ProjectionBuilder<S, R> => ({
|
|
81
|
+
on: <A, I, R2>(
|
|
82
|
+
def: EventDef<A, I, R2>,
|
|
83
|
+
fold: (state: S, payload: A) => S,
|
|
84
|
+
): ProjectionBuilder<S, R | R2> => {
|
|
85
|
+
// A repeated type WITHIN one chain is unambiguously a mistake, so this is
|
|
86
|
+
// a plain `has` rather than the reference-equality rule `mergeSliceSchemas`
|
|
87
|
+
// applies ACROSS slices. There, two entries for one type are the intended
|
|
88
|
+
// way to feed one event into two views, and the schemas being one object is
|
|
89
|
+
// what makes the collision harmless. Here there is only ever one fold per
|
|
90
|
+
// type — `evolve` dispatches by type into a single map — so a second `.on`
|
|
91
|
+
// cannot mean "as well as", only "instead of", and it silently discards
|
|
92
|
+
// the author's first fold along with its schema. The asymmetry is the
|
|
93
|
+
// point: the same `has` up there would outlaw the legitimate case.
|
|
94
|
+
if (schemas.has(def.type)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`duplicate event type '${def.type}' in slice: .on was called twice ` +
|
|
97
|
+
'for it, and the second fold would silently replace the first. ' +
|
|
98
|
+
'Handle both cases in one fold.',
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
// The entry type is annotated `R | R2` rather than inferred, and that
|
|
102
|
+
// annotation is what carries the requirement forward — `build` reads `R`
|
|
103
|
+
// straight off this map, so `RequirementOf` ultimately reads it from here.
|
|
104
|
+
// Widening the copied `R` entries into it costs nothing: `Schema` is
|
|
105
|
+
// declared `<in out A, in out I, out R>` upstream, hence covariant in its
|
|
106
|
+
// requirement.
|
|
107
|
+
const nextSchemas = new Map<
|
|
108
|
+
string,
|
|
109
|
+
Schema.Schema<unknown, unknown, R | R2>
|
|
110
|
+
>(schemas)
|
|
111
|
+
// Erasing `A`/`I` is the one unavoidable cast, and `as unknown as` is what
|
|
112
|
+
// it takes: `Schema` is INVARIANT in both, so `Schema<A, I, R2>` and
|
|
113
|
+
// `Schema<unknown, unknown, R2>` are unrelated in either direction. It is
|
|
114
|
+
// sound because the payload is only ever read back through `evolve`, which
|
|
115
|
+
// re-narrows by event type via the matching `fold`. The target names `R2`
|
|
116
|
+
// and not `never` because `R2` is what the def actually requires — an
|
|
117
|
+
// `Encrypted` field's `CipherKey` is not nothing — so the cast erases only
|
|
118
|
+
// what it has to and states the rest truthfully.
|
|
119
|
+
nextSchemas.set(
|
|
120
|
+
def.type,
|
|
121
|
+
def.schema as unknown as Schema.Schema<unknown, unknown, R2>,
|
|
122
|
+
)
|
|
123
|
+
const nextFolds = new Map(folds)
|
|
124
|
+
// Contravariance in the payload parameter, so this one is a cast too: the
|
|
125
|
+
// map holds folds over `unknown`, and `evolve` supplies the payload the
|
|
126
|
+
// matching def decoded, which is the `A` this fold was written against.
|
|
127
|
+
nextFolds.set(def.type, fold as (state: S, payload: unknown) => S)
|
|
128
|
+
return make<R | R2>(nextSchemas, nextFolds)
|
|
129
|
+
},
|
|
130
|
+
build: (options) => {
|
|
131
|
+
// Internal-layer Query assembly from plain data (see core's Projection.ts
|
|
132
|
+
// note): a type-only-plus-optional-tags item the public surface withholds.
|
|
133
|
+
const types = [...schemas.keys()].map((t) => EventType.make(t))
|
|
134
|
+
const query: Query = {
|
|
135
|
+
items: [{ types, tags: options?.tags ?? [] }],
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
initialState,
|
|
139
|
+
query,
|
|
140
|
+
// Defensive copy: hand out a fresh Map, not the builder's own. `schemas`
|
|
141
|
+
// is typed `ReadonlyMap`, but that is a compile-time guard only — a
|
|
142
|
+
// caller who casts it back to `Map` could otherwise mutate the slice's
|
|
143
|
+
// live decode table after `build`, desyncing it from `query` (whose
|
|
144
|
+
// event-types were snapshotted from these keys just above). The copy is
|
|
145
|
+
// cheap (a handful of keys) and makes the slice genuinely immutable.
|
|
146
|
+
schemas: new Map(schemas),
|
|
147
|
+
// Erased evolve: dispatch by the sequenced event's type to the matching
|
|
148
|
+
// fold over the decoded payload. Per the `AnyProjection.evolve` contract
|
|
149
|
+
// (core), a projection is only ever handed events matching its `query` —
|
|
150
|
+
// `composeProjections` gates each sub before dispatch — so this needs no
|
|
151
|
+
// second `matchesSequenced` check; the `fold === undefined` fallback is
|
|
152
|
+
// the honest total-function guard (evolve is typed over any DecodedEvent).
|
|
153
|
+
evolve: (state, decoded: DecodedEvent) => {
|
|
154
|
+
const fold = folds.get(decoded.sequenced.event.type)
|
|
155
|
+
return fold === undefined ? state : fold(state as S, decoded.payload)
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
// Explicit `never`: an empty `Map` literal gives inference nothing to read the
|
|
161
|
+
// requirement off, and "no defs yet" is exactly `never`, not `unknown`.
|
|
162
|
+
return make<never>(new Map(), new Map())
|
|
163
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The general (registry) path (minimal): decode an arbitrary wire event by its
|
|
3
|
+
* runtime type string. This is the one place a type outside the known set
|
|
4
|
+
* surfaces as a typed `UnknownEventType` — the home of ADR-0005 decode-totality.
|
|
5
|
+
*
|
|
6
|
+
* `makeRegistry(defs)` builds a `type -> Schema` map with a construction-time
|
|
7
|
+
* uniqueness guard (a duplicate type throws at wiring, not at runtime). Its
|
|
8
|
+
* requirement `R` is the union of the defs' requirements. `decode` looks the
|
|
9
|
+
* type up, then runs `deserialiseThenDecode` over the bytes, yielding
|
|
10
|
+
* `{ type, payload: unknown }`. That `unknown` is the point of the path rather
|
|
11
|
+
* than a gap in it: the type is discovered at RUNTIME off the wire event, so the
|
|
12
|
+
* caller has named no def and the return is deliberately NOT a discriminated
|
|
13
|
+
* union over `Defs`. What this path owes its caller is decode TOTALITY — every
|
|
14
|
+
* known type decodes, an unknown one fails as `UnknownEventType` — and not typed
|
|
15
|
+
* access. A caller who wants a typed payload names the def it expects and takes
|
|
16
|
+
* the typed slice path (`projection(init).on(def, fold)`), where each fold
|
|
17
|
+
* receives its payload already decoded and narrowed.
|
|
18
|
+
*
|
|
19
|
+
* What is this path's OWN and what it merely shares. The lookup is its own: a
|
|
20
|
+
* miss here is `UnknownEventType`, where the same miss on the typed path is a
|
|
21
|
+
* `CodecError` about a schema the caller did not supply — the asymmetry ADR-0005
|
|
22
|
+
* exists for. Everything after the lookup is the two-step both paths perform, so
|
|
23
|
+
* it is `deserialiseThenDecode` that owns it and fixes the `deserialise` /
|
|
24
|
+
* `decode` `reason` strings; this module holding a second copy is exactly how one
|
|
25
|
+
* of them would come to be reworded on one path only.
|
|
26
|
+
*/
|
|
27
|
+
import type { DcbEvent } from '@kairos-es/core'
|
|
28
|
+
import { Effect, type Schema } from 'effect'
|
|
29
|
+
import { deserialiseThenDecode } from './deserialiseThenDecode'
|
|
30
|
+
import { type CodecError, UnknownEventType } from './errors'
|
|
31
|
+
import type { EventDef, EventDefAny } from './event'
|
|
32
|
+
import { Serializer } from './Serializer'
|
|
33
|
+
|
|
34
|
+
/** A registry decoding arbitrary wire events; `R` is the union of def requirements. */
|
|
35
|
+
export interface Registry<R> {
|
|
36
|
+
readonly decode: (
|
|
37
|
+
event: DcbEvent,
|
|
38
|
+
) => Effect.Effect<
|
|
39
|
+
{ readonly type: string; readonly payload: unknown },
|
|
40
|
+
UnknownEventType | CodecError,
|
|
41
|
+
Serializer | R
|
|
42
|
+
>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a `Registry` from a def barrel. Throws at construction on a duplicate
|
|
47
|
+
* type; `R` is the union of the defs' requirements.
|
|
48
|
+
*/
|
|
49
|
+
type RequirementOfDefs<Defs extends ReadonlyArray<EventDefAny>> =
|
|
50
|
+
// biome-ignore lint/suspicious/noExplicitAny: match the heterogeneous EventDefAny bound to infer the union of requirements R; A/I are irrelevant here.
|
|
51
|
+
Defs[number] extends EventDef<any, any, infer R> ? R : never
|
|
52
|
+
|
|
53
|
+
export const makeRegistry = <Defs extends ReadonlyArray<EventDefAny>>(
|
|
54
|
+
defs: Defs,
|
|
55
|
+
): Registry<RequirementOfDefs<Defs>> => {
|
|
56
|
+
const map = new Map<string, Schema.Schema<unknown, unknown, unknown>>()
|
|
57
|
+
for (const def of defs) {
|
|
58
|
+
if (map.has(def.type)) {
|
|
59
|
+
throw new Error(`duplicate event type '${def.type}' in registry`)
|
|
60
|
+
}
|
|
61
|
+
map.set(def.type, def.schema)
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
decode: (event) =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const schema = map.get(event.type)
|
|
67
|
+
if (schema === undefined) {
|
|
68
|
+
return yield* Effect.fail(new UnknownEventType({ type: event.type }))
|
|
69
|
+
}
|
|
70
|
+
const serializer = yield* Serializer
|
|
71
|
+
const payload = yield* deserialiseThenDecode(
|
|
72
|
+
serializer,
|
|
73
|
+
schema,
|
|
74
|
+
event.data,
|
|
75
|
+
)
|
|
76
|
+
return { type: event.type, payload }
|
|
77
|
+
// The schema map erases `R` to `unknown`; the declared `R` is recovered
|
|
78
|
+
// from `Defs` at the type level, so the gen block's inferred requirement
|
|
79
|
+
// (`unknown`) is re-stated as `Serializer | R` here, which is this
|
|
80
|
+
// module's only cast.
|
|
81
|
+
}) as Effect.Effect<
|
|
82
|
+
{ readonly type: string; readonly payload: unknown },
|
|
83
|
+
UnknownEventType | CodecError,
|
|
84
|
+
Serializer | RequirementOfDefs<Defs>
|
|
85
|
+
>,
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/toEvent.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event production (codec edge-helper): turn a typed payload into a wire
|
|
3
|
+
* `DcbEvent`. Encode is `Schema.encode(def.schema)` (a `ParseError` becomes a
|
|
4
|
+
* `CodecError`; any encryption transform runs here, *before* serialisation)
|
|
5
|
+
* then `serialiser.serialise`. The wire `tags` are derived from the def's
|
|
6
|
+
* required `tagsFor` mapper (ADR-0006) — they are never hand-passed, so two
|
|
7
|
+
* command sites emitting the same event type cannot tag it differently. A mapper
|
|
8
|
+
* that throws (e.g. an ill-formed `key:value` rejected by `Tag.make`) is
|
|
9
|
+
* surfaced on the `CodecError` channel, not as a defect. `uuid`
|
|
10
|
+
* comes from `options.uuid` or the `Uuid` service; `occurredAt` from
|
|
11
|
+
* `options.occurredAt` or the Effect clock (`DateTime.now`), keeping `core`
|
|
12
|
+
* clock- and RNG-free while staying overridable for backdated imports.
|
|
13
|
+
*/
|
|
14
|
+
import type { DcbEvent } from '@kairos-es/core'
|
|
15
|
+
import { DateTime, Effect, Schema } from 'effect'
|
|
16
|
+
import { CodecError } from './errors'
|
|
17
|
+
import type { EventDef } from './event'
|
|
18
|
+
import { Serializer } from './Serializer'
|
|
19
|
+
import { Uuid } from './Uuid'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Encode `payload` under `def` and stamp it into a wire `DcbEvent`, deriving its
|
|
23
|
+
* `tags` from `def.tagsFor(payload)` (a throwing mapper surfaces as a
|
|
24
|
+
* `CodecError`, not a defect). The tags are stamped exactly as the mapper
|
|
25
|
+
* returns them — no dedup or reordering. `type` and `tags` are cleartext
|
|
26
|
+
* wire-envelope fields (the query surface), so a `tagsFor` mapper may only key
|
|
27
|
+
* off cleartext payload fields (ADR-0006). `uuid` / `occurredAt` are
|
|
28
|
+
* exact-optional overrides; absent, they come from the `Uuid` service and the
|
|
29
|
+
* Effect clock.
|
|
30
|
+
*/
|
|
31
|
+
export const toEvent = <A, I, R>(
|
|
32
|
+
def: EventDef<A, I, R>,
|
|
33
|
+
payload: A,
|
|
34
|
+
options?: {
|
|
35
|
+
readonly uuid?: string
|
|
36
|
+
readonly occurredAt?: DateTime.Utc
|
|
37
|
+
},
|
|
38
|
+
): Effect.Effect<DcbEvent, CodecError, Serializer | Uuid | R> =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
// Derive tags first. A mapper is app code that can throw (e.g. `Tag.make`
|
|
41
|
+
// rejects an ill-formed `key:value`), and the codec's contract is that such
|
|
42
|
+
// failures are values on the `CodecError` channel, never defects (errors.ts).
|
|
43
|
+
// Deriving before the uuid/clock effects also means a bad tag fails without
|
|
44
|
+
// consuming the `Uuid` sequence.
|
|
45
|
+
const tags = yield* Effect.try({
|
|
46
|
+
try: () => def.tagsFor(payload),
|
|
47
|
+
catch: (cause) => new CodecError({ reason: 'tags', cause }),
|
|
48
|
+
})
|
|
49
|
+
const serializer = yield* Serializer
|
|
50
|
+
const encoded = yield* Schema.encode(def.schema)(payload).pipe(
|
|
51
|
+
Effect.mapError((cause) => new CodecError({ reason: 'encode', cause })),
|
|
52
|
+
)
|
|
53
|
+
const data = serializer.serialise(encoded)
|
|
54
|
+
const uuid = options?.uuid ?? (yield* (yield* Uuid).generate)
|
|
55
|
+
const occurredAt = options?.occurredAt ?? (yield* DateTime.now)
|
|
56
|
+
return {
|
|
57
|
+
type: def.type,
|
|
58
|
+
data,
|
|
59
|
+
tags,
|
|
60
|
+
uuid,
|
|
61
|
+
occurredAt,
|
|
62
|
+
}
|
|
63
|
+
})
|