@kairos-es/core 1.0.0-alpha.4 → 1.0.0-alpha.6

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 (114) hide show
  1. package/README.md +3 -3
  2. package/dist/cjs/AppendCondition.js +1 -3
  3. package/dist/cjs/AppendCondition.js.map +1 -1
  4. package/dist/cjs/AppendIsOwned.js +6 -6
  5. package/dist/cjs/AppendIsOwned.js.map +1 -1
  6. package/dist/cjs/ContractStore.js +2 -2
  7. package/dist/cjs/ContractStore.js.map +1 -1
  8. package/dist/cjs/DcbEventStore.js +1 -1
  9. package/dist/cjs/DcbEventStore.js.map +1 -1
  10. package/dist/cjs/Event.js +29 -13
  11. package/dist/cjs/Event.js.map +1 -1
  12. package/dist/cjs/Query.js +1 -1
  13. package/dist/cjs/Query.js.map +1 -1
  14. package/dist/cjs/SubscribeMachine.js +158 -67
  15. package/dist/cjs/SubscribeMachine.js.map +1 -1
  16. package/dist/cjs/inMemory.js +4 -4
  17. package/dist/cjs/inMemory.js.map +1 -1
  18. package/dist/cjs/retryOnConflict.js +4 -10
  19. package/dist/cjs/retryOnConflict.js.map +1 -1
  20. package/dist/cjs/testing.js +2 -2
  21. package/dist/cjs/testing.js.map +1 -1
  22. package/dist/{dts → dts-cjs}/AppendCondition.d.ts +5 -7
  23. package/dist/{dts → dts-cjs}/AppendCondition.d.ts.map +1 -1
  24. package/dist/{dts → dts-cjs}/AppendIsOwned.d.ts +6 -6
  25. package/dist/{dts → dts-cjs}/AppendIsOwned.d.ts.map +1 -1
  26. package/dist/{dts → dts-cjs}/ContractStore.d.ts +3 -2
  27. package/dist/dts-cjs/ContractStore.d.ts.map +1 -0
  28. package/dist/{dts → dts-cjs}/DcbEventStore.d.ts +2 -2
  29. package/dist/{dts → dts-cjs}/DcbEventStore.d.ts.map +1 -1
  30. package/dist/{dts → dts-cjs}/Event.d.ts +31 -20
  31. package/dist/dts-cjs/Event.d.ts.map +1 -0
  32. package/dist/{dts → dts-cjs}/Query.d.ts +5 -5
  33. package/dist/dts-cjs/SubscribeMachine.d.ts +289 -0
  34. package/dist/dts-cjs/SubscribeMachine.d.ts.map +1 -0
  35. package/dist/{dts → dts-cjs}/inMemory.d.ts.map +1 -1
  36. package/dist/{dts → dts-cjs}/index.d.ts +1 -1
  37. package/dist/dts-cjs/package.json +3 -0
  38. package/dist/{dts → dts-cjs}/retryOnConflict.d.ts +1 -7
  39. package/dist/dts-cjs/retryOnConflict.d.ts.map +1 -0
  40. package/dist/{dts → dts-cjs}/testing.d.ts +1 -1
  41. package/dist/dts-esm/AppendCondition.d.ts +21 -0
  42. package/dist/dts-esm/AppendCondition.d.ts.map +1 -0
  43. package/dist/dts-esm/AppendIsOwned.d.ts +78 -0
  44. package/dist/dts-esm/AppendIsOwned.d.ts.map +1 -0
  45. package/dist/dts-esm/ContractStore.d.ts +62 -0
  46. package/dist/dts-esm/ContractStore.d.ts.map +1 -0
  47. package/dist/dts-esm/DcbEventStore.d.ts +64 -0
  48. package/dist/dts-esm/DcbEventStore.d.ts.map +1 -0
  49. package/dist/dts-esm/Event.d.ts +174 -0
  50. package/dist/dts-esm/Event.d.ts.map +1 -0
  51. package/dist/dts-esm/Projection.d.ts +124 -0
  52. package/dist/dts-esm/Projection.d.ts.map +1 -0
  53. package/dist/dts-esm/Query.d.ts +180 -0
  54. package/dist/dts-esm/Query.d.ts.map +1 -0
  55. package/dist/dts-esm/SubscribeMachine.d.ts +289 -0
  56. package/dist/dts-esm/SubscribeMachine.d.ts.map +1 -0
  57. package/dist/dts-esm/errors.d.ts +17 -0
  58. package/dist/dts-esm/errors.d.ts.map +1 -0
  59. package/dist/dts-esm/inMemory.d.ts +76 -0
  60. package/dist/dts-esm/inMemory.d.ts.map +1 -0
  61. package/dist/dts-esm/index.d.ts +40 -0
  62. package/dist/dts-esm/index.d.ts.map +1 -0
  63. package/dist/dts-esm/package.json +3 -0
  64. package/dist/dts-esm/retryOnConflict.d.ts +64 -0
  65. package/dist/dts-esm/retryOnConflict.d.ts.map +1 -0
  66. package/dist/dts-esm/testing.d.ts +39 -0
  67. package/dist/dts-esm/testing.d.ts.map +1 -0
  68. package/dist/esm/AppendCondition.js +1 -3
  69. package/dist/esm/AppendCondition.js.map +1 -1
  70. package/dist/esm/AppendIsOwned.js +6 -6
  71. package/dist/esm/AppendIsOwned.js.map +1 -1
  72. package/dist/esm/ContractStore.js +2 -2
  73. package/dist/esm/ContractStore.js.map +1 -1
  74. package/dist/esm/DcbEventStore.js +1 -1
  75. package/dist/esm/DcbEventStore.js.map +1 -1
  76. package/dist/esm/Event.js +30 -14
  77. package/dist/esm/Event.js.map +1 -1
  78. package/dist/esm/Query.js +1 -1
  79. package/dist/esm/Query.js.map +1 -1
  80. package/dist/esm/SubscribeMachine.js +157 -67
  81. package/dist/esm/SubscribeMachine.js.map +1 -1
  82. package/dist/esm/inMemory.js +5 -5
  83. package/dist/esm/inMemory.js.map +1 -1
  84. package/dist/esm/index.js +1 -1
  85. package/dist/esm/retryOnConflict.js +4 -10
  86. package/dist/esm/retryOnConflict.js.map +1 -1
  87. package/dist/esm/testing.js +2 -2
  88. package/dist/esm/testing.js.map +1 -1
  89. package/package.json +21 -11
  90. package/src/AppendCondition.ts +1 -1
  91. package/src/AppendIsOwned.ts +7 -7
  92. package/src/ContractStore.ts +5 -4
  93. package/src/DcbEventStore.ts +3 -3
  94. package/src/Event.ts +49 -14
  95. package/src/Query.ts +1 -1
  96. package/src/SubscribeMachine.ts +278 -96
  97. package/src/inMemory.ts +6 -6
  98. package/src/index.ts +1 -1
  99. package/src/retryOnConflict.ts +6 -11
  100. package/src/testing.ts +2 -2
  101. package/testing/package.json +1 -1
  102. package/dist/dts/ContractStore.d.ts.map +0 -1
  103. package/dist/dts/Event.d.ts.map +0 -1
  104. package/dist/dts/SubscribeMachine.d.ts +0 -179
  105. package/dist/dts/SubscribeMachine.d.ts.map +0 -1
  106. package/dist/dts/retryOnConflict.d.ts.map +0 -1
  107. /package/dist/{dts → dts-cjs}/Projection.d.ts +0 -0
  108. /package/dist/{dts → dts-cjs}/Projection.d.ts.map +0 -0
  109. /package/dist/{dts → dts-cjs}/Query.d.ts.map +0 -0
  110. /package/dist/{dts → dts-cjs}/errors.d.ts +0 -0
  111. /package/dist/{dts → dts-cjs}/errors.d.ts.map +0 -0
  112. /package/dist/{dts → dts-cjs}/inMemory.d.ts +0 -0
  113. /package/dist/{dts → dts-cjs}/index.d.ts.map +0 -0
  114. /package/dist/{dts → dts-cjs}/testing.d.ts.map +0 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The DCB selection language: `Query` is an OR of `QueryItem`s. Within an item,
3
+ * `types` is an OR-filter (empty means any type) and `tags` is an AND superset
4
+ * test (the event must carry all of the item's tags; extra event tags are
5
+ * fine). The empty query (`items: []`) matches every event.
6
+ *
7
+ * The match predicate is a single pure module function over plain data;
8
+ * `Query` values carry no methods. See `matchesQuery` / `matchesQueryItem`.
9
+ *
10
+ * Smart constructors enforce the indexed fast-path restriction at the
11
+ * constructor surface (not via a runtime throw): every query built through them
12
+ * is the wildcard (`query.all`), a single type-only item (`query.types`), or an
13
+ * OR of tagged items each carrying at least one tag (`query.item` + `query.or`,
14
+ * or `query.tags` for the single-item case). The `query.item` constructor
15
+ * requires a non-empty `tags` at the type level, so there is no public way to
16
+ * build an unrestricted item (empty types and empty tags). `QueryItem`/`Query`
17
+ * are plain data, so internal decision-model plumbing can still assemble any
18
+ * shape directly; the restriction binds the public surface, where consumers
19
+ * build their consistency boundaries.
20
+ *
21
+ * The grammar's DISPATCH lives here too, beside the grammar it dispatches over:
22
+ * `ServableQueryShape` / `classifyServableQuery` own the three shapes an engine
23
+ * serves and the routing of the degenerate empty item.
24
+ */
25
+ import { Array as Arr, Schema } from 'effect';
26
+ import { type DcbEvent, EventType, type SequencedEvent, Tag } from './Event.js';
27
+ /**
28
+ * One clause of a query: a set of types (OR, empty = any) AND a set of tags
29
+ * (superset test, empty = no tag constraint).
30
+ */
31
+ export declare const QueryItem: Schema.Struct<{
32
+ readonly types: Schema.$Array<Schema.brand<Schema.String, "EventType">>;
33
+ readonly tags: Schema.$Array<Schema.brand<Schema.String, "Tag">>;
34
+ }>;
35
+ export type QueryItem = typeof QueryItem.Type;
36
+ /** A query: an OR of items. Empty `items` matches everything. */
37
+ export declare const Query: Schema.Struct<{
38
+ readonly items: Schema.$Array<Schema.Struct<{
39
+ readonly types: Schema.$Array<Schema.brand<Schema.String, "EventType">>;
40
+ readonly tags: Schema.$Array<Schema.brand<Schema.String, "Tag">>;
41
+ }>>;
42
+ }>;
43
+ export type Query = typeof Query.Type;
44
+ /**
45
+ * True when `event` satisfies `item`: its type is in `item.types` (or the item
46
+ * has no type constraint) AND the event's tags are a superset of `item.tags`.
47
+ */
48
+ export declare const matchesQueryItem: (event: DcbEvent, item: QueryItem) => boolean;
49
+ /**
50
+ * The one true match predicate (ported from `popo.py:48-54`): an empty query is
51
+ * a wildcard; otherwise the event must match at least one item (OR across
52
+ * items).
53
+ */
54
+ export declare const matchesQuery: (event: DcbEvent, query: Query) => boolean;
55
+ /** Convenience predicate over a `SequencedEvent`. */
56
+ export declare const matchesSequenced: (sequenced: SequencedEvent, query: Query) => boolean;
57
+ /**
58
+ * Assert `query` is *servable* by the indexed fast paths, throwing a plain
59
+ * `Error` describing the violation otherwise. This enforces at the store
60
+ * boundary the restriction the smart constructors enforce at the type level
61
+ * (`Query.ts` module docs) but which plain-data assembly — e.g. decision-model
62
+ * composition, `Projection.ts:31-36` — can still bypass.
63
+ *
64
+ * A query, or any of its items, carrying a key the grammar does not define is
65
+ * refused before anything else is judged (`unknownKeyFault` below says why).
66
+ *
67
+ * The servable grammar is exactly one of:
68
+ * - the wildcard (`items.length === 0`), which matches every event;
69
+ * - a *single* item (`items.length === 1`), any types/tags, including a
70
+ * type-only item; or
71
+ * - a *multi*-item query (`items.length > 1`) in which every item carries at
72
+ * least one tag.
73
+ *
74
+ * A multi-item query that mixes a tagless (type-only) item with tagged items is
75
+ * NOT servable: the by-type path and the by-tag path are indexed differently,
76
+ * so an OR spanning both cannot ride a single index. This is the previously
77
+ * un-enforced edge (`Projection.ts:31-36`).
78
+ *
79
+ * This function is pure (no clock, no RNG, no effect). Every engine applies it at
80
+ * its read/subscribe/append entry points, through the one shared
81
+ * `makeContractStore` decorator, so the rule is inherited rather than restated per
82
+ * backend; a throw inside a store `Effect` becomes a *defect* — a programming
83
+ * error, not an error-channel value — which is the intended "defect in every
84
+ * engine" behaviour.
85
+ */
86
+ export declare const assertServableQuery: (query: Query) => void;
87
+ /**
88
+ * The unknown-key half of `servableQueryFault`, on its own: the refusal of a key the
89
+ * grammar does not define on the envelope or on any item, or `undefined`.
90
+ *
91
+ * It is the same rule and not a second statement of it — `servableQueryFault` calls
92
+ * this first. It is exported because ONE caller has to tell the two halves apart:
93
+ * the codec's read ledger refuses an unservable SHAPE on its own error channel, and
94
+ * must not report an unknown key as one. An unknown key is not something a slice's
95
+ * tags can fix, so the ledger leaves it to the store, which refuses it as a defect.
96
+ */
97
+ export declare const unknownQueryKeyFault: (query: Query) => string | undefined;
98
+ /**
99
+ * The servable grammar as a TOTAL function: the reason `query` cannot be served,
100
+ * or `undefined` when it can.
101
+ *
102
+ * The same rule as `assertServableQuery` above and NOT a second statement of it —
103
+ * that function is this one plus a throw, so the grammar is defined once and the
104
+ * two differ only in DISPOSITION. A store boundary wants the defect, because a
105
+ * non-servable query there is a programming error with no recovery; a caller that
106
+ * can still act on the answer wants a value, and the codec's read ledger is one:
107
+ * it asks whether a hop it is about to record would make the accumulated union
108
+ * unservable, and refuses on the error channel BEFORE the read is paid for.
109
+ *
110
+ * The alternative was for that caller to re-derive the rule, and it was tried:
111
+ * the codec's own copy tested each recorded query separately, which silently
112
+ * admitted a FIRST hop composed of two tagless slices — a multi-item query, and
113
+ * unservable, dying at the store one layer away. A grammar with two homes drifts
114
+ * at exactly the shapes neither author had in mind. The shape here is the read
115
+ * side's own (`projectionWiringFault` returns `string | undefined` for the same
116
+ * reason), so a fault is one exit route and one return type.
117
+ */
118
+ export declare const servableQueryFault: (query: Query) => string | undefined;
119
+ /**
120
+ * The three shapes a servable query is SERVED by, and therefore the only three an
121
+ * engine's plan compiler dispatches over: the wildcard, a single type-only item,
122
+ * and an all-tagged query. `assertServableQuery` has already refused the mixed
123
+ * tagless-plus-tagged multi-item query, so these three are exhaustive over
124
+ * everything that reaches a store.
125
+ *
126
+ * `typeOnly` carries a NON-EMPTY list of types, and that is the whole guard
127
+ * against the degenerate empty item: see `classifyServableQuery`.
128
+ */
129
+ export type ServableQueryShape = {
130
+ readonly kind: 'wildcard';
131
+ } | {
132
+ readonly kind: 'typeOnly';
133
+ readonly types: Arr.NonEmptyReadonlyArray<EventType>;
134
+ } | {
135
+ readonly kind: 'tagged';
136
+ };
137
+ /**
138
+ * Classify a servable `Query` into the shape that serves it.
139
+ *
140
+ * ## Why the dispatch lives beside the grammar
141
+ *
142
+ * The three shapes ARE the three bullets `assertServableQuery` above admits, so an
143
+ * engine that re-derives the dispatch is a second answer to "which shape is
144
+ * this?", and the shapes only stay in step while there is one answer. Core states
145
+ * the grammar, so stating the dispatch over it here is that one fact once rather
146
+ * than once per backend. It is pure — no clock, no RNG, no effect — and knows
147
+ * nothing of SQL: what a shape COMPILES to stays entirely the engine's.
148
+ *
149
+ * ## The degenerate empty item routes to the WILDCARD, structurally
150
+ *
151
+ * A single item with neither types nor tags — `{ items: [{ types: [], tags: [] }] }`
152
+ * — matches EVERY event: `matchesQueryItem` reads empty types as "any type" and
153
+ * empty tags as a vacuous superset test. It must therefore be served by the
154
+ * wildcard. Served by the type-only path it would compile to a value list with
155
+ * nothing in it, and a membership test against an empty list matches NOTHING — the
156
+ * exact inversion of what the query means. Nothing upstream stops it reaching an
157
+ * engine: `assertServableQuery` admits a SINGLE item of any shape, and the servable
158
+ * grammar forbids a tagless item only in a MULTI-item query, so this dispatch is
159
+ * the last place the divergence can be closed.
160
+ *
161
+ * It is closed by the SHAPE and not by a branch in front of it. `typeOnly` carries
162
+ * `NonEmptyReadonlyArray`, so there is no way to construct it from an empty list;
163
+ * the emptiness test IS the constructor, and deleting it does not silently restore
164
+ * the wrong routing, it stops compiling. A guard clause placed before the
165
+ * construction would have been deletable by anyone tidying what looks like a
166
+ * redundant check on a list that "obviously" has types in it.
167
+ */
168
+ export declare const classifyServableQuery: (query: Query) => ServableQueryShape;
169
+ /** Namespaced smart constructors for `Query`. */
170
+ export declare const query: {
171
+ readonly all: () => Query;
172
+ readonly item: (fields: {
173
+ readonly tags: readonly [Tag, ...ReadonlyArray<Tag>];
174
+ readonly types?: ReadonlyArray<EventType>;
175
+ }) => QueryItem;
176
+ readonly or: (items: readonly [QueryItem, ...ReadonlyArray<QueryItem>]) => Query;
177
+ readonly tags: (tags: readonly [Tag, ...ReadonlyArray<Tag>], types?: ReadonlyArray<EventType>) => Query;
178
+ readonly types: (types: readonly [EventType, ...ReadonlyArray<EventType>]) => Query;
179
+ };
180
+ //# sourceMappingURL=Query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Query.d.ts","sourceRoot":"","sources":["../../src/Query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAE5E;;;GAGG;AACH,eAAO,MAAM,SAAS;;;EAGpB,CAAA;AACF,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC,IAAI,CAAA;AAE7C,iEAAiE;AACjE,eAAO,MAAM,KAAK;;;;;EAEhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAErC;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,QAAQ,EAAE,MAAM,SAAS,KAAG,OAOnE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,QAAQ,EAAE,OAAO,KAAK,KAAG,OAK5D,CAAA;AAED,qDAAqD;AACrD,eAAO,MAAM,gBAAgB,GAC3B,WAAW,cAAc,EACzB,OAAO,KAAK,KACX,OAA+C,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,KAAK,KAAG,IAKlD,CAAA;AAkDD;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,KAAK,KAAG,MAAM,GAAG,SAM1D,CAAA;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,KAAK,KAAG,MAAM,GAAG,SAsB1D,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACrD,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAA;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,KAAK,KAAG,kBAWpD,CAAA;AAoDD,iDAAiD;AACjD,eAAO,MAAM,KAAK;wBAlCF,KAAK;4BATC;QACpB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAA;QACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAA;KAC1C,KAAG,SAAS;yBAkCJ,SAAS,CAAC,SAAS,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,KACvD,KAAK;0BAtBA,SAAS,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,UACnC,aAAa,CAAC,SAAS,CAAC,KAC/B,KAAK;4BAUC,SAAS,CAAC,SAAS,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,KACvD,KAAK;CAoBE,CAAA"}
@@ -0,0 +1,289 @@
1
+ /**
2
+ * The subscribe machine every engine's `subscribe` is built from — the in-memory
3
+ * store, `@kairos-es/store-postgres` and `@kairos-es/store-sqlite`: the wake queue,
4
+ * the wake-source drain, the paged catch-up reconcile, the `> boundary` de-dup (via
5
+ * the cursor's exclusive lower bound) and the poll loop that together turn a paged
6
+ * read into a live subscription.
7
+ *
8
+ * The algorithm is `subscribeItems`, generic over the item `A` it delivers and the
9
+ * cursor `C` it resumes from, because nothing in it is about an event: any source
10
+ * that can page-read strictly after a cursor and name the cursor of what it
11
+ * returned is subscribable by the same loop. `subscribeStream` is the event-store
12
+ * shape — `SequencedEvent` ordered by `Position` — over the same loop, and it is
13
+ * what every engine calls, unchanged by the generalisation. A second kind of
14
+ * source reuses the loop rather than growing a second one, for the reason the next
15
+ * paragraph gives.
16
+ *
17
+ * It lives in core because delivery semantics are contract semantics. An engine
18
+ * supplies TRANSPORT through two STUBBABLE seams — `fetch`, a paged read strictly
19
+ * after a cursor, and `listen`, an optional wake source — and inherits the delivery
20
+ * behaviour whole, exactly as it supplies mechanics to `makeContractStore` and
21
+ * inherits the contract policy whole. Two engines that each grew their own wake
22
+ * loop would be identical only by inspection, and `subscribe` is the verb where
23
+ * that is least affordable: a divergence there loses events rather than failing.
24
+ * Both seams take plain in-memory values in a test, so the wake/poll/de-dup logic
25
+ * is exercisable with no live backend at all.
26
+ *
27
+ * All THREE engines are callers, and the count matters to a reader here: it means
28
+ * there is exactly ONE delivery algorithm in this repo, and it is the code below. The
29
+ * in-memory store is a caller like the rest — it hands in a `fetch` over its log
30
+ * `Ref` and its append `PubSub` as a wake source and keeps no delivery logic of its
31
+ * own — which is what makes the shared contract suite's subscribe cases a comparison
32
+ * of two BACKENDS rather than of two delivery algorithms.
33
+ *
34
+ * Correctness rests on the periodic cursor-based reconcile — a re-read from the
35
+ * cursor every poll interval — and NEVER on the wake source, which only lowers
36
+ * delivery latency below that interval. A subscription with no wake source at all
37
+ * is therefore complete and correct, merely slower, which is why `listen` is
38
+ * optional in the TYPE rather than by the convention of passing `Stream.never`:
39
+ * poll-only delivery is a supported shape, not a degraded one.
40
+ *
41
+ * ## The precondition every caller inherits: ASCENDING COMMIT ORDER
42
+ *
43
+ * It binds EVERY caller, over whatever cursor it chose — an engine's `Position`, or
44
+ * any other `C` handed to `subscribeItems` — and the example below is written in
45
+ * positions only because the engines are the callers it was first written for. A
46
+ * cursor that is committed and VISIBLE must never be below one already delivered.
47
+ * Gaps are fine: the cursor is an ascending exclusive lower bound, so a number
48
+ * that is allocated and never committed, or never allocated at all, is simply a
49
+ * number the machine never sees. Out-of-order commits are not fine. If one
50
+ * writer allocates position 5 and another allocates 6, and 6 commits first, a poll
51
+ * landing between the two commits delivers 6 and advances the cursor past it — and
52
+ * 5 is then lost forever, because no later read looks below the cursor and nothing
53
+ * anywhere notices the hole. A caller whose concurrent writers can commit out of
54
+ * order MUST serialise them, or this machine will silently skip. An engine that
55
+ * takes an exclusive lock on both of its append paths discharges the obligation
56
+ * that way; an engine whose append is a single statement discharges it
57
+ * structurally.
58
+ *
59
+ * The obligation is written down HERE, in the algorithm that depends on it, rather
60
+ * than in whichever caller happens to satisfy it. It is a property of this delivery
61
+ * loop and of nothing else, so an author reading only their own source has
62
+ * no way to discover that anything downstream needs it — and the failure mode is
63
+ * the worst kind there is: no error, no defect, no failing test, just an event that
64
+ * a subscriber never receives. A single engine owning both its append path and its
65
+ * own wake loop can leave the assumption implicit; a shared machine cannot.
66
+ *
67
+ * ## `catchUpPageSize` bounds the READ, not the emission — unless `emit: 'page'`
68
+ *
69
+ * Under the default `emit: 'backlog'`, which is what `subscribeStream` and so every
70
+ * engine uses, `drainFrom` below PAGES the catch-up read — `catchUpPageSize` bounds
71
+ * each `fetch` — but it COLLECTS every page into one array, which is emitted as a
72
+ * SINGLE chunk. So peak memory at subscription start is O(backlog) rather than O(page): a
73
+ * subscriber joining behind a large log materialises the whole backlog before its
74
+ * consumer sees the first element.
75
+ *
76
+ * That is a memory characteristic and nothing more, which is why ONE chunk is
77
+ * enough here: downstream commit boundaries do not depend on the upstream chunking
78
+ * at all, because the read side's `Stream.groupedWithin` re-chunks the stream into
79
+ * bounded micro-batches whatever arrives (ADR-0007), so a projection's transactions
80
+ * stay small even against a one-chunk catch-up. The cost therefore lands on backlog
81
+ * memory alone, and it lands hardest on the durable-log-with-in-memory-view mode,
82
+ * where an ephemeral view re-catches-up the entire log on every boot.
83
+ *
84
+ * `emit: 'page'` changes exactly that and nothing else. Each fetched page is
85
+ * emitted as its own chunk and the cursor advances past it before the next read, so
86
+ * peak memory is O(page) and the consumer sees the first item after ONE read rather
87
+ * than after the whole backlog. Delivery is unchanged: the same items in the same
88
+ * order, the same de-dup, the same wake and poll. What a caller gives up is the
89
+ * one-chunk catch-up, so it must not read meaning into chunk boundaries under
90
+ * either mode. It is for a consumer that does NOT re-chunk downstream and whose
91
+ * catch-up can be the whole of a large source — a full resync of a table, say —
92
+ * where holding the backlog would be the cost the paging was meant to avoid. The
93
+ * engines keep `'backlog'` because the read side's re-chunking already bounds what
94
+ * they commit, and changing their emission would move a behaviour nobody asked to
95
+ * move.
96
+ */
97
+ import { Effect, Schema, type Scope, Stream } from 'effect';
98
+ import type { Position, SequencedEvent } from './Event.js';
99
+ import { ReadLimit } from './Event.js';
100
+ /**
101
+ * A short catch-up page size default for the reconcile loop. Reads page by page
102
+ * until a short page signals the tail is drained, so a large backlog is streamed in
103
+ * bounded chunks rather than one unbounded read. Every engine takes its
104
+ * `catchUpPageSize` from here — each SQL engine as its config's default, the
105
+ * in-memory store as its options' — so backends cannot drift on tuning.
106
+ */
107
+ export declare const DEFAULT_CATCH_UP_PAGE_SIZE = 512;
108
+ /**
109
+ * The default upper bound a subscriber waits for a wake before re-reading from its
110
+ * cursor ANYWAY (milliseconds). Every engine takes its `pollInterval` from here —
111
+ * each SQL engine as its config's default, the in-memory store as its options' — so
112
+ * backends cannot drift on tuning.
113
+ *
114
+ * This periodic position-based poll — not the wake source — IS the correctness
115
+ * guarantee: every committed position is reconciled within this interval whether or
116
+ * not a wake for it was ever heard. That covers a wake fired before the source was
117
+ * fully established, a coalesced or dropped wake, and a wake source that died
118
+ * silently and never said so. A wake source only lowers delivery latency BELOW this
119
+ * interval; it never carries delivery, so no engine's wake mechanism can make the
120
+ * right figure here engine-specific. It trades worst-case delivery latency against
121
+ * idle read volume and nothing else. Upstream `postgres_tt.py` picks the same
122
+ * one-second fallback poll (`conn.wait(..., interval=1)`).
123
+ */
124
+ export declare const DEFAULT_POLL_INTERVAL_MILLIS = 1000;
125
+ /**
126
+ * The two subscribe-tuning config FIELDS, for an engine whose config is a `Schema`
127
+ * to spread into its own struct.
128
+ *
129
+ * These knobs are the machine's, not any engine's: they name what the loop above
130
+ * does, the figures they fall back to are the two constants declared beside them,
131
+ * and neither is derivable from a backend's own properties — which is why the
132
+ * defaults already live here. Shipping the FIELD rather than only the default is
133
+ * what closes the last gap in that arrangement. An engine that spreads these gets
134
+ * the key NAME, the VALIDATION and the DEFAULT from one place, so a caller who has
135
+ * tuned one engine's subscribe reads another's without translating, and the two
136
+ * cannot drift in what they ACCEPT any more than in what they assume. Two engines
137
+ * hand-authoring the identical `Schema.optionalKey(…)` twice were identical only
138
+ * by inspection.
139
+ *
140
+ * The bounds differ between the two because the quantities do. `pollInterval` is a
141
+ * duration, so a fraction of a millisecond is meaningful and only `isGreaterThan(0)`
142
+ * and `isFinite` matter: at zero or below the loop would spin without ever waiting for a
143
+ * wake, and at `Infinity` the periodic re-read — the half that carries delivery —
144
+ * would never come, which silently converts the wake source from an optimisation
145
+ * into the guarantee. `catchUpPageSize` is a COUNT, so it must be an integer
146
+ * (`Schema.Int` excludes `Infinity` with it): the reconcile loop compares a page's
147
+ * length against it to decide the tail is drained, and a page of zero would report
148
+ * "drained" forever without reading anything.
149
+ *
150
+ * `optionalKey` on both keeps the decoded type at `key?: T` rather than
151
+ * `T | undefined`, matching the repo's `exactOptionalPropertyTypes` discipline — an
152
+ * absent key, never an explicit `undefined`.
153
+ */
154
+ export declare const subscriptionTuningFields: {
155
+ readonly pollInterval: Schema.optionalKey<Schema.Number>;
156
+ readonly catchUpPageSize: Schema.optionalKey<Schema.Int>;
157
+ };
158
+ /**
159
+ * How a subscription hands its catch-up to the consumer: the whole backlog as ONE
160
+ * chunk (`'backlog'`), or each fetched page as a chunk of its own (`'page'`). The
161
+ * module doc's "`catchUpPageSize` bounds the READ" section owns what each costs.
162
+ *
163
+ * A closed union of two words rather than a boolean, so a call site says WHICH
164
+ * behaviour it chose instead of what it turned off, and a third shape, should one
165
+ * ever be argued for, is added beside these rather than encoded in a second flag.
166
+ */
167
+ export type SubscriptionEmission = 'backlog' | 'page';
168
+ /**
169
+ * The machine's inputs over ANY item `A` with an ascending cursor `C`: the two seams,
170
+ * where the cursor comes from and starts, and the tuning.
171
+ *
172
+ * Generic in the item and the cursor because nothing in the algorithm is about an
173
+ * EVENT. The wake queue, the poll, the paged reconcile and the exclusive-lower-bound
174
+ * de-dup need only three things of what they deliver: a page-read strictly after a
175
+ * cursor, a way to read the cursor OFF a delivered item, and a starting cursor. A
176
+ * log of `SequencedEvent`s ordered by `Position` is one such source; a feed of rows
177
+ * ordered by a feed-local sequence is another, and a second copy of this loop for it
178
+ * would be exactly the divergence the module doc argues against.
179
+ *
180
+ * The machine never COMPARES two cursors. `fetch` is the only thing that interprets
181
+ * one — its exclusive lower bound is what does the de-dup — so `C` needs no ordering
182
+ * instance here, and the ascending-commit-order obligation in the module doc is the
183
+ * caller's, stated over whatever `C` it chose.
184
+ *
185
+ * `initial` is REQUIRED, unlike `subscribeStream`'s `after`. Only a cursor type the
186
+ * machine knows can have a default — `ORIGIN` for `Position` — and for any other the
187
+ * caller is the only party that knows what "before everything" is.
188
+ *
189
+ * `pollInterval` and `catchUpPageSize` are optional here and fall back to
190
+ * `DEFAULT_POLL_INTERVAL_MILLIS` and `DEFAULT_CATCH_UP_PAGE_SIZE`: a caller with no
191
+ * config of its own takes the same figures every engine does, from the same place.
192
+ * The event-store shape, `SubscriptionDeps`, keeps them required, because every
193
+ * engine already resolves them from its config and that type is unchanged.
194
+ */
195
+ export interface ItemSubscriptionDeps<A, C, E> {
196
+ /**
197
+ * Page-read the items strictly after `after`, up to `limit`, ascending by cursor.
198
+ * Returning FEWER than `limit` signals the tail is drained. The exclusive lower
199
+ * bound IS the de-dup: a re-read from the cursor never returns what was delivered.
200
+ */
201
+ readonly fetch: (after: C, limit: ReadLimit) => Effect.Effect<ReadonlyArray<A>, E>;
202
+ /** The cursor of a delivered item: the next `fetch` starts strictly after it. */
203
+ readonly cursorOf: (item: A) => C;
204
+ /** The cursor to start from, exclusive: the first `fetch` reads strictly after it. */
205
+ readonly initial: C;
206
+ /** The raw wake source — see `SubscriptionDeps.listen`, whose rules it shares. */
207
+ readonly listen?: Stream.Stream<unknown, E>;
208
+ /**
209
+ * The poll interval in milliseconds (default `DEFAULT_POLL_INTERVAL_MILLIS`):
210
+ * finite and greater than 0, or `subscribeItems` dies.
211
+ */
212
+ readonly pollInterval?: number;
213
+ /**
214
+ * The catch-up page size (default `DEFAULT_CATCH_UP_PAGE_SIZE`): an integer
215
+ * greater than 0, or `subscribeItems` dies.
216
+ */
217
+ readonly catchUpPageSize?: number;
218
+ /** The catch-up's chunking (default `'backlog'`); see `SubscriptionEmission`. */
219
+ readonly emit?: SubscriptionEmission;
220
+ }
221
+ /**
222
+ * The two seams the event-store subscribe machine runs over, plus the two tuning
223
+ * knobs: `ItemSubscriptionDeps` fixed to `SequencedEvent` and `Position`, the shape
224
+ * every engine's `subscribe` already hands to `subscribeStream`. `E` is the engine's
225
+ * own infrastructure error — whatever its reads fail with — and it stays on the
226
+ * channel through to `makeContractStore`, which dies it.
227
+ *
228
+ * ONE type parameter and not two, deliberately: a wake source with an error type of
229
+ * its own is `Stream.orDie`d by the caller, yielding a `Stream<unknown, never>`
230
+ * that is assignable here under Effect's covariant error channel. A second
231
+ * parameter would buy nothing and would have to be spelled at every call site. Nor
232
+ * is there an `R`: neither seam contributes a requirement, and unused generality is
233
+ * not free.
234
+ */
235
+ export interface SubscriptionDeps<E> {
236
+ /**
237
+ * Page-read the events strictly after `from`, up to `limit`, ascending by
238
+ * position. Returning FEWER than `limit` signals the tail is drained.
239
+ */
240
+ readonly fetch: (from: Position, limit: ReadLimit) => Effect.Effect<ReadonlyArray<SequencedEvent>, E>;
241
+ /**
242
+ * The raw wake source: each element is an OPAQUE wake signal. The machine taps it
243
+ * into its own wake queue and drains it on a background fibre; it never reads the
244
+ * elements, so anything an engine can push on a change will do.
245
+ *
246
+ * OPTIONAL, and omitting it defaults to `Stream.never` at the point of use — an
247
+ * engine with no wake mechanism gets poll-only delivery, which the poll loop
248
+ * already makes correct. Under `exactOptionalPropertyTypes` that means omitting
249
+ * the key entirely rather than passing `undefined`.
250
+ */
251
+ readonly listen?: Stream.Stream<unknown, E>;
252
+ /** The poll interval in milliseconds (see `DEFAULT_POLL_INTERVAL_MILLIS`). */
253
+ readonly pollInterval: number;
254
+ /** The catch-up page size (see `DEFAULT_CATCH_UP_PAGE_SIZE`). */
255
+ readonly catchUpPageSize: number;
256
+ }
257
+ /**
258
+ * Build a subscription `Stream` over any ascending-cursor source: catch up from
259
+ * `initial`, then track the live tail. The cursor threads through the
260
+ * `Stream.paginate` state, starting at `initial` and advancing to the cursor of the
261
+ * last delivered item each batch; the stream never completes on its own (the next
262
+ * state is always `Option.some`), so the enclosing scope interrupts it on close. An
263
+ * `E` mid-stream stays on the channel, for the caller to die or to supervise.
264
+ *
265
+ * This is THE delivery algorithm, and `subscribeStream` runs the same machine. Under
266
+ * `emit: 'backlog'` a batch is everything a drain to the tail returned; under
267
+ * `'page'` it is one fetched page, so the paginate step and the page coincide and a
268
+ * full page is followed at once by the next read rather than by a wait.
269
+ *
270
+ * A `pollInterval` or `catchUpPageSize` the config fields would refuse is a DEFECT
271
+ * here, raised by the stream before it reads or forks anything, with a message
272
+ * naming the field. This caller may have no config whose decode would have caught
273
+ * it, and unchecked the two failures are silent or late: a page size of 0 gives a
274
+ * stream that never emits and never fails, and 1.5 dies only at the first read,
275
+ * inside `ReadLimit`, in words that name neither knob.
276
+ */
277
+ export declare const subscribeItems: <A, C, E>(deps: ItemSubscriptionDeps<A, C, E>) => Stream.Stream<A, E, Scope.Scope>;
278
+ /**
279
+ * Build the event subscription `Stream`: catch up from `after`, then track the live
280
+ * tail. A THIN wrapper over the machine `subscribeItems` runs, fixing the item to
281
+ * `SequencedEvent`, the cursor to its `position`, the start to `ORIGIN` when `after`
282
+ * is absent, and the emission to the default `'backlog'` — so every engine's call
283
+ * site, and every figure it delivers, is what it was before the machine became
284
+ * generic. It skips `subscribeItems`' tuning check, for the reason on `machine`. An `E`
285
+ * mid-stream stays on the channel; `makeContractStore` dies it (subscribe is
286
+ * `E = never` on the contract).
287
+ */
288
+ export declare const subscribeStream: <E>(deps: SubscriptionDeps<E>, after: Position | undefined) => Stream.Stream<SequencedEvent, E, Scope.Scope>;
289
+ //# sourceMappingURL=SubscribeMachine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubscribeMachine.d.ts","sourceRoot":"","sources":["../../src/SubscribeMachine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+FG;AACH,OAAO,EACL,MAAM,EAIN,MAAM,EACN,KAAK,KAAK,EACV,MAAM,EACP,MAAM,QAAQ,CAAA;AACf,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAAU,SAAS,EAAE,MAAM,SAAS,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAA;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAA;AAWhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,wBAAwB;;;CAG3B,CAAA;AA2CV;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,CAAA;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,SAAS,KACb,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACvC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAA;IACjC,sFAAsF;IACtF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IACnB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3C;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,oBAAoB,CAAA;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,SAAS,KACb,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAA;IACpD;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3C,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EACpC,MAAM,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAClC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAGjC,CAAA;AA4HD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAC/B,MAAM,gBAAgB,CAAC,CAAC,CAAC,EACzB,OAAO,QAAQ,GAAG,SAAS,KAC1B,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAQ3C,CAAA"}
@@ -0,0 +1,17 @@
1
+ import type { AppendCondition } from './AppendCondition.js';
2
+ declare const AppendConditionFailed_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
3
+ readonly _tag: "AppendConditionFailed";
4
+ } & Readonly<A>;
5
+ /**
6
+ * Raised by `append` when the append condition's `failIfEventsMatch` query
7
+ * matches at least one event after `condition.after`. Carries only the
8
+ * offending condition: a correct retry re-reads the store to its new head and
9
+ * rebuilds its condition from there, so no conflicting position is carried (it
10
+ * would already be stale by the time it was acted on).
11
+ */
12
+ export declare class AppendConditionFailed extends AppendConditionFailed_base<{
13
+ readonly condition: AppendCondition;
14
+ }> {
15
+ }
16
+ export {};
17
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;;;;AAExD;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,2BAEzC;IACA,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAA;CACpC,CAAC;CAAG"}
@@ -0,0 +1,76 @@
1
+ import { Effect, Layer } from 'effect';
2
+ import { DcbEventStore } from './DcbEventStore.js';
3
+ /**
4
+ * Options for the in-memory store factory.
5
+ *
6
+ * `beforeCommit` runs inside the append critical section, between the
7
+ * conflict-check and the commit. It defaults to `Effect.void` — a synchronous
8
+ * no-op — so the shipped critical section stays atomic by construction
9
+ * (`Ref.get` -> filter -> `Ref.set`, no suspension), which is the whole
10
+ * exactly-one-wins guarantee. It is a test seam: the concurrency contract test
11
+ * overrides it with `Effect.yieldNow()` to force a check/commit suspension, so
12
+ * the guarantee genuinely depends on `withPermits(1)` serialising across that
13
+ * boundary — without an injected suspension the test is vacuous (it passes even
14
+ * with the mutex removed). Real async backends (e.g. Postgres) have this
15
+ * boundary intrinsically; the in-memory store injects it only under test.
16
+ *
17
+ * `suppressWake` stops `append` publishing to the hub, so no wake ever reaches a
18
+ * live subscriber and the only path left to a delivery is the subscribe machine's
19
+ * periodic poll. It defaults to `false` — the shipped store always wakes — and it
20
+ * earns its place for the same reason `beforeCommit` does: the machine's stated
21
+ * guarantee is that the POLL and not the wake source is what makes delivery correct,
22
+ * and every ordinary subscribe test could be passing purely via the hub, which
23
+ * leaves that load-bearing claim asserted only in a comment. Suppressing the PUBLISH
24
+ * rather than the machine's `listen` keeps `subscribe`'s wiring identical to
25
+ * production's: what changes is that the wake never fires, which is exactly what a
26
+ * wake source that died silently does to a real engine.
27
+ *
28
+ * `catchUpPageSize` lowers the catch-up page size the subscribe machine runs this
29
+ * store's subscriptions at, defaulting to core's `DEFAULT_CATCH_UP_PAGE_SIZE` — and it
30
+ * earns its place the same way its siblings here do. At that default a catch-up drains
31
+ * in ONE page for any backlog a test would write, so the page loop's exit condition is
32
+ * taken on the FIRST page every time and `fetchPage`'s whole reason for existing is a
33
+ * claim about a boundary nothing reaches. Lowering the page brings the SECOND one
34
+ * within reach: a store that cannot be made to page cannot be asked whether it pages
35
+ * correctly.
36
+ *
37
+ * `pollInterval` lowers the ceiling on how long the subscribe machine waits for a
38
+ * wake before re-reading from its cursor anyway (milliseconds), defaulting to core's
39
+ * `DEFAULT_POLL_INTERVAL_MILLIS`. The oracle takes its subscribe tuning from exactly
40
+ * where the engines it judges take theirs, so a figure chosen here instead would be
41
+ * this store drifting from all of them by a factor nothing outside its own tests ever
42
+ * asked for.
43
+ *
44
+ * A seam rather than a shorter shipped default, and what that costs is MEASURED
45
+ * rather than assumed: at core's default, NO subscribe case in the shared contract
46
+ * suite reaches a poll timeout against this store. Those cases run on a real clock
47
+ * and finish in a few hundred milliseconds, and the hub wake carries every delivery
48
+ * they assert, so the poll — the half that carries delivery whenever no wake is
49
+ * heard — is never what delivered one. A case that wants the POLL to deliver here
50
+ * must therefore ask for an interval short enough to elapse inside it, and taking the
51
+ * wake away is what makes such a case prove anything. Asking is cheap: the machine
52
+ * treats the interval as a CEILING on a wait and never as a tick rate, so a wake
53
+ * still returns at once and only an IDLE subscription pays — one `fetch` per
54
+ * interval, which here is a binary search plus a bounded scan over an array.
55
+ */
56
+ export interface InMemoryStoreOptions {
57
+ readonly beforeCommit?: Effect.Effect<void>;
58
+ readonly suppressWake?: boolean;
59
+ readonly catchUpPageSize?: number;
60
+ readonly pollInterval?: number;
61
+ }
62
+ /** The in-memory `DcbEventStore` layer (production; no test seam). */
63
+ export declare const DcbEventStoreInMemory: Layer.Layer<DcbEventStore>;
64
+ /**
65
+ * Test-only in-memory layer factory: the same store as `DcbEventStoreInMemory`, but
66
+ * with `InMemoryStoreOptions`' seams open. Kept off the package's public surface
67
+ * (deliberately not re-exported from `index.ts`); production code always uses
68
+ * `DcbEventStoreInMemory`. Every seam exists to make a claim about this store
69
+ * FALSIFIABLE rather than merely commented — the store contract suite forces the
70
+ * check/commit interleaving the exactly-one-wins case needs, one core test takes the
71
+ * wake away and shortens the poll so that what delivers can only be the poll, and
72
+ * another lowers the catch-up page to reach the second-page boundary the shipped page
73
+ * size puts out of reach.
74
+ */
75
+ export declare const makeInMemoryTestLayer: (options: InMemoryStoreOptions) => Layer.Layer<DcbEventStore>;
76
+ //# sourceMappingURL=inMemory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemory.d.ts","sourceRoot":"","sources":["../../src/inMemory.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAY,MAAM,EAAE,KAAK,EAAkC,MAAM,QAAQ,CAAA;AAIhF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAuL/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAA;IAC/B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B;AAuID,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,KAAK,CAAC,aAAa,CAG5D,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,GAChC,SAAS,oBAAoB,KAC5B,KAAK,CAAC,KAAK,CAAC,aAAa,CAC+C,CAAA"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @kairos-es/core: DCB-first event-sourcing contract and in-memory store for
3
+ * Effect.
4
+ *
5
+ * Exposes the domain types (`DcbEvent`, `SequencedEvent`, `Position`, `Tag`,
6
+ * `EventType`), the selection language (`Query`, `QueryItem`, the pure match
7
+ * predicate + smart constructors, the servable grammar in both its dispositions —
8
+ * `assertServableQuery`'s throw and `servableQueryFault`'s total `string |
9
+ * undefined` — and the servable-shape dispatch every engine compiles its plans
10
+ * through), the `AppendCondition`, tagged errors, the
11
+ * `DcbEventStore` contract, `AppendIsOwned` and the refusal every engine's `append`
12
+ * inherits from it, the in-memory layer, the shared subscribe machine
13
+ * every engine's `subscribe` is built from (`SubscribeMachine.ts`), the pure
14
+ * projection primitives (`Projection`, `DecodedEvent`, `composeProjections` and the
15
+ * `unionQueries` its query merge is reachable through without a record), and
16
+ * `retryOnConflict`.
17
+ *
18
+ * Core keeps only the *pure* fold: projections fold an already-`DecodedEvent`
19
+ * (decoding needs `Schema` + a `Serializer`, which core must not depend on).
20
+ * The event-def builder and the decode-aware `buildDecisionModel` live in
21
+ * `@kairos-es/codec`.
22
+ *
23
+ * `retryOnConflict` is here because it is the recovery half of the append
24
+ * condition — the only consistency mechanism there is. It matches the
25
+ * `AppendConditionFailed` core defines and needs nothing beyond `effect`'s
26
+ * `Schedule`, which core already peers, so homing it downstream only made a
27
+ * write-only consumer depend on a package it uses nothing else of (ADR-0008).
28
+ */
29
+ export * from './AppendCondition.js';
30
+ export * from './AppendIsOwned.js';
31
+ export * from './ContractStore.js';
32
+ export * from './DcbEventStore.js';
33
+ export * from './Event.js';
34
+ export * from './errors.js';
35
+ export { DcbEventStoreInMemory } from './inMemory.js';
36
+ export * from './Projection.js';
37
+ export * from './Query.js';
38
+ export * from './retryOnConflict.js';
39
+ export * from './SubscribeMachine.js';
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AAIxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAClD,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }