@kairos-es/codec 1.0.0-alpha.0 → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +75 -3
  2. package/dist/cjs/decider.js +44 -69
  3. package/dist/cjs/decider.js.map +1 -1
  4. package/dist/cjs/decision.js +45 -0
  5. package/dist/cjs/decision.js.map +1 -0
  6. package/dist/cjs/decisionModel.js +76 -20
  7. package/dist/cjs/decisionModel.js.map +1 -1
  8. package/dist/cjs/index.js +63 -0
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/precondition.js +191 -0
  11. package/dist/cjs/precondition.js.map +1 -0
  12. package/dist/cjs/readLedger.js +224 -0
  13. package/dist/cjs/readLedger.js.map +1 -0
  14. package/dist/dts/decider.d.ts +57 -8
  15. package/dist/dts/decider.d.ts.map +1 -1
  16. package/dist/dts/decision.d.ts +161 -0
  17. package/dist/dts/decision.d.ts.map +1 -0
  18. package/dist/dts/decisionModel.d.ts +33 -13
  19. package/dist/dts/decisionModel.d.ts.map +1 -1
  20. package/dist/dts/index.d.ts +69 -0
  21. package/dist/dts/index.d.ts.map +1 -1
  22. package/dist/dts/precondition.d.ts +244 -0
  23. package/dist/dts/precondition.d.ts.map +1 -0
  24. package/dist/dts/readLedger.d.ts +263 -0
  25. package/dist/dts/readLedger.d.ts.map +1 -0
  26. package/dist/esm/decider.js +42 -67
  27. package/dist/esm/decider.js.map +1 -1
  28. package/dist/esm/decision.js +37 -0
  29. package/dist/esm/decision.js.map +1 -0
  30. package/dist/esm/decisionModel.js +77 -21
  31. package/dist/esm/decisionModel.js.map +1 -1
  32. package/dist/esm/index.js +69 -0
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/precondition.js +182 -0
  35. package/dist/esm/precondition.js.map +1 -0
  36. package/dist/esm/readLedger.js +212 -0
  37. package/dist/esm/readLedger.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/decider.ts +98 -19
  40. package/src/decision.ts +174 -0
  41. package/src/decisionModel.ts +87 -23
  42. package/src/index.ts +87 -0
  43. package/src/precondition.ts +371 -0
  44. package/src/readLedger.ts +394 -0
package/README.md CHANGED
@@ -30,6 +30,42 @@ because it touches none of that package's machinery, and it is deliberately not
30
30
  a decider calls: `runDecider` already folds to head through `buildDecisionModel`,
31
31
  under the condition that read produces.
32
32
 
33
+ `transact(precondition)(body)` is the LAYERED write path, for a decision whose second
34
+ query cannot be written until an earlier read has been folded — the identifier it needs
35
+ is data that fold produced, and better tagging cannot remove the hop. Outside a bracket
36
+ nothing changes and no signature moves. Inside one, every read is recorded wherever and
37
+ by whatever symbol it happened, and the bracket appends what the body returned under the
38
+ union of every query walked, at the EARLIEST head observed. No append condition reaches
39
+ you, so there is none to assemble wrongly — and the familiar `buildDecisionModel`, which
40
+ guards only its own hop outside a bracket, derives from the whole walk inside one.
41
+
42
+ The bracket takes ONE precondition, a `{ tag, evaluate }` pair it evaluates INSIDE the
43
+ ledger so the guard's own reads are part of what guards the write; its result reaches the
44
+ body as a service, so a body that consumes it cannot be run without one. `decideOnly`
45
+ returns a decider's events for the bracket to append. `unsafeAppend(events, reason)` is
46
+ the one sanctioned append under no condition, and the reason is required.
47
+
48
+ ⚠ Three things to know before you use it. **Wrap conflict retry around the whole
49
+ bracket**, never inside it: a retry within one re-records into the ledger already
50
+ open and its floor cannot advance. **`modelAtHead`'s error channel carries
51
+ `TaglessSliceInLedger`**, raised when a slice built without tags would be unioned
52
+ with another — build every slice a layered decision reads with
53
+ `build({ tags: [...] })`.
54
+
55
+ And ⚠⚠ **inside a bracket, read through `modelAtHead` and nothing else.** A read
56
+ that consults a projection, a materialised view or raw SQL contributes no hop, so
57
+ its query is absent from the derived union and its head is absent from the floor:
58
+ the decision rests on data the append is not guarded on, and the bracket LOOKS
59
+ guarded. Nothing will tell you — not the types, not `NoReadsRecorded`, which fires
60
+ only on a bracket that read nothing at all, and no lint rule, which could not see a
61
+ read one call away in a helper and would not run in your tree in any case. If a
62
+ read must consult a view, do it outside the bracket, where its answer is not part
63
+ of what the append is guarded on.
64
+
65
+ The reasoning for all of this is in
66
+ [ADR-0014](../../docs/adr/0014-layered-decision-models-an-ambient-read-ledger-and-a-bracket-that-owns-the-append.md).
67
+ The root README says why a repo-relative link does not resolve for you yet.
68
+
33
69
  `decodeSlices(slices)` is the decode stage `modelAtHead` folds through, and so in turn
34
70
  the one `buildDecisionModel` gets by building on `modelAtHead` — exported so the read
35
71
  side's `ProjectionRunner` shares one implementation over a live `subscribe` stream
@@ -37,9 +73,45 @@ rather than reimplementing it. `makeRegistry(defs)` is the minimal general
37
73
  path, decoding an arbitrary wire event by its runtime type string with an unknown
38
74
  type as a typed `UnknownEventType` (ADR-0005).
39
75
 
40
- `Decider` and `runDecider(decider, command)` close the write side's whole command
41
- cycle over that fold: build the decision model, decide, append under the read's
42
- own condition. `decider(projections, decide)` builds one with its slice record,
76
+ `Decider` and `runDecider(decider, command)` close a SINGLE-HOP command cycle over
77
+ that fold: build the decision model, decide, append under the read's own condition.
78
+
79
+ A decision may also answer **`Unchanged`** — "already done, nothing to append" —
80
+ with `Decision.unchanged(reason?)`. It is a success, not an error and not an empty
81
+ batch: `runDecider` returns it in place of a `Position` and appends nothing, and
82
+ `transact` does the same for a body that returns it (without refusing an empty
83
+ bracket as `NoReadsRecorded`, since there is no write to guard). Test for it with
84
+ `isUnchanged`, never with `instanceof` or its `_tag`: the first breaks across the
85
+ dual ESM/CJS build, and the second would match your own union's member of the same
86
+ name. A decider that cannot answer `Unchanged` keeps a plain `Position` result —
87
+ an untyped (`any`) one, and one built in a wrapper generic in its success type,
88
+ included — so no call site changes for code that never uses it. The one thing that
89
+ does is a type query over a signature whose `O` is still open, such as
90
+ `ReturnType<typeof runDecider>`: it now reads `Position | Unchanged`, and
91
+ `typeof runDecider<P, Command, E, R>` gives `Position` back. `decider(...)` infers
92
+ the widening from a branch returning `Decision.unchanged(...)`, and a hand-written
93
+ `Decider` opts in with its fifth type argument, `DecisionOutput`.
94
+
95
+ ⚠ **`runDecider` and `decideOnly` are two dispositions of one cycle**, and each
96
+ refuses exactly where the other is the answer:
97
+
98
+ | | outside a bracket | inside a bracket |
99
+ | ------------ | ------------------------------ | ----------------------------------- |
100
+ | `runDecider` | ✅ the ordinary single-hop case | ⛔ refused as a defect when it runs |
101
+ | `decideOnly` | ⛔ unrunnable, and `tsc` says so | ✅ the layered case |
102
+
103
+ So the remedy is always one identifier away and you never have to guess which you
104
+ wanted. Neither is expressed over the other — each would have to defeat the other's
105
+ refusal to be.
106
+
107
+ ⚠ *Unrunnable* rather than *unwritable*: a bare `decideOnly(...)` outside a bracket
108
+ compiles, and so does a helper you export over it, because its `ReadLedger`
109
+ requirement propagates like any other unmet one. The error lands where you DISCHARGE
110
+ the context — `Effect.provide`, `runPromise`, or an annotation asking for
111
+ `R = never` — which may be a different file. You are told by the type checker rather
112
+ than by a failing run, which is the point; you are not told at the call.
113
+
114
+ `decider(projections, decide)` builds one with its slice record,
43
115
  its error union and its extra requirement all inferred — the command type, on the
44
116
  `projections` parameter, is the only annotation a caller writes. They live here,
45
117
  beside the `buildDecisionModel` they drive, rather
@@ -3,9 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.runDecider = exports.decider = void 0;
6
+ exports.decider = decider;
7
+ exports.runDecider = void 0;
7
8
  var _core = require("@kairos-es/core");
8
9
  var _effect = require("effect");
10
+ var _decision = require("./decision.js");
9
11
  var _decisionModel = require("./decisionModel.js");
10
12
  /**
11
13
  * A `Decider`: the write side's unit of decision. It pairs the projections a
@@ -61,92 +63,65 @@ var _decisionModel = require("./decisionModel.js");
61
63
  * boundary, and decode may fail with `CodecError`. All surface on the error
62
64
  * channel, so a caller handles all three the same way — and
63
65
  * `@kairos-es/testing`'s `.thenError(tag)` can assert any of them.
64
- */
65
-
66
- /**
67
- * Build a `Decider` with all four of its type arguments INFERRED: the slice
68
- * record `P` from what `projections` returns, the error union `E` and the extra
69
- * requirement `R` from what `decide` returns. The command type is the one thing
70
- * a caller still writes, once, as the annotation on the `projections` parameter
71
- * — `Command` occurs only in parameter positions, so nothing else could supply
72
- * it, and it is domain information (the command this decision is parametrised
73
- * by) rather than plumbing.
74
- *
75
- * It adds no capability: the interface stays directly inhabitable and this
76
- * returns exactly the object literal a caller would have written. What it
77
- * changes is what an author must SPELL. An object literal annotated
78
- * `Decider<P, Command, E>` has to state the whole slice record — a
79
- * `ReturnType<typeof someProjection>` per key, a slice's state type being
80
- * inferred by `projection().on().build()` and so having no name to write — plus
81
- * the union of every error its decide branches can fail with. Dropping the
82
- * annotation instead is not open to the author: without it `decide`'s `state`
83
- * and `command` parameters have no contextual type and are implicitly `any`. So
84
- * the cost landed hardest on the worked example, where some thirty lines of type
85
- * plumbing sat between the reader and the three decisions that file exists to
86
- * teach.
87
- *
88
- * `Eff` captures decide's WHOLE return type as one type parameter instead of
89
- * inferring `E` and `R` from an `Effect<…, E, Serializer | Uuid | R>` shaped
90
- * target, and that is the part that had to be got right rather than merely
91
- * written down. A real `decide` returns a UNION of effects — one `Effect.fail`
92
- * per rule it enforces, plus the produce path — and TypeScript infers a type
93
- * parameter from a union source by choosing one constituent's candidate, not by
94
- * unioning them. Against the shaped target `E` therefore came out as whichever
95
- * branch won, and every other branch was then rejected against it. Inference to
96
- * a NAKED type variable takes the union whole, which is why `Eff` is the
97
- * parameter and `Effect.Effect.Error` / `Effect.Effect.Context` recover the two
98
- * channels afterwards: those do distribute across a union of effects.
99
66
  *
100
- * The constraint on `Eff` is load-bearing twice over. It is what still refuses a
101
- * `decide` that could produce an EMPTY batch, the one guarantee the shaped
102
- * target used to give and the one `append` relies on. It is also what
103
- * contextually types the produce path's `(event) => [event]` as a one-element
104
- * TUPLE; unconstrained, that widens to `DcbEvent[]`, which is not a
105
- * `NonEmptyReadonlyArray` and would fail at the call site.
106
- *
107
- * The single `as` is narrow and unavoidable: the declared return is a
108
- * conditional over an unresolved `Eff`, so while `Eff` is still a type variable
109
- * the checker cannot relate `(…) => Eff` to `(…) => Effect<…,
110
- * Effect.Error<Eff>, …>`. The value is the two functions it was handed, so
111
- * there is nothing for the cast to be wrong about.
112
- *
113
- * `R` excludes `Serializer | Uuid` because that is what `R` MEANS on `Decider`:
114
- * the interface writes decide's requirement as `Serializer | Uuid | R`, so `R`
115
- * is what the produce side needs BEYOND the codec's own two services. Leaving
116
- * them in would state the same requirement twice and make an ordinary decider
117
- * read as though it carried an extra one, though the set `runDecider` ends up
118
- * asking for is identical either way. The exclusion cannot swallow a caller's
119
- * own service by accident: a `Context.Tag`'s requirement type carries its key as
120
- * a literal `Id` field, so only a tag declared under `@kairos-es/codec/Uuid` or
121
- * `@kairos-es/codec/Serializer` matches — and a tag under that key IS that
122
- * service, context being keyed by the same string at runtime.
67
+ * `decide` may also answer `Unchanged` — "already done, nothing to append" — but
68
+ * only where its type says so: `Decider`'s fifth type parameter `O` defaults to
69
+ * the non-empty batch, and `runDecider` returns `DecisionResult<O>`, which is
70
+ * plain `Position` unless `O` admits `Unchanged`. So a decider that cannot say
71
+ * "already done" keeps exactly the result type it always had — an untyped one
72
+ * and one built in a wrapper generic in its success type included — and one
73
+ * that can makes its caller handle `Position | Unchanged`. `runDecider` returns an
74
+ * `Unchanged` without calling `append`, so `append`'s refusal of an empty batch
75
+ * is untouched and never met. `decision.ts` owns why it is a success rather than
76
+ * an error or an empty batch, and why it is recognised by a type id.
123
77
  */
124
- const decider = (projections, decide) => ({
125
- projections,
126
- decide
127
- });
78
+
79
+ function decider(projections, decide) {
80
+ return {
81
+ projections,
82
+ decide
83
+ };
84
+ }
128
85
  /**
129
86
  * Run the full DCB command cycle for a decider against the ambient
130
87
  * `DcbEventStore`: build the decision model (real read + decode + fold), decide,
131
- * and append under the read's append condition. Returns the last new position.
88
+ * and append under the read's append condition. Returns the last new position —
89
+ * or, when `decide` answered `Unchanged`, that `Unchanged`, having appended
90
+ * nothing. The result type is `DecisionResult<O>`, so it is plain `Position` for
91
+ * every decider that cannot answer `Unchanged`, including one whose `O` is still
92
+ * a type variable bounded by the batch. ONE signature, deliberately:
93
+ * `decision.ts` says why that alias, and not an overload, is what carries it.
132
94
  *
133
95
  * The requirement is `DcbEventStore | Serializer | Uuid | R |
134
96
  * RequirementOf<P>`: `Serializer` enters through both the decode
135
97
  * (`buildDecisionModel`) and produce (`toEvent`) sides, `Uuid` through produce,
136
98
  * the slices' `RequirementOf<P>` through decode, and `R` through produce. (For a
137
99
  * def whose `Schema` carries a requirement, `RequirementOf<P>` and `R` are the
138
- * same tag.) Any `decide` failure, a decode `CodecError`, or an
139
- * `AppendConditionFailed` propagates on the error channel unchanged.
100
+ * same tag.) Any `decide` failure, a decode `CodecError`, an
101
+ * `AppendConditionFailed`, or the read's own `TaglessSliceInLedger` propagates on
102
+ * the error channel unchanged.
103
+ *
104
+ * ⚠ INSIDE a bracket this function is REFUSED, as a defect, at the store's shared
105
+ * append decorator, and `decideOnly` is what a body reaches for instead.
106
+ * `index.ts` states that pair, why each refuses exactly where the other is the
107
+ * answer, and why neither can be expressed over the other. Read it before
108
+ * changing either signature; this file does not restate the argument.
140
109
  */
141
- exports.decider = decider;
142
110
  const runDecider = (decider, command) => _effect.Effect.gen(function* () {
143
111
  const store = yield* _core.DcbEventStore;
144
112
  const {
145
113
  state,
146
114
  appendCondition
147
115
  } = yield* (0, _decisionModel.buildDecisionModel)(decider.projections(command));
148
- const newEvents = yield* decider.decide(state, command);
149
- return yield* store.append(newEvents, appendCondition);
116
+ const output = yield* decider.decide(state, command);
117
+ // "Already done" never reaches `append`: an empty batch stays a defect there,
118
+ // and this is not one. The two casts restate `DecisionResult<O>`, which the
119
+ // checker cannot relate to either branch while `O` is still a type variable;
120
+ // the branch taken is exactly the one the conditional selects.
121
+ if ((0, _decision.isUnchanged)(output)) {
122
+ return output;
123
+ }
124
+ return yield* store.append(output, appendCondition);
150
125
  });
151
126
  exports.runDecider = runDecider;
152
127
  //# sourceMappingURL=decider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"decider.js","names":["_core","require","_effect","_decisionModel","decider","projections","decide","exports","runDecider","command","Effect","gen","store","DcbEventStore","state","appendCondition","buildDecisionModel","newEvents","append"],"sources":["../../src/decider.ts"],"sourcesContent":[null],"mappings":";;;;;;AA8DA,IAAAA,KAAA,GAAAC,OAAA;AAEA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,cAAA,GAAAF,OAAA;AAjEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DO,MAAMG,OAAO,GAAGA,CASrBC,WAAoC,EACpCC,MAA2D,MAO1D;EAAED,WAAW;EAAEC;AAAM,CAAE,CAKvB;AAEH;;;;;;;;;;;;;AAAAC,OAAA,CAAAH,OAAA,GAAAA,OAAA;AAaO,MAAMI,UAAU,GAAGA,CAMxBJ,OAAkC,EAClCK,OAAgB,KAMhBC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOC,mBAAa;EAClC,MAAM;IAAEC,KAAK;IAAEC;EAAe,CAAE,GAAG,OAAO,IAAAC,iCAAkB,EAC1DZ,OAAO,CAACC,WAAW,CAACI,OAAO,CAAC,CAC7B;EACD,MAAMQ,SAAS,GAAG,OAAOb,OAAO,CAACE,MAAM,CAACQ,KAAK,EAAEL,OAAO,CAAC;EACvD,OAAO,OAAOG,KAAK,CAACM,MAAM,CAACD,SAAS,EAAEF,eAAe,CAAC;AACxD,CAAC,CAAC;AAAAR,OAAA,CAAAC,UAAA,GAAAA,UAAA","ignoreList":[]}
1
+ {"version":3,"file":"decider.js","names":["_core","require","_effect","_decision","_decisionModel","decider","projections","decide","runDecider","command","Effect","gen","store","DcbEventStore","state","appendCondition","buildDecisionModel","output","isUnchanged","append","exports"],"sources":["../../src/decider.ts"],"sourcesContent":[null],"mappings":";;;;;;;AAwEA,IAAAA,KAAA,GAAAC,OAAA;AAEA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,SAAA,GAAAF,OAAA;AAKA,IAAAG,cAAA,GAAAH,OAAA;AAhFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoOM,SAAUI,OAAOA,CACrBC,WAAoC,EACpCC,MAGoD;EAEpD,OAAO;IAAED,WAAW;IAAEC;EAAM,CAM3B;AACH;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBO,MAAMC,UAAU,GAAGA,CAOxBH,OAAqC,EACrCI,OAAgB,KAMhBC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOC,mBAAa;EAClC,MAAM;IAAEC,KAAK;IAAEC;EAAe,CAAE,GAAG,OAAO,IAAAC,iCAAkB,EAC1DX,OAAO,CAACC,WAAW,CAACG,OAAO,CAAC,CAC7B;EACD,MAAMQ,MAAM,GAAmB,OAAOZ,OAAO,CAACE,MAAM,CAACO,KAAK,EAAEL,OAAO,CAAC;EACpE;EACA;EACA;EACA;EACA,IAAI,IAAAS,qBAAW,EAACD,MAAM,CAAC,EAAE;IACvB,OAAOA,MAA2B;EACpC;EACA,OAAQ,OAAOL,KAAK,CAACO,MAAM,CAACF,MAAM,EAAEF,eAAe,CAAC;AACtD,CAAC,CAAC;AAAAK,OAAA,CAAAZ,UAAA,GAAAA,UAAA","ignoreList":[]}
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isUnchanged = exports.UnchangedTypeId = exports.Unchanged = exports.Decision = void 0;
7
+ var _effect = require("effect");
8
+ /**
9
+ * The type id `isUnchanged` recognises. `Symbol.for`, so both halves of the dual
10
+ * build — and any two copies of this package in one graph — agree on it.
11
+ */
12
+ const UnchangedTypeId = exports.UnchangedTypeId = /*#__PURE__*/Symbol.for('@kairos-es/codec/Unchanged');
13
+ /**
14
+ * "Nothing to do: this work is already done." A runner that receives it appends
15
+ * nothing and returns it in place of a `Position`.
16
+ *
17
+ * `reason` is for a human reading a trace or a test failure, and nothing in the
18
+ * library reads it. A `Data.TaggedClass`, so two values with the same reason are
19
+ * `Equal` and a test can compare them structurally.
20
+ *
21
+ * ⚠ Test for it with `isUnchanged`, never with `instanceof` or `_tag` — the
22
+ * module documentation says why each of those is wrong.
23
+ */
24
+ class Unchanged extends /*#__PURE__*/_effect.Data.TaggedClass('Unchanged') {
25
+ [UnchangedTypeId] = UnchangedTypeId;
26
+ }
27
+ /**
28
+ * Whether `u` is an `Unchanged`, by its type id. Total over `unknown`, and
29
+ * realm-safe across the dual ESM/CJS build. An application value that merely
30
+ * carries `_tag: 'Unchanged'` answers `false`, deliberately.
31
+ */
32
+ exports.Unchanged = Unchanged;
33
+ const isUnchanged = u => _effect.Predicate.hasProperty(u, UnchangedTypeId);
34
+ /**
35
+ * The constructors a `decide` step reaches for. One member today; it is a
36
+ * namespace object rather than a bare function so the call site reads as what it
37
+ * is, `Decision.unchanged('already dispatched')`.
38
+ */
39
+ exports.isUnchanged = isUnchanged;
40
+ const Decision = exports.Decision = {
41
+ unchanged: reason => new Unchanged(reason === undefined ? {} : {
42
+ reason
43
+ })
44
+ };
45
+ //# sourceMappingURL=decision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision.js","names":["_effect","require","UnchangedTypeId","exports","Symbol","for","Unchanged","Data","TaggedClass","isUnchanged","u","Predicate","hasProperty","Decision","unchanged","reason","undefined"],"sources":["../../src/decision.ts"],"sourcesContent":[null],"mappings":";;;;;;AA4FA,IAAAA,OAAA,GAAAC,OAAA;AAEA;;;;AAIO,MAAMC,eAAe,GAAAC,OAAA,CAAAD,eAAA,gBAAkBE,MAAM,CAACC,GAAG,CACtD,4BAA4B,CAC7B;AAKD;;;;;;;;;;;AAWM,MAAOC,SAAU,sBAAQC,YAAI,CAACC,WAAW,CAAC,WAAW,CAEzD;EACS,CAACN,eAAe,IAAqBA,eAAe;;AAG/D;;;;;AAAAC,OAAA,CAAAG,SAAA,GAAAA,SAAA;AAKO,MAAMG,WAAW,GAAIC,CAAU,IACpCC,iBAAS,CAACC,WAAW,CAACF,CAAC,EAAER,eAAe,CAAC;AAE3C;;;;;AAAAC,OAAA,CAAAM,WAAA,GAAAA,WAAA;AAKO,MAAMI,QAAQ,GAAAV,OAAA,CAAAU,QAAA,GAEjB;EACFC,SAAS,EAAGC,MAAM,IAAK,IAAIT,SAAS,CAACS,MAAM,KAAKC,SAAS,GAAG,EAAE,GAAG;IAAED;EAAM,CAAE;CAC5E","ignoreList":[]}
@@ -7,6 +7,7 @@ exports.modelAtHead = exports.buildDecisionModel = void 0;
7
7
  var _core = require("@kairos-es/core");
8
8
  var _effect = require("effect");
9
9
  var _decode = require("./decode.js");
10
+ var _readLedger = require("./readLedger.js");
10
11
  /**
11
12
  * Read-and-fold over a slice record: read the composed query, decode each event
12
13
  * through the matching def's `Schema` in an effectful `Stream.mapEffect` stage,
@@ -16,11 +17,18 @@ var _decode = require("./decode.js");
16
17
  * `modelAtHead` stops where the fold does, at `{ state, head }` — a
17
18
  * materialisation of the record with no table, no checkpoint and no runner.
18
19
  * `buildDecisionModel` adds the only part that is genuinely decision-specific:
19
- * the `AppendCondition` derived from the same query and that same head (the DCB
20
- * optimistic-concurrency contract). It is core's old
21
- * `buildDecisionModel(Record<string, AnyProjection>)` surface, moved here
20
+ * the `AppendCondition` (the DCB optimistic-concurrency contract). It is core's
21
+ * old `buildDecisionModel(Record<string, AnyProjection>)` surface, moved here
22
22
  * because decode needs `Schema` + the `Serializer`.
23
23
  *
24
+ * BOTH exports also record into the ambient read ledger when one is in context,
25
+ * which is what makes a decision that walks a chain of identifiers guard on every
26
+ * hop rather than on its last. Recording is `modelAtHead`'s, so it happens
27
+ * whatever symbol performed the read; outside a bracket there is no ledger,
28
+ * nothing is recorded and no behaviour changes. `readLedger.ts` owns the whole
29
+ * argument, including why the recorder is this module rather than a
30
+ * layered-only entry point of its own.
31
+ *
24
32
  * The fold stays pure and in core; only the decode stage is effectful, and that
25
33
  * stage lives in `decode.ts` — `decodeSlices` — so the read side folds a live
26
34
  * `subscribe` stream through the *same* decode this module folds a bounded
@@ -58,6 +66,16 @@ var _decode = require("./decode.js");
58
66
  * single fold cannot serve — across many identities, ordered, joined, paginated
59
67
  * — and never because it is fresher, because it is not.
60
68
  *
69
+ * ⚠ AND INSIDE A BRACKET IT IS NOT THE FLOOR THE APPEND WILL USE. Everything
70
+ * above is a statement about THIS read, and it stays true; what changes is that a
71
+ * bracket guards on the EARLIEST head across every hop it walked, which on any hop
72
+ * after the first is a strictly lower number than the one returned here. So a
73
+ * caller who reads `head` inside a bracket and reasons about what the append will
74
+ * refuse is reasoning from the wrong figure — the guard is wider than this head
75
+ * says. Nothing here is wrong; the trap is assuming the two are the same number,
76
+ * and it is named because it is exactly the assumption that holds outside a
77
+ * bracket and quietly stops holding inside one. `readLedger.ts` owns the floor.
78
+ *
61
79
  * `head` is LOG-GLOBAL: it is the head the store's `read` returned, so it moves
62
80
  * for an event no slice here selects. `test/decisionModel.test.ts` pins that
63
81
  * behaviour EXPLICITLY, over four appended events one of which the composed query
@@ -89,12 +107,28 @@ const modelAtHead = projections => _effect.Effect.gen(function* () {
89
107
  const decode = (0, _decode.decodeSlices)(projections);
90
108
  // The pure fold: core composes the record (slices are AnyProjections).
91
109
  const composite = (0, _core.composeProjections)(projections);
110
+ // Both ledger calls below are no-ops outside a bracket and neither touches
111
+ // the requirement channel; `readLedger.ts` owns why recording happens HERE
112
+ // rather than at a layered-only entry point of its own.
113
+ //
114
+ // Refuse an unservable union BEFORE the read is paid for: the question is
115
+ // answerable from the queries alone, so a hop that would kill the append two
116
+ // layers away fails at the read that caused it instead.
117
+ yield* (0, _readLedger.assertServableAddition)(projections);
118
+ // EAGER: the query is filed before a single event is streamed, so a read that
119
+ // opens and never completes still widens the guard. What comes back is the
120
+ // effect that completes the hop; outside a bracket it does nothing.
121
+ const closeHop = yield* (0, _readLedger.recordQuery)(composite.query);
92
122
  const {
93
123
  events,
94
124
  head
95
125
  } = yield* store.read(composite.query);
96
126
  const decoded = decode(events);
97
127
  const state = yield* _effect.Stream.runFold(decoded, composite.initialState, (acc, event) => composite.evolve(acc, event));
128
+ // LAZY: only once the fold has run to completion. A read that failed
129
+ // part-way folded a PREFIX, and a head recorded for it is a claim it did not
130
+ // earn — so the line below is unreachable on that path, deliberately.
131
+ yield* closeHop(head);
98
132
  return {
99
133
  state,
100
134
  head
@@ -113,17 +147,19 @@ const modelAtHead = projections => _effect.Effect.gen(function* () {
113
147
  * of a near-duplicate: `modelAtHead` is the shared part, this function is the
114
148
  * condition and nothing else, and each is shorter than the one function was.
115
149
  *
116
- * `composeProjections` therefore runs TWICE for one command — once inside
117
- * `modelAtHead` for the read, once here for `failIfEventsMatch` — and that is
118
- * deliberate rather than waste. It is a pure derivation of the record, so the two
119
- * queries cannot disagree however either call site changes. The alternative,
120
- * returning the query from `modelAtHead`, would put a third field on a READ-TIME
121
- * API that no read-time caller wants, to save one pure merge over a handful of
122
- * slices; a surface paying for a body's convenience is the worse trade.
150
+ * Outside a bracket `composeProjections` therefore runs twice for one command —
151
+ * once inside `modelAtHead` for the read, once here for `failIfEventsMatch` — and
152
+ * that is deliberate rather than waste. It is a pure derivation of the record, so
153
+ * the two queries cannot disagree however either call site changes. The
154
+ * alternative, returning the query from `modelAtHead`, would put a third field on
155
+ * a READ-TIME API that no read-time caller wants, to save one pure merge over a
156
+ * handful of slices; a surface paying for a body's convenience is the worse
157
+ * trade. Inside a bracket the second merge is not reached at all: the ledger's
158
+ * own derivation composes every recorded hop instead.
123
159
  *
124
160
  * ## How to attribute the read, the decode and the fold — the rule, stated once
125
161
  *
126
- * Because this function is `Effect.map` over `modelAtHead`, prose about the codec
162
+ * Because this function is expressed OVER `modelAtHead`, prose about the codec
127
163
  * splits into two kinds and only one of them may name this symbol:
128
164
  *
129
165
  * - Naming this function as the ENTRY POINT a caller invokes, or as the path
@@ -141,15 +177,35 @@ const modelAtHead = projections => _effect.Effect.gen(function* () {
141
177
  * soften the sentence.
142
178
  */
143
179
  exports.modelAtHead = modelAtHead;
144
- const buildDecisionModel = projections => _effect.Effect.map(modelAtHead(projections), ({
145
- state,
146
- head
147
- }) => ({
148
- state,
149
- appendCondition: {
150
- failIfEventsMatch: (0, _core.composeProjections)(projections).query,
151
- after: head
180
+ const buildDecisionModel = projections => _effect.Effect.gen(function* () {
181
+ const {
182
+ state,
183
+ head
184
+ } = yield* modelAtHead(projections);
185
+ // INSIDE A BRACKET the condition comes from the ledger — every query the
186
+ // bracket walked, at the earliest head observed — and not from this one read.
187
+ // That is the change's whole ergonomic claim: the call whose current
188
+ // behaviour is the hazard becomes CORRECT inside a bracket, with nothing at
189
+ // the call site changing beyond entering one. The cost is that one expression
190
+ // means two things depending on ambient context, and that is the trade taken.
191
+ //
192
+ // `modelAtHead` has just recorded a hop, so inside a bracket the derivation
193
+ // cannot be empty; `None` here means there is no ledger at all.
194
+ const ambient = yield* _readLedger.ambientLayeredCondition;
195
+ if (_effect.Option.isSome(ambient)) {
196
+ return {
197
+ state,
198
+ appendCondition: ambient.value
199
+ };
152
200
  }
153
- }));
201
+ // OUTSIDE a bracket this is byte-for-byte what shipped.
202
+ return {
203
+ state,
204
+ appendCondition: {
205
+ failIfEventsMatch: (0, _core.composeProjections)(projections).query,
206
+ after: head
207
+ }
208
+ };
209
+ });
154
210
  exports.buildDecisionModel = buildDecisionModel;
155
211
  //# sourceMappingURL=decisionModel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"decisionModel.js","names":["_core","require","_effect","_decode","modelAtHead","projections","Effect","gen","store","DcbEventStore","decode","decodeSlices","composite","composeProjections","events","head","read","query","decoded","state","Stream","runFold","initialState","acc","event","evolve","exports","buildDecisionModel","map","appendCondition","failIfEventsMatch","after"],"sources":["../../src/decisionModel.ts"],"sourcesContent":[null],"mappings":";;;;;;AA0BA,IAAAA,KAAA,GAAAC,OAAA;AAQA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AAnCA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CO,MAAMG,WAAW,GACtBC,WAAc,IAMdC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOC,mBAAa;EAElC;EACA;EACA;EACA,MAAMC,MAAM,GAAG,IAAAC,oBAAY,EAACN,WAAW,CAAC;EAExC;EACA,MAAMO,SAAS,GAAG,IAAAC,wBAAkB,EAClCR,WAA4C,CAC7C;EACD,MAAM;IAAES,MAAM;IAAEC;EAAI,CAAE,GAAG,OAAOP,KAAK,CAACQ,IAAI,CAACJ,SAAS,CAACK,KAAK,CAAC;EAE3D,MAAMC,OAAO,GAAGR,MAAM,CAACI,MAAM,CAAC;EAE9B,MAAMK,KAAK,GAAI,OAAOC,cAAM,CAACC,OAAO,CAClCH,OAAO,EACPN,SAAS,CAACU,YAAY,EACtB,CAACC,GAAG,EAAEC,KAAK,KAAKZ,SAAS,CAACa,MAAM,CAACF,GAAG,EAAEC,KAAK,CAAC,CACvB;EAEvB,OAAO;IAAEL,KAAK;IAAEJ;EAAI,CAAE;AACxB,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAW,OAAA,CAAAtB,WAAA,GAAAA,WAAA;AAwCO,MAAMuB,kBAAkB,GAC7BtB,WAAc,IASdC,cAAM,CAACsB,GAAG,CAACxB,WAAW,CAACC,WAAW,CAAC,EAAE,CAAC;EAAEc,KAAK;EAAEJ;AAAI,CAAE,MAAM;EACzDI,KAAK;EACLU,eAAe,EAAE;IACfC,iBAAiB,EAAE,IAAAjB,wBAAkB,EACnCR,WAA4C,CAC7C,CAACY,KAAK;IACPc,KAAK,EAAEhB;;CAEV,CAAC,CAAC;AAAAW,OAAA,CAAAC,kBAAA,GAAAA,kBAAA","ignoreList":[]}
1
+ {"version":3,"file":"decisionModel.js","names":["_core","require","_effect","_decode","_readLedger","modelAtHead","projections","Effect","gen","store","DcbEventStore","decode","decodeSlices","composite","composeProjections","assertServableAddition","closeHop","recordQuery","query","events","head","read","decoded","state","Stream","runFold","initialState","acc","event","evolve","exports","buildDecisionModel","ambient","ambientLayeredCondition","Option","isSome","appendCondition","value","failIfEventsMatch","after"],"sources":["../../src/decisionModel.ts"],"sourcesContent":[null],"mappings":";;;;;;AAiCA,IAAAA,KAAA,GAAAC,OAAA;AAQA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AAGA,IAAAG,WAAA,GAAAH,OAAA;AA7CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDO,MAAMI,WAAW,GACtBC,WAAc,IAMdC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOC,mBAAa;EAElC;EACA;EACA;EACA,MAAMC,MAAM,GAAG,IAAAC,oBAAY,EAACN,WAAW,CAAC;EAExC;EACA,MAAMO,SAAS,GAAG,IAAAC,wBAAkB,EAClCR,WAA4C,CAC7C;EAED;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAAO,IAAAS,kCAAsB,EAACT,WAA4C,CAAC;EAE3E;EACA;EACA;EACA,MAAMU,QAAQ,GAAG,OAAO,IAAAC,uBAAW,EAACJ,SAAS,CAACK,KAAK,CAAC;EAEpD,MAAM;IAAEC,MAAM;IAAEC;EAAI,CAAE,GAAG,OAAOX,KAAK,CAACY,IAAI,CAACR,SAAS,CAACK,KAAK,CAAC;EAE3D,MAAMI,OAAO,GAAGX,MAAM,CAACQ,MAAM,CAAC;EAE9B,MAAMI,KAAK,GAAI,OAAOC,cAAM,CAACC,OAAO,CAClCH,OAAO,EACPT,SAAS,CAACa,YAAY,EACtB,CAACC,GAAG,EAAEC,KAAK,KAAKf,SAAS,CAACgB,MAAM,CAACF,GAAG,EAAEC,KAAK,CAAC,CACvB;EAEvB;EACA;EACA;EACA,OAAOZ,QAAQ,CAACI,IAAI,CAAC;EAErB,OAAO;IAAEG,KAAK;IAAEH;EAAI,CAAE;AACxB,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAU,OAAA,CAAAzB,WAAA,GAAAA,WAAA;AA0CO,MAAM0B,kBAAkB,GAC7BzB,WAAc,IASdC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAM;IAAEe,KAAK;IAAEH;EAAI,CAAE,GAAG,OAAOf,WAAW,CAACC,WAAW,CAAC;EAEvD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAM0B,OAAO,GAAG,OAAOC,mCAAuB;EAC9C,IAAIC,cAAM,CAACC,MAAM,CAACH,OAAO,CAAC,EAAE;IAC1B,OAAO;MAAET,KAAK;MAAEa,eAAe,EAAEJ,OAAO,CAACK;IAAK,CAAE;EAClD;EAEA;EACA,OAAO;IACLd,KAAK;IACLa,eAAe,EAAE;MACfE,iBAAiB,EAAE,IAAAxB,wBAAkB,EACnCR,WAA4C,CAC7C,CAACY,KAAK;MACPqB,KAAK,EAAEnB;;GAEV;AACH,CAAC,CAAC;AAAAU,OAAA,CAAAC,kBAAA,GAAAA,kBAAA","ignoreList":[]}
package/dist/cjs/index.js CHANGED
@@ -9,6 +9,24 @@ Object.defineProperty(exports, "CodecError", {
9
9
  return _errors.CodecError;
10
10
  }
11
11
  });
12
+ Object.defineProperty(exports, "Decision", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _decision.Decision;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "NoReadsRecorded", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _readLedger.NoReadsRecorded;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "ReadLedger", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _readLedger.ReadLedger;
28
+ }
29
+ });
12
30
  Object.defineProperty(exports, "Serializer", {
13
31
  enumerable: true,
14
32
  get: function () {
@@ -21,6 +39,24 @@ Object.defineProperty(exports, "SerializerDefault", {
21
39
  return _Serializer.SerializerDefault;
22
40
  }
23
41
  });
42
+ Object.defineProperty(exports, "TaglessSliceInLedger", {
43
+ enumerable: true,
44
+ get: function () {
45
+ return _readLedger.TaglessSliceInLedger;
46
+ }
47
+ });
48
+ Object.defineProperty(exports, "Unchanged", {
49
+ enumerable: true,
50
+ get: function () {
51
+ return _decision.Unchanged;
52
+ }
53
+ });
54
+ Object.defineProperty(exports, "UnchangedTypeId", {
55
+ enumerable: true,
56
+ get: function () {
57
+ return _decision.UnchangedTypeId;
58
+ }
59
+ });
24
60
  Object.defineProperty(exports, "UnknownEventType", {
25
61
  enumerable: true,
26
62
  get: function () {
@@ -51,6 +87,12 @@ Object.defineProperty(exports, "buildDecisionModel", {
51
87
  return _decisionModel.buildDecisionModel;
52
88
  }
53
89
  });
90
+ Object.defineProperty(exports, "decideOnly", {
91
+ enumerable: true,
92
+ get: function () {
93
+ return _precondition.decideOnly;
94
+ }
95
+ });
54
96
  Object.defineProperty(exports, "decider", {
55
97
  enumerable: true,
56
98
  get: function () {
@@ -69,6 +111,12 @@ Object.defineProperty(exports, "event", {
69
111
  return _event.event;
70
112
  }
71
113
  });
114
+ Object.defineProperty(exports, "isUnchanged", {
115
+ enumerable: true,
116
+ get: function () {
117
+ return _decision.isUnchanged;
118
+ }
119
+ });
72
120
  Object.defineProperty(exports, "makeRegistry", {
73
121
  enumerable: true,
74
122
  get: function () {
@@ -99,12 +147,27 @@ Object.defineProperty(exports, "toEvent", {
99
147
  return _toEvent.toEvent;
100
148
  }
101
149
  });
150
+ Object.defineProperty(exports, "transact", {
151
+ enumerable: true,
152
+ get: function () {
153
+ return _precondition.transact;
154
+ }
155
+ });
156
+ Object.defineProperty(exports, "unsafeAppend", {
157
+ enumerable: true,
158
+ get: function () {
159
+ return _precondition.unsafeAppend;
160
+ }
161
+ });
102
162
  var _decider = require("./decider.js");
163
+ var _decision = require("./decision.js");
103
164
  var _decisionModel = require("./decisionModel.js");
104
165
  var _decode = require("./decode.js");
105
166
  var _errors = require("./errors.js");
106
167
  var _event = require("./event.js");
168
+ var _precondition = require("./precondition.js");
107
169
  var _projection = require("./projection.js");
170
+ var _readLedger = require("./readLedger.js");
108
171
  var _registry = require("./registry.js");
109
172
  var _Serializer = require("./Serializer.js");
110
173
  var _toEvent = require("./toEvent.js");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["_decider","require","_decisionModel","_decode","_errors","_event","_projection","_registry","_Serializer","_toEvent","_Uuid"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,IAAAA,QAAA,GAAAC,OAAA;AACA,IAAAC,cAAA,GAAAD,OAAA;AAKA,IAAAE,OAAA,GAAAF,OAAA;AACA,IAAAG,OAAA,GAAAH,OAAA;AACA,IAAAI,MAAA,GAAAJ,OAAA;AACA,IAAAK,WAAA,GAAAL,OAAA;AAKA,IAAAM,SAAA,GAAAN,OAAA;AACA,IAAAO,WAAA,GAAAP,OAAA;AACA,IAAAQ,QAAA,GAAAR,OAAA;AACA,IAAAS,KAAA,GAAAT,OAAA","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["_decider","require","_decision","_decisionModel","_decode","_errors","_event","_precondition","_projection","_readLedger","_registry","_Serializer","_toEvent","_Uuid"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuHA,IAAAA,QAAA,GAAAC,OAAA;AACA,IAAAC,SAAA,GAAAD,OAAA;AAQA,IAAAE,cAAA,GAAAF,OAAA;AAKA,IAAAG,OAAA,GAAAH,OAAA;AACA,IAAAI,OAAA,GAAAJ,OAAA;AACA,IAAAK,MAAA,GAAAL,OAAA;AACA,IAAAM,aAAA,GAAAN,OAAA;AAMA,IAAAO,WAAA,GAAAP,OAAA;AAKA,IAAAQ,WAAA,GAAAR,OAAA;AAOA,IAAAS,SAAA,GAAAT,OAAA;AACA,IAAAU,WAAA,GAAAV,OAAA;AACA,IAAAW,QAAA,GAAAX,OAAA;AACA,IAAAY,KAAA,GAAAZ,OAAA","ignoreList":[]}