@kairos-es/codec 1.0.0-alpha.1 → 1.0.0-alpha.3

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision.d.ts","sourceRoot":"","sources":["../../src/decision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,KAAK,KAAK,IAAI,GAAG,EAAmB,MAAM,QAAQ,CAAA;AAE3D;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,MAEpC,CAAA;AAED,qCAAqC;AACrC,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAA;;;;AAEpD;;;;;;;;;;GAUG;AACH,qBAAa,SAAU,SAAQ,eAA8B;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB,CAAC;IACA,QAAQ,CAAC,CAAC,eAAe,CAAC,EAAE,eAAe,CAAkB;CAC9D;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,GAAG,OAAO,KAAG,CAAC,IAAI,SACH,CAAA;AAE3C;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE;IACrB,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,SAAS,CAAA;CAGnD,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAA;AAE5E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAC3C,QAAQ,GACR,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACjB,QAAQ,GACR,CAAC,SAAS,SAAS,GACjB,SAAS,GACT,QAAQ,CAAA"}
@@ -34,6 +34,17 @@
34
34
  * error union are written once, in the two function bodies, rather than restated
35
35
  * as an annotation above them.
36
36
  *
37
+ * A decision may also answer `Unchanged` — "already done, nothing to append" —
38
+ * built with `Decision.unchanged(reason?)` and recognised ONLY by
39
+ * `isUnchanged`, by its `UnchangedTypeId`. It is a success, not an error and not
40
+ * an empty batch: every runner returns it in place of a `Position` and appends
41
+ * nothing. `Decider`'s fifth type parameter carries the widening and defaults to
42
+ * the non-empty batch, and each runner's result is `DecisionResult<O>`, so a
43
+ * decider or body that cannot answer `Unchanged` keeps a plain `Position` — an
44
+ * untyped one and one generic in its success type included — and its caller is
45
+ * never asked to handle it. `decision.ts` owns the argument, and the one type
46
+ * query that does read differently.
47
+ *
37
48
  * `transact(precondition)(body)` is the layered write path, for a decision whose
38
49
  * second query cannot be written until an earlier read has been folded. It opens
39
50
  * an ambient read ledger, evaluates the precondition inside it, hands that
@@ -54,6 +65,12 @@
54
65
  * | `runDecider` | ✅ the ordinary single-hop case | ⛔ a DEFECT, refused at the store |
55
66
  * | `decideOnly` | ⛔ unrunnable, and `tsc` says so | ✅ the layered case |
56
67
  *
68
+ * Both carry `Unchanged` the same way, which is what keeps them one pair:
69
+ * `runDecider` returns it having appended nothing, and `decideOnly` passes it to
70
+ * the body unaltered, where `transact` returns it having appended nothing —
71
+ * and, having no write to guard, without refusing an empty bracket as
72
+ * `NoReadsRecorded`.
73
+ *
57
74
  * ⚠ *Unrunnable* rather than *unwritable*, and the difference is measured. A bare
58
75
  * `decideOnly(d, cmd)` outside a bracket COMPILES, and so does an exported helper
59
76
  * over it: its `ReadLedger` requirement simply propagates, as any unmet requirement
@@ -100,6 +117,7 @@
100
117
  * record other than the one the query came from (see `decode.ts`).
101
118
  */
102
119
  export { type Decider, decider, runDecider } from './decider.js';
120
+ export { Decision, type DecisionOutput, type DecisionResult, isUnchanged, Unchanged, UnchangedTypeId, } from './decision.js';
103
121
  export { buildDecisionModel, modelAtHead, type RequirementOf, } from './decisionModel.js';
104
122
  export { decodeSlices } from './decode.js';
105
123
  export { CodecError, UnknownEventType } from './errors.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoGG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AAC9E,OAAO,EACL,UAAU,EACV,KAAK,YAAY,EACjB,QAAQ,EACR,YAAY,GACb,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,iBAAiB,EACtB,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,GAAG,EACR,eAAe,EACf,UAAU,EACV,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,YAAY,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqHG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC7D,OAAO,EACL,QAAQ,EACR,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,WAAW,EACX,SAAS,EACT,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AAC9E,OAAO,EACL,UAAU,EACV,KAAK,YAAY,EACjB,QAAQ,EACR,YAAY,GACb,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,iBAAiB,EACtB,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,GAAG,EACR,eAAe,EACf,UAAU,EACV,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,YAAY,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA"}
@@ -24,6 +24,21 @@
24
24
  * ⚠ The name says *declare, then do*. It is NOT a database transaction: an append
25
25
  * under a condition is not two-phase, and nothing rolls back.
26
26
  *
27
+ * ## A body may answer `Unchanged`, and then there is nothing to guard
28
+ *
29
+ * The body's success is a `DecisionOutput`: a non-empty batch, or `Unchanged` —
30
+ * "already done". On `Unchanged` the bracket appends nothing, derives no
31
+ * condition and returns the `Unchanged`, and the result type is
32
+ * `DecisionResult<O>`, so a body that cannot answer it keeps a plain `Position`.
33
+ * `decideOnly` passes a decider's `Unchanged` through to the body unaltered.
34
+ *
35
+ * It is NOT refused as `NoReadsRecorded`, even from a bracket that read nothing,
36
+ * and that is not a hole in the refusal. `NoReadsRecorded` exists because an
37
+ * append with no prior read would go out UNGUARDED while looking guarded. An
38
+ * `Unchanged` appends nothing, so there is no write for a condition to guard and
39
+ * nothing for the refusal to protect. The same empty bracket returning EVENTS is
40
+ * still refused, exactly as before. `decision.ts` owns what `Unchanged` is.
41
+ *
27
42
  * ## Order is the design, and it is the one line here that must not move
28
43
  *
29
44
  * The precondition is evaluated INSIDE the bracket and never before it. Evaluated
@@ -91,6 +106,7 @@ import { AppendIsOwned, DcbEventStore } from '@kairos-es/core';
91
106
  import type { Array as Arr } from 'effect';
92
107
  import { Context, Effect } from 'effect';
93
108
  import type { Decider } from './decider.js';
109
+ import { type DecisionOutput, type DecisionResult } from './decision.js';
94
110
  import { type RequirementOf } from './decisionModel.js';
95
111
  import type { CodecError } from './errors.js';
96
112
  import type { SliceProjection } from './projection.js';
@@ -126,7 +142,8 @@ export interface Precondition<Id, Service, E, R> {
126
142
  }
127
143
  /**
128
144
  * The DCB command cycle MINUS the append: read the decision model, decode, fold,
129
- * decide, and RETURN the events for the bracket to append.
145
+ * decide, and RETURN the events for the bracket to append — or the decider's
146
+ * `Unchanged`, passed through as it came, for the bracket to append nothing.
130
147
  *
131
148
  * Bracket-only, and the refusal is STATIC — see the section below for the mechanism
132
149
  * and, more importantly, for where the error actually lands, which is not where a
@@ -176,7 +193,7 @@ export interface Precondition<Id, Service, E, R> {
176
193
  * says why the second failure means there should be no third attempt. Plant the
177
194
  * mutation instead.
178
195
  */
179
- export declare const decideOnly: <P extends Record<string, SliceProjection>, Command, E, R>(decider: Decider<P, Command, E, R>, command: Command) => Effect.Effect<Arr.NonEmptyReadonlyArray<DcbEvent>, E | CodecError | TaglessSliceInLedger, ReadLedger | DcbEventStore | Serializer | Uuid | R | RequirementOf<P>>;
196
+ export declare const decideOnly: <P extends Record<string, SliceProjection>, Command, E, R, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>>(decider: Decider<P, Command, E, R, O>, command: Command) => Effect.Effect<O, E | CodecError | TaglessSliceInLedger, ReadLedger | DcbEventStore | Serializer | Uuid | R | RequirementOf<P>>;
180
197
  /**
181
198
  * The sanctioned exception to the nesting refusal: append these events under NO
182
199
  * condition, and say in the identifier and in a required `reason` why that is
@@ -216,6 +233,12 @@ export declare const unsafeAppend: (events: Arr.NonEmptyReadonlyArray<DcbEvent>,
216
233
  * appends without opening one, and its events usually match the union the bracket
217
234
  * is about to guard on, so the bracket's own close would be refused by a condition
218
235
  * its own body invalidated.
236
+ *
237
+ * The body may succeed with `Unchanged` (see the module documentation), and the
238
+ * result is then `DecisionResult<O>`: `Position` for a body that cannot answer
239
+ * it, `Position | Unchanged` for one that can. `O` is last and defaulted, so an
240
+ * explicit `<E, R>` written against the narrower signature still means what it
241
+ * did.
219
242
  */
220
- export declare const transact: <Id, Service, PE, PR>(precondition: Precondition<Id, Service, PE, PR>) => <E, R>(body: Effect.Effect<Arr.NonEmptyReadonlyArray<DcbEvent>, E, R>) => Effect.Effect<Position, E | PE | NoReadsRecorded | AppendConditionFailed, Exclude<Exclude<R, Id>, ReadLedger | AppendIsOwned> | Exclude<PR, ReadLedger | AppendIsOwned> | DcbEventStore>;
243
+ export declare const transact: <Id, Service, PE, PR>(precondition: Precondition<Id, Service, PE, PR>) => <E, R, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>>(body: Effect.Effect<O, E, R>) => Effect.Effect<DecisionResult<O>, E | PE | NoReadsRecorded | AppendConditionFailed, Exclude<Exclude<R, Id>, ReadLedger | AppendIsOwned> | Exclude<PR, ReadLedger | AppendIsOwned> | DcbEventStore>;
221
244
  //# sourceMappingURL=precondition.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"precondition.d.ts","sourceRoot":"","sources":["../../src/precondition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAS,MAAM,QAAQ,CAAA;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAGL,eAAe,EACf,UAAU,EACV,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAElC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,UAAU,GACrB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,CAAC,EACD,CAAC,EAED,SAAS,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAClC,SAAS,OAAO,KACf,MAAM,CAAC,MAAM,CACd,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EACnC,CAAC,GAAG,UAAU,GAAG,oBAAoB,EACrC,UAAU,GAAG,aAAa,GAAG,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAOnE,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,YAAY,GACvB,QAAQ,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAc3C,QAAQ,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,qBAAqB,EAAE,aAAa,CAe5D,CAAA;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,QAAQ,GAClB,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,cAAc,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,MACpE,CAAC,EAAE,CAAC,EACH,MAAM,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC7D,MAAM,CAAC,MAAM,CACd,QAAQ,EACR,CAAC,GAAG,EAAE,GAAG,eAAe,GAAG,qBAAqB,EAC9C,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,aAAa,CAAC,GACnD,OAAO,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,CAAC,GACvC,aAAa,CA6Cb,CAAA"}
1
+ {"version":3,"file":"precondition.d.ts","sourceRoot":"","sources":["../../src/precondition.ts"],"names":[],"mappings":"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"}
@@ -52,76 +52,37 @@
52
52
  * boundary, and decode may fail with `CodecError`. All surface on the error
53
53
  * channel, so a caller handles all three the same way — and
54
54
  * `@kairos-es/testing`'s `.thenError(tag)` can assert any of them.
55
+ *
56
+ * `decide` may also answer `Unchanged` — "already done, nothing to append" — but
57
+ * only where its type says so: `Decider`'s fifth type parameter `O` defaults to
58
+ * the non-empty batch, and `runDecider` returns `DecisionResult<O>`, which is
59
+ * plain `Position` unless `O` admits `Unchanged`. So a decider that cannot say
60
+ * "already done" keeps exactly the result type it always had — an untyped one
61
+ * and one built in a wrapper generic in its success type included — and one
62
+ * that can makes its caller handle `Position | Unchanged`. `runDecider` returns an
63
+ * `Unchanged` without calling `append`, so `append`'s refusal of an empty batch
64
+ * is untouched and never met. `decision.ts` owns why it is a success rather than
65
+ * an error or an empty batch, and why it is recognised by a type id.
55
66
  */
56
67
  import { DcbEventStore } from '@kairos-es/core';
57
68
  import { Effect } from 'effect';
69
+ import { isUnchanged } from "./decision.js";
58
70
  import { buildDecisionModel } from "./decisionModel.js";
59
- /**
60
- * Build a `Decider` with all four of its type arguments INFERRED: the slice
61
- * record `P` from what `projections` returns, the error union `E` and the extra
62
- * requirement `R` from what `decide` returns. The command type is the one thing
63
- * a caller still writes, once, as the annotation on the `projections` parameter
64
- * — `Command` occurs only in parameter positions, so nothing else could supply
65
- * it, and it is domain information (the command this decision is parametrised
66
- * by) rather than plumbing.
67
- *
68
- * It adds no capability: the interface stays directly inhabitable and this
69
- * returns exactly the object literal a caller would have written. What it
70
- * changes is what an author must SPELL. An object literal annotated
71
- * `Decider<P, Command, E>` has to state the whole slice record — a
72
- * `ReturnType<typeof someProjection>` per key, a slice's state type being
73
- * inferred by `projection().on().build()` and so having no name to write — plus
74
- * the union of every error its decide branches can fail with. Dropping the
75
- * annotation instead is not open to the author: without it `decide`'s `state`
76
- * and `command` parameters have no contextual type and are implicitly `any`. So
77
- * the cost landed hardest on the worked example, where some thirty lines of type
78
- * plumbing sat between the reader and the three decisions that file exists to
79
- * teach.
80
- *
81
- * `Eff` captures decide's WHOLE return type as one type parameter instead of
82
- * inferring `E` and `R` from an `Effect<…, E, Serializer | Uuid | R>` shaped
83
- * target, and that is the part that had to be got right rather than merely
84
- * written down. A real `decide` returns a UNION of effects — one `Effect.fail`
85
- * per rule it enforces, plus the produce path — and TypeScript infers a type
86
- * parameter from a union source by choosing one constituent's candidate, not by
87
- * unioning them. Against the shaped target `E` therefore came out as whichever
88
- * branch won, and every other branch was then rejected against it. Inference to
89
- * a NAKED type variable takes the union whole, which is why `Eff` is the
90
- * parameter and `Effect.Effect.Error` / `Effect.Effect.Context` recover the two
91
- * channels afterwards: those do distribute across a union of effects.
92
- *
93
- * The constraint on `Eff` is load-bearing twice over. It is what still refuses a
94
- * `decide` that could produce an EMPTY batch, the one guarantee the shaped
95
- * target used to give and the one `append` relies on. It is also what
96
- * contextually types the produce path's `(event) => [event]` as a one-element
97
- * TUPLE; unconstrained, that widens to `DcbEvent[]`, which is not a
98
- * `NonEmptyReadonlyArray` and would fail at the call site.
99
- *
100
- * The single `as` is narrow and unavoidable: the declared return is a
101
- * conditional over an unresolved `Eff`, so while `Eff` is still a type variable
102
- * the checker cannot relate `(…) => Eff` to `(…) => Effect<…,
103
- * Effect.Error<Eff>, …>`. The value is the two functions it was handed, so
104
- * there is nothing for the cast to be wrong about.
105
- *
106
- * `R` excludes `Serializer | Uuid` because that is what `R` MEANS on `Decider`:
107
- * the interface writes decide's requirement as `Serializer | Uuid | R`, so `R`
108
- * is what the produce side needs BEYOND the codec's own two services. Leaving
109
- * them in would state the same requirement twice and make an ordinary decider
110
- * read as though it carried an extra one, though the set `runDecider` ends up
111
- * asking for is identical either way. The exclusion cannot swallow a caller's
112
- * own service by accident: a `Context.Tag`'s requirement type carries its key as
113
- * a literal `Id` field, so only a tag declared under `@kairos-es/codec/Uuid` or
114
- * `@kairos-es/codec/Serializer` matches — and a tag under that key IS that
115
- * service, context being keyed by the same string at runtime.
116
- */
117
- export const decider = (projections, decide) => ({
118
- projections,
119
- decide
120
- });
71
+ export function decider(projections, decide) {
72
+ return {
73
+ projections,
74
+ decide
75
+ };
76
+ }
121
77
  /**
122
78
  * Run the full DCB command cycle for a decider against the ambient
123
79
  * `DcbEventStore`: build the decision model (real read + decode + fold), decide,
124
- * and append under the read's append condition. Returns the last new position.
80
+ * and append under the read's append condition. Returns the last new position —
81
+ * or, when `decide` answered `Unchanged`, that `Unchanged`, having appended
82
+ * nothing. The result type is `DecisionResult<O>`, so it is plain `Position` for
83
+ * every decider that cannot answer `Unchanged`, including one whose `O` is still
84
+ * a type variable bounded by the batch. ONE signature, deliberately:
85
+ * `decision.ts` says why that alias, and not an overload, is what carries it.
125
86
  *
126
87
  * The requirement is `DcbEventStore | Serializer | Uuid | R |
127
88
  * RequirementOf<P>`: `Serializer` enters through both the decode
@@ -144,7 +105,14 @@ export const runDecider = (decider, command) => Effect.gen(function* () {
144
105
  state,
145
106
  appendCondition
146
107
  } = yield* buildDecisionModel(decider.projections(command));
147
- const newEvents = yield* decider.decide(state, command);
148
- return yield* store.append(newEvents, appendCondition);
108
+ const output = yield* decider.decide(state, command);
109
+ // "Already done" never reaches `append`: an empty batch stays a defect there,
110
+ // and this is not one. The two casts restate `DecisionResult<O>`, which the
111
+ // checker cannot relate to either branch while `O` is still a type variable;
112
+ // the branch taken is exactly the one the conditional selects.
113
+ if (isUnchanged(output)) {
114
+ return output;
115
+ }
116
+ return yield* store.append(output, appendCondition);
149
117
  });
150
118
  //# sourceMappingURL=decider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"decider.js","names":["DcbEventStore","Effect","buildDecisionModel","decider","projections","decide","runDecider","command","gen","store","state","appendCondition","newEvents","append"],"sources":["../../src/decider.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,SAASA,aAAa,QAAQ,iBAAiB;AAE/C,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,kBAAkB,QAA4B,oBAAiB;AAuCxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,OAAO,MAAMC,OAAO,GAAGA,CASrBC,WAAoC,EACpCC,MAA2D,MAO1D;EAAED,WAAW;EAAEC;AAAM,CAAE,CAKvB;AAEH;;;;;;;;;;;;;;;;;;;;AAoBA,OAAO,MAAMC,UAAU,GAAGA,CAMxBH,OAAkC,EAClCI,OAAgB,KAMhBN,MAAM,CAACO,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOT,aAAa;EAClC,MAAM;IAAEU,KAAK;IAAEC;EAAe,CAAE,GAAG,OAAOT,kBAAkB,CAC1DC,OAAO,CAACC,WAAW,CAACG,OAAO,CAAC,CAC7B;EACD,MAAMK,SAAS,GAAG,OAAOT,OAAO,CAACE,MAAM,CAACK,KAAK,EAAEH,OAAO,CAAC;EACvD,OAAO,OAAOE,KAAK,CAACI,MAAM,CAACD,SAAS,EAAED,eAAe,CAAC;AACxD,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"decider.js","names":["DcbEventStore","Effect","isUnchanged","buildDecisionModel","decider","projections","decide","runDecider","command","gen","store","state","appendCondition","output","append"],"sources":["../../src/decider.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAASA,aAAa,QAAQ,iBAAiB;AAE/C,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAGEC,WAAW,QACN,eAAY;AACnB,SAASC,kBAAkB,QAA4B,oBAAiB;AAoJxE,OAAM,SAAUC,OAAOA,CACrBC,WAAoC,EACpCC,MAGoD;EAEpD,OAAO;IAAED,WAAW;IAAEC;EAAM,CAM3B;AACH;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,OAAO,MAAMC,UAAU,GAAGA,CAOxBH,OAAqC,EACrCI,OAAgB,KAMhBP,MAAM,CAACQ,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOV,aAAa;EAClC,MAAM;IAAEW,KAAK;IAAEC;EAAe,CAAE,GAAG,OAAOT,kBAAkB,CAC1DC,OAAO,CAACC,WAAW,CAACG,OAAO,CAAC,CAC7B;EACD,MAAMK,MAAM,GAAmB,OAAOT,OAAO,CAACE,MAAM,CAACK,KAAK,EAAEH,OAAO,CAAC;EACpE;EACA;EACA;EACA;EACA,IAAIN,WAAW,CAACW,MAAM,CAAC,EAAE;IACvB,OAAOA,MAA2B;EACpC;EACA,OAAQ,OAAOH,KAAK,CAACI,MAAM,CAACD,MAAM,EAAED,eAAe,CAAC;AACtD,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,37 @@
1
+ import { Data, Predicate } from 'effect';
2
+ /**
3
+ * The type id `isUnchanged` recognises. `Symbol.for`, so both halves of the dual
4
+ * build — and any two copies of this package in one graph — agree on it.
5
+ */
6
+ export const UnchangedTypeId = /*#__PURE__*/Symbol.for('@kairos-es/codec/Unchanged');
7
+ /**
8
+ * "Nothing to do: this work is already done." A runner that receives it appends
9
+ * nothing and returns it in place of a `Position`.
10
+ *
11
+ * `reason` is for a human reading a trace or a test failure, and nothing in the
12
+ * library reads it. A `Data.TaggedClass`, so two values with the same reason are
13
+ * `Equal` and a test can compare them structurally.
14
+ *
15
+ * ⚠ Test for it with `isUnchanged`, never with `instanceof` or `_tag` — the
16
+ * module documentation says why each of those is wrong.
17
+ */
18
+ export class Unchanged extends /*#__PURE__*/Data.TaggedClass('Unchanged') {
19
+ [UnchangedTypeId] = UnchangedTypeId;
20
+ }
21
+ /**
22
+ * Whether `u` is an `Unchanged`, by its type id. Total over `unknown`, and
23
+ * realm-safe across the dual ESM/CJS build. An application value that merely
24
+ * carries `_tag: 'Unchanged'` answers `false`, deliberately.
25
+ */
26
+ export const isUnchanged = u => Predicate.hasProperty(u, UnchangedTypeId);
27
+ /**
28
+ * The constructors a `decide` step reaches for. One member today; it is a
29
+ * namespace object rather than a bare function so the call site reads as what it
30
+ * is, `Decision.unchanged('already dispatched')`.
31
+ */
32
+ export const Decision = {
33
+ unchanged: reason => new Unchanged(reason === undefined ? {} : {
34
+ reason
35
+ })
36
+ };
37
+ //# sourceMappingURL=decision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision.js","names":["Data","Predicate","UnchangedTypeId","Symbol","for","Unchanged","TaggedClass","isUnchanged","u","hasProperty","Decision","unchanged","reason","undefined"],"sources":["../../src/decision.ts"],"sourcesContent":[null],"mappings":"AA4FA,SAA4BA,IAAI,EAAEC,SAAS,QAAQ,QAAQ;AAE3D;;;;AAIA,OAAO,MAAMC,eAAe,gBAAkBC,MAAM,CAACC,GAAG,CACtD,4BAA4B,CAC7B;AAKD;;;;;;;;;;;AAWA,OAAM,MAAOC,SAAU,sBAAQL,IAAI,CAACM,WAAW,CAAC,WAAW,CAEzD;EACS,CAACJ,eAAe,IAAqBA,eAAe;;AAG/D;;;;;AAKA,OAAO,MAAMK,WAAW,GAAIC,CAAU,IACpCP,SAAS,CAACQ,WAAW,CAACD,CAAC,EAAEN,eAAe,CAAC;AAE3C;;;;;AAKA,OAAO,MAAMQ,QAAQ,GAEjB;EACFC,SAAS,EAAGC,MAAM,IAAK,IAAIP,SAAS,CAACO,MAAM,KAAKC,SAAS,GAAG,EAAE,GAAG;IAAED;EAAM,CAAE;CAC5E","ignoreList":[]}
package/dist/esm/index.js CHANGED
@@ -34,6 +34,17 @@
34
34
  * error union are written once, in the two function bodies, rather than restated
35
35
  * as an annotation above them.
36
36
  *
37
+ * A decision may also answer `Unchanged` — "already done, nothing to append" —
38
+ * built with `Decision.unchanged(reason?)` and recognised ONLY by
39
+ * `isUnchanged`, by its `UnchangedTypeId`. It is a success, not an error and not
40
+ * an empty batch: every runner returns it in place of a `Position` and appends
41
+ * nothing. `Decider`'s fifth type parameter carries the widening and defaults to
42
+ * the non-empty batch, and each runner's result is `DecisionResult<O>`, so a
43
+ * decider or body that cannot answer `Unchanged` keeps a plain `Position` — an
44
+ * untyped one and one generic in its success type included — and its caller is
45
+ * never asked to handle it. `decision.ts` owns the argument, and the one type
46
+ * query that does read differently.
47
+ *
37
48
  * `transact(precondition)(body)` is the layered write path, for a decision whose
38
49
  * second query cannot be written until an earlier read has been folded. It opens
39
50
  * an ambient read ledger, evaluates the precondition inside it, hands that
@@ -54,6 +65,12 @@
54
65
  * | `runDecider` | ✅ the ordinary single-hop case | ⛔ a DEFECT, refused at the store |
55
66
  * | `decideOnly` | ⛔ unrunnable, and `tsc` says so | ✅ the layered case |
56
67
  *
68
+ * Both carry `Unchanged` the same way, which is what keeps them one pair:
69
+ * `runDecider` returns it having appended nothing, and `decideOnly` passes it to
70
+ * the body unaltered, where `transact` returns it having appended nothing —
71
+ * and, having no write to guard, without refusing an empty bracket as
72
+ * `NoReadsRecorded`.
73
+ *
57
74
  * ⚠ *Unrunnable* rather than *unwritable*, and the difference is measured. A bare
58
75
  * `decideOnly(d, cmd)` outside a bracket COMPILES, and so does an exported helper
59
76
  * over it: its `ReadLedger` requirement simply propagates, as any unmet requirement
@@ -100,6 +117,7 @@
100
117
  * record other than the one the query came from (see `decode.ts`).
101
118
  */
102
119
  export { decider, runDecider } from "./decider.js";
120
+ export { Decision, isUnchanged, Unchanged, UnchangedTypeId } from "./decision.js";
103
121
  export { buildDecisionModel, modelAtHead } from "./decisionModel.js";
104
122
  export { decodeSlices } from "./decode.js";
105
123
  export { CodecError, UnknownEventType } from "./errors.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["decider","runDecider","buildDecisionModel","modelAtHead","decodeSlices","CodecError","UnknownEventType","event","decideOnly","transact","unsafeAppend","projection","NoReadsRecorded","ReadLedger","TaglessSliceInLedger","makeRegistry","Serializer","SerializerDefault","toEvent","Uuid","UuidLive","UuidTest"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsGA,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,SACEC,UAAU,EAEVC,QAAQ,EACRC,YAAY,QACP,mBAAgB;AACvB,SAEEC,UAAU,QAEL,iBAAc;AACrB,SAGEC,eAAe,EACfC,UAAU,EACVC,oBAAoB,QACf,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":[]}
1
+ {"version":3,"file":"index.js","names":["decider","runDecider","Decision","isUnchanged","Unchanged","UnchangedTypeId","buildDecisionModel","modelAtHead","decodeSlices","CodecError","UnknownEventType","event","decideOnly","transact","unsafeAppend","projection","NoReadsRecorded","ReadLedger","TaglessSliceInLedger","makeRegistry","Serializer","SerializerDefault","toEvent","Uuid","UuidLive","UuidTest"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuHA,SAAuBA,OAAO,EAAEC,UAAU,QAAQ,cAAW;AAC7D,SACEC,QAAQ,EAGRC,WAAW,EACXC,SAAS,EACTC,eAAe,QACV,eAAY;AACnB,SACEC,kBAAkB,EAClBC,WAAW,QAEN,oBAAiB;AACxB,SAASC,YAAY,QAAQ,aAAU;AACvC,SAASC,UAAU,EAAEC,gBAAgB,QAAQ,aAAU;AACvD,SAA0CC,KAAK,QAAsB,YAAS;AAC9E,SACEC,UAAU,EAEVC,QAAQ,EACRC,YAAY,QACP,mBAAgB;AACvB,SAEEC,UAAU,QAEL,iBAAc;AACrB,SAGEC,eAAe,EACfC,UAAU,EACVC,oBAAoB,QACf,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":[]}
@@ -1,10 +1,12 @@
1
1
  import { AppendIsOwned, DcbEventStore } from '@kairos-es/core';
2
2
  import { Context, Effect, Layer } from 'effect';
3
+ import { isUnchanged } from "./decision.js";
3
4
  import { modelAtHead } from "./decisionModel.js";
4
5
  import { deriveLayeredCondition, makeReadLedger, NoReadsRecorded, ReadLedger } from "./readLedger.js";
5
6
  /**
6
7
  * The DCB command cycle MINUS the append: read the decision model, decode, fold,
7
- * decide, and RETURN the events for the bracket to append.
8
+ * decide, and RETURN the events for the bracket to append — or the decider's
9
+ * `Unchanged`, passed through as it came, for the bracket to append nothing.
8
10
  *
9
11
  * Bracket-only, and the refusal is STATIC — see the section below for the mechanism
10
12
  * and, more importantly, for where the error actually lands, which is not where a
@@ -124,11 +126,17 @@ Effect.mapInputContext(store.append(events), context => Context.omit(AppendIsOwn
124
126
  * appends without opening one, and its events usually match the union the bracket
125
127
  * is about to guard on, so the bracket's own close would be refused by a condition
126
128
  * its own body invalidated.
129
+ *
130
+ * The body may succeed with `Unchanged` (see the module documentation), and the
131
+ * result is then `DecisionResult<O>`: `Position` for a body that cannot answer
132
+ * it, `Position | Unchanged` for one that can. `O` is last and defaulted, so an
133
+ * explicit `<E, R>` written against the narrower signature still means what it
134
+ * did.
127
135
  */
128
136
  export const transact = precondition => body => Effect.gen(function* () {
129
137
  const store = yield* DcbEventStore;
130
138
  const ledger = yield* makeReadLedger;
131
- const events = yield* Effect.gen(function* () {
139
+ const output = yield* Effect.gen(function* () {
132
140
  // ORDER IS THE DESIGN. The precondition runs first and runs HERE, with the
133
141
  // ledger already in context, so its reads are hops in the very condition
134
142
  // that will guard the body's write.
@@ -156,10 +164,19 @@ export const transact = precondition => body => Effect.gen(function* () {
156
164
  Effect.provide(Layer.merge(ledger.layer, Layer.succeed(AppendIsOwned, {
157
165
  by: 'transact'
158
166
  }))));
167
+ // "Already done": nothing to append, so no condition to derive and nothing
168
+ // for `NoReadsRecorded` to protect — that refusal guards a WRITE that would
169
+ // go out unguarded, and there is none. It must stay BEFORE the derivation,
170
+ // or an empty bracket answering `Unchanged` would be refused for a write it
171
+ // never asked for. The casts restate `DecisionResult<O>`, which the checker
172
+ // cannot relate to either branch while `O` is still a type variable.
173
+ if (isUnchanged(output)) {
174
+ return output;
175
+ }
159
176
  const condition = deriveLayeredCondition(yield* ledger.hops);
160
177
  if (condition === undefined) {
161
178
  return yield* new NoReadsRecorded();
162
179
  }
163
- return yield* store.append(events, condition);
180
+ return yield* store.append(output, condition);
164
181
  });
165
182
  //# sourceMappingURL=precondition.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"precondition.js","names":["AppendIsOwned","DcbEventStore","Context","Effect","Layer","modelAtHead","deriveLayeredCondition","makeReadLedger","NoReadsRecorded","ReadLedger","decideOnly","decider","command","gen","state","projections","decide","unsafeAppend","events","reason","flatMap","store","mapInputContext","append","context","omit","transact","precondition","body","ledger","proof","evaluate","pipe","provideService","tag","provide","merge","layer","succeed","by","condition","hops","undefined"],"sources":["../../src/precondition.ts"],"sourcesContent":[null],"mappings":"AAyFA,SAASA,aAAa,EAAEC,aAAa,QAAQ,iBAAiB;AAE9D,SAASC,OAAO,EAAEC,MAAM,EAAEC,KAAK,QAAQ,QAAQ;AAE/C,SAASC,WAAW,QAA4B,oBAAiB;AAGjE,SACEC,sBAAsB,EACtBC,cAAc,EACdC,eAAe,EACfC,UAAU,QAEL,iBAAc;AAgCrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,OAAO,MAAMC,UAAU,GAAGA,CAMxBC,OAAkC,EAClCC,OAAgB,KAMhBT,MAAM,CAACU,GAAG,CAAC,aAAS;EAClB,OAAOJ,UAAU;EAEjB,MAAM;IAAEK;EAAK,CAAE,GAAG,OAAOT,WAAW,CAACM,OAAO,CAACI,WAAW,CAACH,OAAO,CAAC,CAAC;EAClE,OAAO,OAAOD,OAAO,CAACK,MAAM,CAACF,KAAK,EAAEF,OAAO,CAAC;AAC9C,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;AAsBA,OAAO,MAAMK,YAAY,GAAGA,CAC1BC,MAA2C;AAC3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAC,MAAc,KAEdhB,MAAM,CAACiB,OAAO,CAACnB,aAAa,EAAGoB,KAAK;AAClC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAlB,MAAM,CAACmB,eAAe,CACpBD,KAAK,CAACE,MAAM,CAACL,MAAM,CAAC,EACnBM,OAA+B,IAAKtB,OAAO,CAACuB,IAAI,CAACzB,aAAa,CAAC,CAACwB,OAAO,CAAC,CAC1E,CACF;AAEH;;;;;;;;;;;;;;;;;AAiBA,OAAO,MAAME,QAAQ,GACGC,YAA+C,IAEnEC,IAA8D,IAQ9DzB,MAAM,CAACU,GAAG,CAAC,aAAS;EAClB,MAAMQ,KAAK,GAAG,OAAOpB,aAAa;EAClC,MAAM4B,MAAM,GAAG,OAAOtB,cAAc;EAEpC,MAAMW,MAAM,GAAG,OAAOf,MAAM,CAACU,GAAG,CAAC,aAAS;IACxC;IACA;IACA;IACA,MAAMiB,KAAK,GAAG,OAAOH,YAAY,CAACI,QAAQ;IAC1C,OAAO,OAAOH,IAAI,CAACI,IAAI,CAAC7B,MAAM,CAAC8B,cAAc,CAACN,YAAY,CAACO,GAAG,EAAEJ,KAAK,CAAC,CAAC;EACzE,CAAC,CAAC,CAACE,IAAI;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA7B,MAAM,CAACgC,OAAO,CACZ/B,KAAK,CAACgC,KAAK,CACTP,MAAM,CAACQ,KAAK,EACZjC,KAAK,CAACkC,OAAO,CAACtC,aAAa,EAAE;IAAEuC,EAAE,EAAE;EAAU,CAAE,CAAC,CACjD,CACF,CACF;EAED,MAAMC,SAAS,GAAGlC,sBAAsB,CAAC,OAAOuB,MAAM,CAACY,IAAI,CAAC;EAC5D,IAAID,SAAS,KAAKE,SAAS,EAAE;IAC3B,OAAO,OAAO,IAAIlC,eAAe,EAAE;EACrC;EAEA,OAAO,OAAOa,KAAK,CAACE,MAAM,CAACL,MAAM,EAAEsB,SAAS,CAAC;AAC/C,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"precondition.js","names":["AppendIsOwned","DcbEventStore","Context","Effect","Layer","isUnchanged","modelAtHead","deriveLayeredCondition","makeReadLedger","NoReadsRecorded","ReadLedger","decideOnly","decider","command","gen","state","projections","decide","unsafeAppend","events","reason","flatMap","store","mapInputContext","append","context","omit","transact","precondition","body","ledger","output","proof","evaluate","pipe","provideService","tag","provide","merge","layer","succeed","by","condition","hops","undefined"],"sources":["../../src/precondition.ts"],"sourcesContent":[null],"mappings":"AAwGA,SAASA,aAAa,EAAEC,aAAa,QAAQ,iBAAiB;AAE9D,SAASC,OAAO,EAAEC,MAAM,EAAEC,KAAK,QAAQ,QAAQ;AAE/C,SAGEC,WAAW,QACN,eAAY;AACnB,SAASC,WAAW,QAA4B,oBAAiB;AAGjE,SACEC,sBAAsB,EACtBC,cAAc,EACdC,eAAe,EACfC,UAAU,QAEL,iBAAc;AAgCrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,OAAO,MAAMC,UAAU,GAAGA,CAOxBC,OAAqC,EACrCC,OAAgB,KAMhBV,MAAM,CAACW,GAAG,CAAC,aAAS;EAClB,OAAOJ,UAAU;EAEjB,MAAM;IAAEK;EAAK,CAAE,GAAG,OAAOT,WAAW,CAACM,OAAO,CAACI,WAAW,CAACH,OAAO,CAAC,CAAC;EAClE,OAAO,OAAOD,OAAO,CAACK,MAAM,CAACF,KAAK,EAAEF,OAAO,CAAC;AAC9C,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;AAsBA,OAAO,MAAMK,YAAY,GAAGA,CAC1BC,MAA2C;AAC3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAC,MAAc,KAEdjB,MAAM,CAACkB,OAAO,CAACpB,aAAa,EAAGqB,KAAK;AAClC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAnB,MAAM,CAACoB,eAAe,CACpBD,KAAK,CAACE,MAAM,CAACL,MAAM,CAAC,EACnBM,OAA+B,IAAKvB,OAAO,CAACwB,IAAI,CAAC1B,aAAa,CAAC,CAACyB,OAAO,CAAC,CAC1E,CACF;AAEH;;;;;;;;;;;;;;;;;;;;;;;AAuBA,OAAO,MAAME,QAAQ,GACGC,YAA+C,IAEnEC,IAA4B,IAQ5B1B,MAAM,CAACW,GAAG,CAAC,aAAS;EAClB,MAAMQ,KAAK,GAAG,OAAOrB,aAAa;EAClC,MAAM6B,MAAM,GAAG,OAAOtB,cAAc;EAEpC,MAAMuB,MAAM,GAAmB,OAAO5B,MAAM,CAACW,GAAG,CAAC,aAAS;IACxD;IACA;IACA;IACA,MAAMkB,KAAK,GAAG,OAAOJ,YAAY,CAACK,QAAQ;IAC1C,OAAO,OAAOJ,IAAI,CAACK,IAAI,CAAC/B,MAAM,CAACgC,cAAc,CAACP,YAAY,CAACQ,GAAG,EAAEJ,KAAK,CAAC,CAAC;EACzE,CAAC,CAAC,CAACE,IAAI;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA/B,MAAM,CAACkC,OAAO,CACZjC,KAAK,CAACkC,KAAK,CACTR,MAAM,CAACS,KAAK,EACZnC,KAAK,CAACoC,OAAO,CAACxC,aAAa,EAAE;IAAEyC,EAAE,EAAE;EAAU,CAAE,CAAC,CACjD,CACF,CACF;EAED;EACA;EACA;EACA;EACA;EACA;EACA,IAAIpC,WAAW,CAAC0B,MAAM,CAAC,EAAE;IACvB,OAAOA,MAA2B;EACpC;EAEA,MAAMW,SAAS,GAAGnC,sBAAsB,CAAC,OAAOuB,MAAM,CAACa,IAAI,CAAC;EAC5D,IAAID,SAAS,KAAKE,SAAS,EAAE;IAC3B,OAAO,OAAO,IAAInC,eAAe,EAAE;EACrC;EAEA,OAAQ,OAAOa,KAAK,CAACE,MAAM,CAACO,MAAM,EAAEW,SAAS,CAAC;AAChD,CAAC,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairos-es/codec",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.3",
4
4
  "description": "Schema-first codec with encryption seam for kairos-es",
5
5
  "license": "BSD-3-Clause",
6
6
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "homepage": "https://github.com/neverbland/kairos-es",
13
13
  "peerDependencies": {
14
14
  "effect": "^3.22.0",
15
- "@kairos-es/core": "^1.0.0-alpha.1"
15
+ "@kairos-es/core": "^1.0.0-alpha.3"
16
16
  },
17
17
  "main": "./dist/cjs/index.js",
18
18
  "module": "./dist/esm/index.js",
package/src/decider.ts CHANGED
@@ -52,17 +52,32 @@
52
52
  * boundary, and decode may fail with `CodecError`. All surface on the error
53
53
  * channel, so a caller handles all three the same way — and
54
54
  * `@kairos-es/testing`'s `.thenError(tag)` can assert any of them.
55
+ *
56
+ * `decide` may also answer `Unchanged` — "already done, nothing to append" — but
57
+ * only where its type says so: `Decider`'s fifth type parameter `O` defaults to
58
+ * the non-empty batch, and `runDecider` returns `DecisionResult<O>`, which is
59
+ * plain `Position` unless `O` admits `Unchanged`. So a decider that cannot say
60
+ * "already done" keeps exactly the result type it always had — an untyped one
61
+ * and one built in a wrapper generic in its success type included — and one
62
+ * that can makes its caller handle `Position | Unchanged`. `runDecider` returns an
63
+ * `Unchanged` without calling `append`, so `append`'s refusal of an empty batch
64
+ * is untouched and never met. `decision.ts` owns why it is a success rather than
65
+ * an error or an empty batch, and why it is recognised by a type id.
55
66
  */
56
67
 
57
68
  import type {
58
69
  AppendConditionFailed,
59
70
  CompositeState,
60
71
  DcbEvent,
61
- Position,
62
72
  } from '@kairos-es/core'
63
73
  import { DcbEventStore } from '@kairos-es/core'
64
74
  import type { Array as Arr } from 'effect'
65
75
  import { Effect } from 'effect'
76
+ import {
77
+ type DecisionOutput,
78
+ type DecisionResult,
79
+ isUnchanged,
80
+ } from './decision'
66
81
  import { buildDecisionModel, type RequirementOf } from './decisionModel'
67
82
  import type { CodecError } from './errors'
68
83
  import type { SliceProjection } from './projection'
@@ -84,22 +99,26 @@ import type { Uuid } from './Uuid'
84
99
  * schema-backed, state-erased bound for a heterogeneous record); the
85
100
  * precise per-key state is recovered by core's shared `CompositeState`. The
86
101
  * requirement `R` is the union carried by the record's slices' schemas.
102
+ *
103
+ * `O` is what `decide` may succeed with. It defaults to the non-empty batch, and
104
+ * a decider that can answer "already done" widens it to `DecisionOutput`
105
+ * (`NonEmptyReadonlyArray<DcbEvent> | Unchanged`). It is LAST and DEFAULTED so
106
+ * that every `Decider<P, Command, E, R>` already written means exactly what it
107
+ * meant. Its bound is `DecisionOutput`, which still excludes a possibly-empty
108
+ * array — the widening adds an answer and removes no guarantee.
87
109
  */
88
110
  export interface Decider<
89
111
  P extends Record<string, SliceProjection>,
90
112
  Command,
91
113
  E = never,
92
114
  R = never,
115
+ O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>,
93
116
  > {
94
117
  readonly projections: (command: Command) => P
95
118
  readonly decide: (
96
119
  state: CompositeState<P>,
97
120
  command: Command,
98
- ) => Effect.Effect<
99
- Arr.NonEmptyReadonlyArray<DcbEvent>,
100
- E,
101
- Serializer | Uuid | R
102
- >
121
+ ) => Effect.Effect<O, E, Serializer | Uuid | R>
103
122
  }
104
123
 
105
124
  /**
@@ -149,6 +168,22 @@ export interface Decider<
149
168
  * Effect.Error<Eff>, …>`. The value is the two functions it was handed, so
150
169
  * there is nothing for the cast to be wrong about.
151
170
  *
171
+ * The success channel is how a decider opts into `Unchanged` without writing a
172
+ * fifth type argument, and it is why this function is declared TWICE. The first
173
+ * declaration bounds `Eff` by the non-empty batch and returns a `Decider` with
174
+ * the DEFAULT `O`; the second bounds it by `DecisionOutput` and returns one whose
175
+ * `O` is `DecisionOutput`. A `decide` with a branch returning
176
+ * `Decision.unchanged(…)` fails the first bound and lands on the second, and one
177
+ * with no such branch keeps the default. Either way `O` is one of the two a
178
+ * caller ever writes, never the inferred success type passed through: inference
179
+ * is more precise than anyone writes — the produce path's one-element tuple, say
180
+ * — and a `Decider<…, readonly [DcbEvent]>` would not be the
181
+ * `Decider<P, Command, E, R>` its caller spells, though it behaves identically.
182
+ * Asking by ASSIGNABILITY rather than by a conditional over `Eff`'s success is
183
+ * what keeps an untyped (`any`) success, and a success that is a type variable
184
+ * bounded by the batch, on the default; `decision.ts` owns that argument, and
185
+ * the one type query the pair cannot answer as before.
186
+ *
152
187
  * `R` excludes `Serializer | Uuid` because that is what `R` MEANS on `Decider`:
153
188
  * the interface writes decide's requirement as `Serializer | Uuid | R`, so `R`
154
189
  * is what the produce side needs BEYOND the codec's own two services. Leaving
@@ -160,7 +195,7 @@ export interface Decider<
160
195
  * `@kairos-es/codec/Serializer` matches — and a tag under that key IS that
161
196
  * service, context being keyed by the same string at runtime.
162
197
  */
163
- export const decider = <
198
+ export function decider<
164
199
  P extends Record<string, SliceProjection>,
165
200
  Command,
166
201
  Eff extends Effect.Effect<
@@ -176,18 +211,46 @@ export const decider = <
176
211
  Command,
177
212
  Effect.Effect.Error<Eff>,
178
213
  Exclude<Effect.Effect.Context<Eff>, Serializer | Uuid>
179
- > =>
180
- ({ projections, decide }) as Decider<
214
+ >
215
+ export function decider<
216
+ P extends Record<string, SliceProjection>,
217
+ Command,
218
+ Eff extends Effect.Effect<DecisionOutput, unknown, unknown>,
219
+ >(
220
+ projections: (command: Command) => P,
221
+ decide: (state: CompositeState<P>, command: Command) => Eff,
222
+ ): Decider<
223
+ P,
224
+ Command,
225
+ Effect.Effect.Error<Eff>,
226
+ Exclude<Effect.Effect.Context<Eff>, Serializer | Uuid>,
227
+ DecisionOutput
228
+ >
229
+ export function decider<P extends Record<string, SliceProjection>, Command>(
230
+ projections: (command: Command) => P,
231
+ decide: (
232
+ state: CompositeState<P>,
233
+ command: Command,
234
+ ) => Effect.Effect<DecisionOutput, unknown, unknown>,
235
+ ): Decider<P, Command, unknown, unknown, DecisionOutput> {
236
+ return { projections, decide } as Decider<
181
237
  P,
182
238
  Command,
183
- Effect.Effect.Error<Eff>,
184
- Exclude<Effect.Effect.Context<Eff>, Serializer | Uuid>
239
+ unknown,
240
+ unknown,
241
+ DecisionOutput
185
242
  >
243
+ }
186
244
 
187
245
  /**
188
246
  * Run the full DCB command cycle for a decider against the ambient
189
247
  * `DcbEventStore`: build the decision model (real read + decode + fold), decide,
190
- * and append under the read's append condition. Returns the last new position.
248
+ * and append under the read's append condition. Returns the last new position —
249
+ * or, when `decide` answered `Unchanged`, that `Unchanged`, having appended
250
+ * nothing. The result type is `DecisionResult<O>`, so it is plain `Position` for
251
+ * every decider that cannot answer `Unchanged`, including one whose `O` is still
252
+ * a type variable bounded by the batch. ONE signature, deliberately:
253
+ * `decision.ts` says why that alias, and not an overload, is what carries it.
191
254
  *
192
255
  * The requirement is `DcbEventStore | Serializer | Uuid | R |
193
256
  * RequirementOf<P>`: `Serializer` enters through both the decode
@@ -209,11 +272,12 @@ export const runDecider = <
209
272
  Command,
210
273
  E,
211
274
  R,
275
+ O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>,
212
276
  >(
213
- decider: Decider<P, Command, E, R>,
277
+ decider: Decider<P, Command, E, R, O>,
214
278
  command: Command,
215
279
  ): Effect.Effect<
216
- Position,
280
+ DecisionResult<O>,
217
281
  E | CodecError | AppendConditionFailed | TaglessSliceInLedger,
218
282
  DcbEventStore | Serializer | Uuid | R | RequirementOf<P>
219
283
  > =>
@@ -222,6 +286,13 @@ export const runDecider = <
222
286
  const { state, appendCondition } = yield* buildDecisionModel(
223
287
  decider.projections(command),
224
288
  )
225
- const newEvents = yield* decider.decide(state, command)
226
- return yield* store.append(newEvents, appendCondition)
289
+ const output: DecisionOutput = yield* decider.decide(state, command)
290
+ // "Already done" never reaches `append`: an empty batch stays a defect there,
291
+ // and this is not one. The two casts restate `DecisionResult<O>`, which the
292
+ // checker cannot relate to either branch while `O` is still a type variable;
293
+ // the branch taken is exactly the one the conditional selects.
294
+ if (isUnchanged(output)) {
295
+ return output as DecisionResult<O>
296
+ }
297
+ return (yield* store.append(output, appendCondition)) as DecisionResult<O>
227
298
  })