@kairos-es/core 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +33 -0
  3. package/dist/cjs/AppendCondition.js +26 -0
  4. package/dist/cjs/AppendCondition.js.map +1 -0
  5. package/dist/cjs/ContractStore.js +80 -0
  6. package/dist/cjs/ContractStore.js.map +1 -0
  7. package/dist/cjs/DcbEventStore.js +10 -0
  8. package/dist/cjs/DcbEventStore.js.map +1 -0
  9. package/dist/cjs/Event.js +89 -0
  10. package/dist/cjs/Event.js.map +1 -0
  11. package/dist/cjs/Projection.js +73 -0
  12. package/dist/cjs/Projection.js.map +1 -0
  13. package/dist/cjs/Query.js +221 -0
  14. package/dist/cjs/Query.js.map +1 -0
  15. package/dist/cjs/SubscribeMachine.js +235 -0
  16. package/dist/cjs/SubscribeMachine.js.map +1 -0
  17. package/dist/cjs/errors.js +22 -0
  18. package/dist/cjs/errors.js.map +1 -0
  19. package/dist/cjs/inMemory.js +191 -0
  20. package/dist/cjs/inMemory.js.map +1 -0
  21. package/dist/cjs/index.js +124 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/retryOnConflict.js +95 -0
  24. package/dist/cjs/retryOnConflict.js.map +1 -0
  25. package/dist/cjs/testing.js +54 -0
  26. package/dist/cjs/testing.js.map +1 -0
  27. package/dist/dts/AppendCondition.d.ts +23 -0
  28. package/dist/dts/AppendCondition.d.ts.map +1 -0
  29. package/dist/dts/ContractStore.d.ts +56 -0
  30. package/dist/dts/ContractStore.d.ts.map +1 -0
  31. package/dist/dts/DcbEventStore.d.ts +64 -0
  32. package/dist/dts/DcbEventStore.d.ts.map +1 -0
  33. package/dist/dts/Event.d.ts +104 -0
  34. package/dist/dts/Event.d.ts.map +1 -0
  35. package/dist/dts/Projection.d.ts +108 -0
  36. package/dist/dts/Projection.d.ts.map +1 -0
  37. package/dist/dts/Query.d.ts +145 -0
  38. package/dist/dts/Query.d.ts.map +1 -0
  39. package/dist/dts/SubscribeMachine.d.ts +179 -0
  40. package/dist/dts/SubscribeMachine.d.ts.map +1 -0
  41. package/dist/dts/errors.d.ts +17 -0
  42. package/dist/dts/errors.d.ts.map +1 -0
  43. package/dist/dts/inMemory.d.ts +76 -0
  44. package/dist/dts/inMemory.d.ts.map +1 -0
  45. package/dist/dts/index.d.ts +35 -0
  46. package/dist/dts/index.d.ts.map +1 -0
  47. package/dist/dts/retryOnConflict.d.ts +70 -0
  48. package/dist/dts/retryOnConflict.d.ts.map +1 -0
  49. package/dist/dts/testing.d.ts +39 -0
  50. package/dist/dts/testing.d.ts.map +1 -0
  51. package/dist/esm/AppendCondition.js +19 -0
  52. package/dist/esm/AppendCondition.js.map +1 -0
  53. package/dist/esm/ContractStore.js +73 -0
  54. package/dist/esm/ContractStore.js.map +1 -0
  55. package/dist/esm/DcbEventStore.js +3 -0
  56. package/dist/esm/DcbEventStore.js.map +1 -0
  57. package/dist/esm/Event.js +81 -0
  58. package/dist/esm/Event.js.map +1 -0
  59. package/dist/esm/Projection.js +66 -0
  60. package/dist/esm/Projection.js.map +1 -0
  61. package/dist/esm/Query.js +209 -0
  62. package/dist/esm/Query.js.map +1 -0
  63. package/dist/esm/SubscribeMachine.js +227 -0
  64. package/dist/esm/SubscribeMachine.js.map +1 -0
  65. package/dist/esm/errors.js +14 -0
  66. package/dist/esm/errors.js.map +1 -0
  67. package/dist/esm/inMemory.js +184 -0
  68. package/dist/esm/inMemory.js.map +1 -0
  69. package/dist/esm/index.js +38 -0
  70. package/dist/esm/index.js.map +1 -0
  71. package/dist/esm/package.json +4 -0
  72. package/dist/esm/retryOnConflict.js +87 -0
  73. package/dist/esm/retryOnConflict.js.map +1 -0
  74. package/dist/esm/testing.js +46 -0
  75. package/dist/esm/testing.js.map +1 -0
  76. package/package.json +39 -0
  77. package/src/AppendCondition.ts +18 -0
  78. package/src/ContractStore.ts +148 -0
  79. package/src/DcbEventStore.ts +77 -0
  80. package/src/Event.ts +135 -0
  81. package/src/Projection.ts +168 -0
  82. package/src/Query.ts +237 -0
  83. package/src/SubscribeMachine.ts +316 -0
  84. package/src/errors.ts +19 -0
  85. package/src/inMemory.ts +356 -0
  86. package/src/index.ts +37 -0
  87. package/src/retryOnConflict.ts +100 -0
  88. package/src/testing.ts +47 -0
  89. package/testing/package.json +6 -0
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.retryOnConflict = void 0;
7
+ var _effect = require("effect");
8
+ /**
9
+ * `retryOnConflict` — the decider-level retry for `AppendConditionFailed`
10
+ * (ADR-0008).
11
+ *
12
+ * `append` surfaces a conflict as a SIGNAL only: a tagged `AppendConditionFailed`
13
+ * flattened to the tripped condition, carrying no conflicting position,
14
+ * deliberately so that a retry cannot resume from a stale conflict point. A
15
+ * correct DCB retry must therefore re-derive its append-condition lower bound
16
+ * from a FRESH READ to the current head — which means re-running the whole
17
+ * read-decide-append cycle, not re-submitting the same events.
18
+ *
19
+ * That is why this is a combinator over a command and NOT a `Layer` decorating
20
+ * `DcbEventStore.append`. A decorator sees only the events and the condition; it
21
+ * cannot re-read the log and it cannot re-decide, so it is STRUCTURALLY incapable
22
+ * of a correct DCB retry — the only thing it could do is re-submit against a
23
+ * condition that is already known to be stale, which either fails again forever
24
+ * or, worse, succeeds against a boundary the decision was never made under. No
25
+ * retrying `Layer` is shipped, and if a production command-runner service ever
26
+ * emerges, a `Layer` over THAT (a whole cycle) is the shape to add.
27
+ *
28
+ * It lives HERE, in `@kairos-es/core`, because conflict retry is a WRITE-side
29
+ * concern and core is where the write side is defined: core declares the `append`
30
+ * contract, defines the `AppendConditionFailed` this combinator matches, and
31
+ * already peers `effect` for `Schedule`, so the move costs nothing and adds no
32
+ * dependency in either direction. It shipped from `@kairos-es/read` first on the
33
+ * argument that it added no peer surface there either — true, but an argument for
34
+ * where it MAY live, not where it SHOULD. What that home actually cost was a
35
+ * write-only consumer having to depend on the READ package to retry its own
36
+ * appends, for a combinator that touches no projection, no checkpoint and no view
37
+ * (ADR-0008, homing amended).
38
+ *
39
+ * Core stays pure with it here. `Schedule`'s driver reads the time through
40
+ * `Clock.currentTimeMillis` and sleeps through `Clock.sleep`, both resolved from
41
+ * the fibre's default services, so `TestClock` still governs every delay; the
42
+ * jitter draws from `Random.next`, a default service on the same footing as the
43
+ * clock rather than a host `Math.random`. No `node:*`, no wall-clock reading, no
44
+ * ungoverned RNG.
45
+ *
46
+ * Effect v3 caveat: the `Schedule` API below is the THREE-type-parameter
47
+ * `Schedule<Out, In, R>` that `effect@3` exports, with `Schedule.intersect`. The
48
+ * currently-published docs show a newer four-type-parameter `Schedule` without
49
+ * `intersect` — do not copy them; this file is written against the installed
50
+ * `effect@3` type definitions.
51
+ */
52
+
53
+ /**
54
+ * The default conflict backoff: exponential from 20ms, jittered, capped at five
55
+ * retries.
56
+ *
57
+ * `Schedule.intersect` continues only while BOTH schedules continue, so the
58
+ * `recurs(5)` arm is what bounds the attempts and the exponential arm is what
59
+ * spaces them. Jitter matters more here than in a typical backoff: contending
60
+ * deciders conflict precisely because they read the same boundary at the same
61
+ * time, so an unjittered backoff would re-synchronise them into the same conflict
62
+ * on every round. The figures are the conventional starting point (Emmett,
63
+ * Kurrent and Dudycz all land in this neighbourhood) and are a DEFAULT — a
64
+ * command with a tighter latency budget, or one contending harder, should pass its
65
+ * own schedule.
66
+ */
67
+ const conflictBackoff = /*#__PURE__*/_effect.Schedule.intersect(/*#__PURE__*/_effect.Schedule.jittered(/*#__PURE__*/_effect.Schedule.exponential('20 millis', 2)), /*#__PURE__*/_effect.Schedule.recurs(5));
68
+ /**
69
+ * Retry `command` while — and only while — it fails `AppendConditionFailed`.
70
+ *
71
+ * `command` must be the WHOLE read-decide-append cycle (read a decision model to
72
+ * the current head, decide, append under that read's condition), because each
73
+ * retry's correctness comes from the re-read: the fresh read re-derives the
74
+ * append condition's `after` from the new head, so the retried append is judged
75
+ * against the boundary the retried decision was actually made under. Wrapping only
76
+ * the `append` re-submits a stale condition and is wrong.
77
+ *
78
+ * A non-conflict `E` is preserved on the error channel and escapes IMMEDIATELY
79
+ * with zero retries: the `while` predicate is false for it, so a domain rejection
80
+ * ("course is full") never pays a backoff and never retries into a decision the
81
+ * log has already refused. `A`, the non-conflict `E`, and `R` all pass through
82
+ * untouched.
83
+ *
84
+ * The predicate matches on the `_tag` rather than `instanceof`. The tag is the
85
+ * error's identity across module realms, and a published package ships dual
86
+ * ESM/CJS, so an `instanceof` check can see two distinct classes for the same
87
+ * error when a consumer's graph loads both builds — a conflict would then escape
88
+ * unretried and silently.
89
+ */
90
+ const retryOnConflict = (command, schedule = conflictBackoff) => _effect.Effect.retry(command, {
91
+ schedule,
92
+ while: _effect.Predicate.isTagged('AppendConditionFailed')
93
+ });
94
+ exports.retryOnConflict = retryOnConflict;
95
+ //# sourceMappingURL=retryOnConflict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retryOnConflict.js","names":["_effect","require","conflictBackoff","Schedule","intersect","jittered","exponential","recurs","retryOnConflict","command","schedule","Effect","retry","while","Predicate","isTagged","exports"],"sources":["../../src/retryOnConflict.ts"],"sourcesContent":[null],"mappings":";;;;;;AA4CA,IAAAA,OAAA,GAAAC,OAAA;AA5CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA;;;;;;;;;;;;;;AAcA,MAAMC,eAAe,gBAAGC,gBAAQ,CAACC,SAAS,cACxCD,gBAAQ,CAACE,QAAQ,cAACF,gBAAQ,CAACG,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,eACvDH,gBAAQ,CAACI,MAAM,CAAC,CAAC,CAAC,CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;AAsBO,MAAMC,eAAe,GAAGA,CAC7BC,OAAuD,EACvDC,QAAA,GAIIR,eAAe,KAEnBS,cAAM,CAACC,KAAK,CAACH,OAAO,EAAE;EACpBC,QAAQ;EACRG,KAAK,EAAEC,iBAAS,CAACC,QAAQ,CAAC,uBAAuB;CAClD,CAAC;AAAAC,OAAA,CAAAR,eAAA,GAAAA,eAAA","ignoreList":[]}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.makeSequentialUuid = exports.EPOCH = void 0;
7
+ var _effect = require("effect");
8
+ /**
9
+ * Deterministic test-support utilities, shared across every kairos-es suite via
10
+ * the published `@kairos-es/core/testing` subpath.
11
+ *
12
+ * WHY a published subpath rather than a test-only package: three suites (core's
13
+ * own tests, the store contract suite, and the codec's `UuidTest` layer) each
14
+ * carried a byte-identical copy of the same counter-based uuid scheme and the
15
+ * `EPOCH` domain time. Copies drift. The one place every consumer already
16
+ * depends on is `@kairos-es/core`, so the single source of truth lives here as a
17
+ * *published* subpath — never a runtime dependency from a published package onto
18
+ * an unpublished/test-only one, which would create a packaging cycle.
19
+ *
20
+ * Everything here is PURE and DETERMINISTIC: no RNG, no wall-clock. That is the
21
+ * whole point — suites must produce the same bytes and positions on every run,
22
+ * so `data`/`uuid`/`occurredAt` never introduce nondeterminism into assertions.
23
+ */
24
+
25
+ /**
26
+ * A fixed domain time (`DateTime` at the Unix epoch) for test events.
27
+ *
28
+ * Ordering in the log is by `position` alone, so an event's `occurredAt` is
29
+ * informational; pinning it to the epoch keeps serialised bytes and comparisons
30
+ * stable across runs. Built via `DateTime.unsafeMake(0)` (the value is a
31
+ * compile-time constant, not read from the host clock).
32
+ */
33
+ const EPOCH = exports.EPOCH = /*#__PURE__*/_effect.DateTime.unsafeMake(0);
34
+ /**
35
+ * Build a fresh, deterministic uuid generator: a zero-based counter producing
36
+ * RFC-4122 v4-shaped ids `00000000-0000-4000-8000-<counter as 12 hex digits>`,
37
+ * starting at `...000000000000` and incrementing on each call.
38
+ *
39
+ * A *factory* (not a shared singleton) so each layer/suite instance owns its own
40
+ * counter and the sequence is repeatable in isolation — two independent suites
41
+ * do not interfere, yet each is stable on its own. No RNG: the ids are
42
+ * comparable and predictable, which is what assertions need, while still being
43
+ * distinct enough to tell events apart.
44
+ */
45
+ const makeSequentialUuid = () => {
46
+ let counter = 0;
47
+ return () => {
48
+ const suffix = counter.toString(16).padStart(12, '0');
49
+ counter += 1;
50
+ return `00000000-0000-4000-8000-${suffix}`;
51
+ };
52
+ };
53
+ exports.makeSequentialUuid = makeSequentialUuid;
54
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","names":["_effect","require","EPOCH","exports","DateTime","unsafeMake","makeSequentialUuid","counter","suffix","toString","padStart"],"sources":["../../src/testing.ts"],"sourcesContent":[null],"mappings":";;;;;;AAgBA,IAAAA,OAAA,GAAAC,OAAA;AAhBA;;;;;;;;;;;;;;;;;AAkBA;;;;;;;;AAQO,MAAMC,KAAK,GAAAC,OAAA,CAAAD,KAAA,gBAAGE,gBAAQ,CAACC,UAAU,CAAC,CAAC,CAAC;AAE3C;;;;;;;;;;;AAWO,MAAMC,kBAAkB,GAAGA,CAAA,KAAqB;EACrD,IAAIC,OAAO,GAAG,CAAC;EACf,OAAO,MAAK;IACV,MAAMC,MAAM,GAAGD,OAAO,CAACE,QAAQ,CAAC,EAAE,CAAC,CAACC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACrDH,OAAO,IAAI,CAAC;IACZ,OAAO,2BAA2BC,MAAM,EAAE;EAC5C,CAAC;AACH,CAAC;AAAAL,OAAA,CAAAG,kBAAA,GAAAA,kBAAA","ignoreList":[]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The optimistic-concurrency guard for `append`. The append fails if any event
3
+ * matching `failIfEventsMatch` exists strictly after `after`. This is the whole
4
+ * DCB consistency mechanism: "fail if anything I based my decision on has
5
+ * changed since I read it."
6
+ *
7
+ * `after` is exact-optional (absent, never `undefined`) per
8
+ * `exactOptionalPropertyTypes`. Absent means "scan the whole log".
9
+ */
10
+ import { Schema } from 'effect';
11
+ export declare const AppendCondition: Schema.Struct<{
12
+ failIfEventsMatch: Schema.Struct<{
13
+ items: Schema.Array$<Schema.Struct<{
14
+ types: Schema.Array$<Schema.brand<Schema.filter<typeof Schema.String>, "EventType">>;
15
+ tags: Schema.Array$<Schema.brand<Schema.filter<typeof Schema.String>, "Tag">>;
16
+ }>>;
17
+ }>;
18
+ after: Schema.optionalWith<Schema.brand<Schema.filter<typeof Schema.BigInt>, "Position">, {
19
+ exact: true;
20
+ }>;
21
+ }>;
22
+ export type AppendCondition = typeof AppendCondition.Type;
23
+ //# sourceMappingURL=AppendCondition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppendCondition.d.ts","sourceRoot":"","sources":["../../src/AppendCondition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAI/B,eAAO,MAAM,eAAe;;;;;;;;;;EAG1B,CAAA;AACF,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAA"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `makeContractStore` — the shared contract-enforcement POLICY every
3
+ * `DcbEventStore` engine wears, factored out of the engines so the policy is
4
+ * DEFINITIONALLY identical across backends rather than identical by inspection.
5
+ *
6
+ * Each engine implements only the MECHANICS (`RawDcbEventStore`): the actual
7
+ * read/append/subscribe, free to surface infrastructure faults on the error
8
+ * channel (`never` for the pure in-memory engine; `SqlError` for either SQL one).
9
+ * The decorator wraps those mechanics with the policy the contract demands — the
10
+ * policy the first two engines hand-duplicated, with the Postgres one even
11
+ * needing a bespoke `catchAll`/`instanceof`:
12
+ *
13
+ * - `assertServableQuery` at every verb's entry (read, the append condition's
14
+ * guard query, subscribe) — a non-servable query is a DEFECT in every engine;
15
+ * - `ReadLimit` decode on `read`'s `limit` — a negative/non-integer limit is a
16
+ * DEFECT (so `read` keeps `E = never`), validated through the branded schema
17
+ * even at an untyped boundary that reaches the store as `unknown`;
18
+ * - `orDie` on read/subscribe infrastructure errors — `read`/`subscribe` are
19
+ * `E = never` on the contract, so a raw fault is a defect, not a channel value;
20
+ * - the `AppendConditionFailed`-else-die classification on append, matched by
21
+ * `_tag` and never by class identity — ONLY the contract conflict rides the
22
+ * error channel; every other fault is a defect.
23
+ *
24
+ * The throws are deferred into the returned Effect/Stream (via `Effect.suspend`
25
+ * / `Effect.sync`), so a validation failure surfaces as a DEFECT WHEN THE EFFECT
26
+ * RUNS — never as a synchronous throw at the call site. This gives the contract
27
+ * suite a single trusted choke point and makes a further backend impossible to get
28
+ * subtly wrong: it supplies mechanics, the policy is inherited whole. That is how
29
+ * the third engine, `@kairos-es/store-sqlite`, acquired the policy — it writes not
30
+ * a line of it.
31
+ */
32
+ import type { Array as Arr, Scope } from 'effect';
33
+ import { Effect, Stream } from 'effect';
34
+ import type { AppendCondition } from './AppendCondition.js';
35
+ import type { DcbEventStore, ReadOptions, ReadResponse } from './DcbEventStore.js';
36
+ import type { DcbEvent, Position, SequencedEvent } from './Event.js';
37
+ import type { AppendConditionFailed } from './errors.js';
38
+ import { type Query } from './Query.js';
39
+ /**
40
+ * The MECHANICS half of a store: the same three verbs as the `DcbEventStore`
41
+ * contract, but free to surface infrastructure faults (`E`) on the error
42
+ * channel. `E` is `never` for a pure engine (in-memory) and e.g. `SqlError` for
43
+ * a database engine. `append` may additionally fail with the contract's
44
+ * `AppendConditionFailed`. `makeContractStore` turns this into the contract.
45
+ */
46
+ export interface RawDcbEventStore<E> {
47
+ readonly read: (query: Query, options?: ReadOptions) => Effect.Effect<ReadResponse, E>;
48
+ readonly append: (events: Arr.NonEmptyReadonlyArray<DcbEvent>, condition?: AppendCondition) => Effect.Effect<Position, AppendConditionFailed | E>;
49
+ readonly subscribe: (query: Query, after?: Position) => Stream.Stream<SequencedEvent, E, Scope.Scope>;
50
+ }
51
+ /**
52
+ * Wrap a raw engine's mechanics with the shared contract-enforcement policy,
53
+ * producing a full `DcbEventStore` service value.
54
+ */
55
+ export declare const makeContractStore: <E>(raw: RawDcbEventStore<E>) => DcbEventStore["Type"];
56
+ //# sourceMappingURL=ContractStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContractStore.d.ts","sourceRoot":"","sources":["../../src/ContractStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,MAAM,EAAqB,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC/E,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AACrD,OAAO,EAAuB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAA;AAKzD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,CACb,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,WAAW,KAClB,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAC3C,SAAS,CAAC,EAAE,eAAe,KACxB,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,CAAC,CAAC,CAAA;IACvD,QAAQ,CAAC,SAAS,EAAE,CAClB,KAAK,EAAE,KAAK,EACZ,KAAK,CAAC,EAAE,QAAQ,KACb,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;CACnD;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,KAAK,gBAAgB,CAAC,CAAC,CAAC,KACvB,aAAa,CAAC,MAAM,CA4ErB,CAAA"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The `DcbEventStore` contract: a `Context.Tag` with no bundled implementation.
3
+ * Consumers supply a `Layer` (the in-memory one lives in this package; each SQL
4
+ * engine is a package of its own — `@kairos-es/store-postgres` and
5
+ * `@kairos-es/store-sqlite`).
6
+ *
7
+ * Head semantics (ported from `popo.py:62-65`):
8
+ * - `read` with no `limit` => `head` = the global last position (ignoring the
9
+ * query filter) if the store is non-empty, else `ORIGIN` (0n).
10
+ * - `read` with a `limit` => `head` = the last *returned* event's position
11
+ * (post-filter, post-limit), or `ORIGIN` if none returned.
12
+ *
13
+ * `append` returns the last new position (read-your-writes). `subscribe` is
14
+ * catch-up then live and requires a `Scope` so the live subscription is cleaned
15
+ * up on scope close.
16
+ */
17
+ import type { Array as Arr, Effect, Scope, Stream } from 'effect';
18
+ import { Context } from 'effect';
19
+ import type { AppendCondition } from './AppendCondition.js';
20
+ import type { DcbEvent, Position, ReadLimit, SequencedEvent } from './Event.js';
21
+ import type { AppendConditionFailed } from './errors.js';
22
+ import type { Query } from './Query.js';
23
+ /** Options for a read: a lower position bound and/or a maximum count. */
24
+ export interface ReadOptions {
25
+ /** Only events strictly after this position (exclusive). Absent = from the start. */
26
+ readonly after?: Position;
27
+ /**
28
+ * Cap the number of events returned (a branded non-negative integer). `0`
29
+ * returns none; absent = all matching events. A negative/non-integer value is
30
+ * a defect at the store boundary — see `ReadLimit`.
31
+ */
32
+ readonly limit?: ReadLimit;
33
+ }
34
+ /**
35
+ * The result of a read: a stream of matching events (ascending by position)
36
+ * plus the consistency `head`.
37
+ */
38
+ export interface ReadResponse {
39
+ readonly events: Stream.Stream<SequencedEvent>;
40
+ readonly head: Position;
41
+ }
42
+ declare const DcbEventStore_base: Context.TagClass<DcbEventStore, "@kairos-es/core/DcbEventStore", {
43
+ /**
44
+ * Read events matching `query` (empty query = all), optionally bounded by
45
+ * `after`/`limit`. See head semantics in the module docs.
46
+ */
47
+ readonly read: (query: Query, options?: ReadOptions) => Effect.Effect<ReadResponse>;
48
+ /**
49
+ * Append one or more events atomically. If `condition` is given, the append
50
+ * fails with `AppendConditionFailed` when its query matches any event after
51
+ * `condition.after`. Returns the last new position.
52
+ */
53
+ readonly append: (events: Arr.NonEmptyReadonlyArray<DcbEvent>, condition?: AppendCondition) => Effect.Effect<Position, AppendConditionFailed>;
54
+ /**
55
+ * Subscribe to matching events: first the backlog after `after` (catch-up),
56
+ * then live events as they are appended. Scoped: the live subscription is
57
+ * released when the scope closes.
58
+ */
59
+ readonly subscribe: (query: Query, after?: Position) => Stream.Stream<SequencedEvent, never, Scope.Scope>;
60
+ }>;
61
+ export declare class DcbEventStore extends DcbEventStore_base {
62
+ }
63
+ export {};
64
+ //# sourceMappingURL=DcbEventStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DcbEventStore.d.ts","sourceRoot":"","sources":["../../src/DcbEventStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAChC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;IAC9C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CACxB;;IAKG;;;OAGG;mBACY,CACb,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,WAAW,KAClB,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;IAEhC;;;;OAIG;qBACc,CACf,MAAM,EAAE,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAC3C,SAAS,CAAC,EAAE,eAAe,KACxB,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IAEnD;;;;OAIG;wBACiB,CAClB,KAAK,EAAE,KAAK,EACZ,KAAK,CAAC,EAAE,QAAQ,KACb,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;;AA9B1D,qBAAa,aAAc,SAAQ,kBAgChC;CAAG"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Core domain types for the DCB event log: branded scalars and the wire
3
+ * `DcbEvent`/`SequencedEvent` shapes.
4
+ *
5
+ * `data` is opaque bytes at the store layer; the codec (`@kairos-es/codec`) turns typed
6
+ * payloads into these bytes. Ordering is by `position` alone; `occurredAt` is
7
+ * producer-supplied domain time and is informational only.
8
+ */
9
+ import { Schema } from 'effect';
10
+ /**
11
+ * A position in the single global event log: a branded, non-negative `bigint`.
12
+ *
13
+ * The log is 64-bit (`bigserial` in Postgres) and exceeds JS `number` safety at
14
+ * 2^53, so positions are a `bigint`. They are compared, never arithmetic'd.
15
+ * `Schema.BigInt` encodes to a decimal string (JSON-safe) and decodes to
16
+ * `bigint`. `0n` is the "before the first event" sentinel used by `after` and by
17
+ * an empty store's head.
18
+ */
19
+ export declare const Position: Schema.brand<Schema.filter<typeof Schema.BigInt>, "Position">;
20
+ export type Position = typeof Position.Type;
21
+ /** The sentinel position meaning "before the first event". */
22
+ export declare const ORIGIN: Position;
23
+ /**
24
+ * A business identifier attached to an event, following the `key:value`
25
+ * convention (e.g. `student:s1`). Tags replace the per-stream id of classic
26
+ * event sourcing; a query matches an event when the event carries all of an
27
+ * item's tags (superset test).
28
+ */
29
+ export declare const Tag: Schema.brand<Schema.filter<typeof Schema.String>, "Tag">;
30
+ export type Tag = typeof Tag.Type;
31
+ /**
32
+ * A reusable tag identity: a `key` plus a `.make(value)` that composes the
33
+ * branded `Tag` `` `${key}:${value}` `` through core's `Tag` schema. Define an
34
+ * identity once (`const CourseId = tagIdentity('course')`) and reuse
35
+ * `CourseId.make(id)` everywhere that entity is tagged. The `key` is exposed for
36
+ * key-aware tooling (e.g. a tag-vocabulary index, a future retag pass).
37
+ *
38
+ * The same constructor authors an event's tags (in a codec `tagsFor` mapper,
39
+ * ADR-0006) and a query's tags, so the `key:value` string a mapper writes and
40
+ * the string a query matches are produced by one function and cannot drift. It
41
+ * lives here beside `Tag` — pure (no clock, no RNG, no effect) — so it is
42
+ * reachable from every tag-authoring site, including core's own query
43
+ * constructors.
44
+ *
45
+ * `key` and `value` are each a single colon-free atom: the `key:value` grammar
46
+ * splits on the first colon, so both are validated to be non-empty and contain
47
+ * no `:` — `key` once at construction, `value` on each `make`. This keeps
48
+ * composition unambiguous and catches the double-prefix footgun loudly:
49
+ * `CourseId.make('course:123')` throws rather than silently yielding
50
+ * `course:course:123`. Raw `Tag.make` remains the escape hatch for a value that
51
+ * must itself contain a colon.
52
+ */
53
+ export interface TagIdentity {
54
+ readonly key: string;
55
+ readonly make: (value: string) => Tag;
56
+ }
57
+ export declare const tagIdentity: (key: string) => TagIdentity;
58
+ /**
59
+ * A read cap: a branded non-negative integer bounding how many events a `read`
60
+ * returns. `0` returns none; an absent limit returns all matching events.
61
+ *
62
+ * The bound lives in the type, not merely in prose: a negative or non-integer
63
+ * limit is a programming error, so it is a DEFECT at the store boundary — the
64
+ * store validates through this schema and dies rather than surfacing it on the
65
+ * error channel (`read` keeps `E = never`). Constructed at call sites with
66
+ * `ReadLimit.make(n)`.
67
+ */
68
+ export declare const ReadLimit: Schema.brand<Schema.filter<typeof Schema.Int>, "ReadLimit">;
69
+ export type ReadLimit = typeof ReadLimit.Type;
70
+ /**
71
+ * An event type name, a branded non-empty string. Versioning is by convention
72
+ * in the name (e.g. `course-registered.v2`); only non-emptiness is enforced.
73
+ */
74
+ export declare const EventType: Schema.brand<Schema.filter<typeof Schema.String>, "EventType">;
75
+ export type EventType = typeof EventType.Type;
76
+ /**
77
+ * An event as stored: an opaque payload plus its type, tags, a
78
+ * producer-supplied `uuid`, and producer-supplied `occurredAt` domain time.
79
+ *
80
+ * `data` is `Uint8Array` (opaque bytes); the store does not interpret it.
81
+ * `uuid` is producer-supplied to keep `core` deterministic (no RNG in core).
82
+ * `occurredAt` is informational only; ordering is by position alone.
83
+ */
84
+ export declare const DcbEvent: Schema.Struct<{
85
+ type: Schema.brand<Schema.filter<typeof Schema.String>, "EventType">;
86
+ data: typeof Schema.Uint8ArrayFromSelf;
87
+ tags: Schema.Array$<Schema.brand<Schema.filter<typeof Schema.String>, "Tag">>;
88
+ uuid: typeof Schema.UUID;
89
+ occurredAt: typeof Schema.DateTimeUtc;
90
+ }>;
91
+ export type DcbEvent = typeof DcbEvent.Type;
92
+ /** A persisted event: the event together with the position it was written at. */
93
+ export declare const SequencedEvent: Schema.Struct<{
94
+ event: Schema.Struct<{
95
+ type: Schema.brand<Schema.filter<typeof Schema.String>, "EventType">;
96
+ data: typeof Schema.Uint8ArrayFromSelf;
97
+ tags: Schema.Array$<Schema.brand<Schema.filter<typeof Schema.String>, "Tag">>;
98
+ uuid: typeof Schema.UUID;
99
+ occurredAt: typeof Schema.DateTimeUtc;
100
+ }>;
101
+ position: Schema.brand<Schema.filter<typeof Schema.BigInt>, "Position">;
102
+ }>;
103
+ export type SequencedEvent = typeof SequencedEvent.Type;
104
+ //# sourceMappingURL=Event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Event.d.ts","sourceRoot":"","sources":["../../src/Event.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,+DAGpB,CAAA;AACD,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C,8DAA8D;AAC9D,eAAO,MAAM,MAAM,EAAE,QAA4B,CAAA;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,GAAG,0DAGf,CAAA;AACD,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,IAAI,CAAA;AAEjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,CAAA;CACtC;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,WAiBzC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,6DAGrB,CAAA;AACD,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC,IAAI,CAAA;AAE7C;;;GAGG;AACH,eAAO,MAAM,SAAS,gEAGrB,CAAA;AACD,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC,IAAI,CAAA;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;EAMnB,CAAA;AACF,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C,iFAAiF;AACjF,eAAO,MAAM,cAAc;;;;;;;;;EAGzB,CAAA;AACF,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Pure projections and the composition primitive they fold through.
3
+ *
4
+ * A `Projection<S>` = `{ initialState, query, evolve }`: a query selecting the
5
+ * events it cares about and a pure `evolve` folding an already-*decoded* event
6
+ * into state. Decoding (wire bytes -> typed payload via `Schema`) is not core's
7
+ * job — it needs `Schema` and a `Serializer`, which core must not depend on —
8
+ * so `evolve` folds a `DecodedEvent` (the sequenced envelope plus an opaque
9
+ * `payload`). `composeProjections` merges several projections into one whose
10
+ * state is a named record.
11
+ *
12
+ * There is no builder and no decision-model orchestration here: both are the
13
+ * decode-aware `@kairos-es/codec` package's job (its `projection(...)` builder
14
+ * produces schema-backed projections, and its `modelAtHead` performs the effectful
15
+ * decode then folds through `composeProjections`, which `buildDecisionModel` builds
16
+ * on). Core keeps only the pure fold primitives.
17
+ *
18
+ * `Projection<S>` is *invariant* in `S`, so a heterogeneous record of
19
+ * projections (different state per key) cannot be bounded by
20
+ * `Projection<unknown>`. `AnyProjection` is the single authoritative,
21
+ * state-erased bound that owns that unsoundness in one place (built only via
22
+ * `AnyProjection.of`), so no downstream package writes `Projection<any>`; the
23
+ * precise per-key state is recovered by `StateOf` / `CompositeState`.
24
+ *
25
+ * The fold itself is pure and Effect-free.
26
+ */
27
+ import type { SequencedEvent } from './Event.js';
28
+ import type { Query } from './Query.js';
29
+ /**
30
+ * An already-decoded event handed to a projection's fold: the sequenced wire
31
+ * envelope (position, type, tags, timings) plus the decoded `payload`.
32
+ *
33
+ * Core does not decode — it only folds events another layer has already decoded
34
+ * — so `payload` is `unknown` at this layer. `@kairos-es/codec`'s builder emits
35
+ * a projection whose `evolve` narrows `payload` per matched event type.
36
+ */
37
+ export interface DecodedEvent {
38
+ readonly sequenced: SequencedEvent;
39
+ readonly payload: unknown;
40
+ }
41
+ /**
42
+ * A pure projection: the events it reads (`query`), its starting state, and a
43
+ * fold step.
44
+ *
45
+ * `evolve` contract: a projection is only ever invoked with an event that
46
+ * matches its `query`. `composeProjections` enforces this — it gates every
47
+ * sub-projection on its own query before dispatching — so an `evolve` may
48
+ * assume the event matched and need not re-check `matchesSequenced` itself. (It
49
+ * still dispatches by the event's type, which selects the fold; that is fold
50
+ * *selection*, not query gating.) `payload` has already been decoded by the
51
+ * codec layer.
52
+ */
53
+ export interface Projection<S> {
54
+ readonly initialState: S;
55
+ readonly query: Query;
56
+ readonly evolve: (state: S, event: DecodedEvent) => S;
57
+ }
58
+ /**
59
+ * A state-erased view of a `Projection<S>`: the single authoritative bound for
60
+ * a *heterogeneous* record of projections. `Projection<S>` is invariant in `S`
61
+ * (S is both a parameter and the result of `evolve`), so `Record<string,
62
+ * Projection<unknown>>` rejects a record whose keys fold different state types.
63
+ * Rather than leak `Projection<any>` into every package that holds such a
64
+ * record, this type hides the invariant `evolve` behind an erased
65
+ * `(unknown) => unknown` surface and carries `S` only through the covariant
66
+ * `initialState`, so `Record<string, AnyProjection>` is sound with no `any`
67
+ * anywhere. Build one only via `AnyProjection.of`; recover the precise per-key
68
+ * `S` with `StateOf` / `CompositeState`. The `evolve` contract is the same as
69
+ * `Projection`'s: it is only ever called with an event matching `query`
70
+ * (`composeProjections` gates first), so an implementation may assume the match.
71
+ */
72
+ export interface AnyProjection<out S = unknown> {
73
+ readonly initialState: S;
74
+ readonly query: Query;
75
+ readonly evolve: (state: unknown, event: DecodedEvent) => unknown;
76
+ }
77
+ export declare const AnyProjection: {
78
+ /**
79
+ * View a typed `Projection<S>` as an opaque `AnyProjection<S>`. This is the
80
+ * one place the invariant `evolve` is widened to the erased surface, so the
81
+ * unsoundness of a heterogeneous projection record is discharged here and
82
+ * nowhere else. `composeProjections` calls `evolve` with each projection's
83
+ * own state, so the widening is sound at runtime.
84
+ */
85
+ readonly of: <S>(projection: Projection<S>) => AnyProjection<S>;
86
+ };
87
+ /** Recover the state type a projection folds to. */
88
+ export type StateOf<P> = P extends AnyProjection<infer S> ? S : never;
89
+ /**
90
+ * The record of named sub-states a composed projection (or decision model)
91
+ * folds to: each key's precise state type, recovered from its `AnyProjection`.
92
+ */
93
+ export type CompositeState<T extends Record<string, AnyProjection>> = {
94
+ readonly [K in keyof T]: StateOf<T[K]>;
95
+ };
96
+ /**
97
+ * Compose named projections into one whose state is a record of sub-states and
98
+ * whose query is the OR-merge of all sub-queries. Each sub-projection only
99
+ * folds events matching its own query — the guarantee that `Projection.evolve`'s
100
+ * contract lets sub-projections rely on (so they need not re-gate themselves).
101
+ *
102
+ * The fold always writes each *matching* sub-result into a fresh record and
103
+ * returns it (no `changed` short-circuit, no `evolved !== prev` reference
104
+ * check): an in-place-mutating fold that returns the same reference would
105
+ * otherwise be silently dropped. At decision-model scale the extra copy is nil.
106
+ */
107
+ export declare const composeProjections: <T extends Record<string, AnyProjection>>(projections: T) => Projection<CompositeState<T>>;
108
+ //# sourceMappingURL=Projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Projection.d.ts","sourceRoot":"","sources":["../../src/Projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAWpC;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,KAAK,CAAC,CAAA;CACtD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO;IAC5C,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAA;CAClE;AAED,eAAO,MAAM,aAAa;IACxB;;;;;;OAMG;kBACE,CAAC,cAAc,UAAU,CAAC,CAAC,CAAC,KAAG,aAAa,CAAC,CAAC,CAAC;CAE5C,CAAA;AAEV,oDAAoD;AACpD,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAErE;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI;IACpE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACvC,CAAA;AAUD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EACxE,aAAa,CAAC,KACb,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAkC9B,CAAA"}