@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.
- package/README.md +17 -1
- package/dist/cjs/decider.js +35 -67
- package/dist/cjs/decider.js.map +1 -1
- package/dist/cjs/decision.js +45 -0
- package/dist/cjs/decision.js.map +1 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/precondition.js +20 -3
- package/dist/cjs/precondition.js.map +1 -1
- package/dist/dts/decider.d.ts +47 -6
- package/dist/dts/decider.d.ts.map +1 -1
- package/dist/dts/decision.d.ts +161 -0
- package/dist/dts/decision.d.ts.map +1 -0
- package/dist/dts/index.d.ts +18 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/precondition.d.ts +26 -3
- package/dist/dts/precondition.d.ts.map +1 -1
- package/dist/esm/decider.js +33 -65
- package/dist/esm/decider.js.map +1 -1
- package/dist/esm/decision.js +37 -0
- package/dist/esm/decision.js.map +1 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/precondition.js +20 -3
- package/dist/esm/precondition.js.map +1 -1
- package/package.json +2 -2
- package/src/decider.ts +87 -16
- package/src/decision.ts +174 -0
- package/src/index.ts +25 -0
- package/src/precondition.ts +46 -8
|
@@ -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"}
|
package/dist/dts/index.d.ts
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 { 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';
|
package/dist/dts/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
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
|
|
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
|
|
1
|
+
{"version":3,"file":"precondition.d.ts","sourceRoot":"","sources":["../../src/precondition.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/decider.js
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
148
|
-
|
|
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
|
package/dist/esm/decider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decider.js","names":["DcbEventStore","Effect","buildDecisionModel","decider","projections","decide","runDecider","command","gen","store","state","appendCondition","
|
|
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";
|
package/dist/esm/index.js.map
CHANGED
|
@@ -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
|
|
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":[]}
|
package/dist/esm/precondition.js
CHANGED
|
@@ -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
|
|
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(
|
|
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":"
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
226
|
-
|
|
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
|
})
|