@kairos-es/core 1.0.0-alpha.4 → 1.0.0-alpha.5
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 +3 -3
- package/dist/cjs/AppendCondition.js +1 -3
- package/dist/cjs/AppendCondition.js.map +1 -1
- package/dist/cjs/AppendIsOwned.js +6 -6
- package/dist/cjs/AppendIsOwned.js.map +1 -1
- package/dist/cjs/ContractStore.js +2 -2
- package/dist/cjs/ContractStore.js.map +1 -1
- package/dist/cjs/DcbEventStore.js +1 -1
- package/dist/cjs/DcbEventStore.js.map +1 -1
- package/dist/cjs/Event.js +29 -13
- package/dist/cjs/Event.js.map +1 -1
- package/dist/cjs/Query.js +1 -1
- package/dist/cjs/Query.js.map +1 -1
- package/dist/cjs/SubscribeMachine.js +20 -22
- package/dist/cjs/SubscribeMachine.js.map +1 -1
- package/dist/cjs/inMemory.js +4 -4
- package/dist/cjs/inMemory.js.map +1 -1
- package/dist/cjs/retryOnConflict.js +4 -10
- package/dist/cjs/retryOnConflict.js.map +1 -1
- package/dist/cjs/testing.js +2 -2
- package/dist/cjs/testing.js.map +1 -1
- package/dist/{dts → dts-cjs}/AppendCondition.d.ts +5 -7
- package/dist/{dts → dts-cjs}/AppendCondition.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/AppendIsOwned.d.ts +6 -6
- package/dist/{dts → dts-cjs}/AppendIsOwned.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/ContractStore.d.ts +3 -2
- package/dist/dts-cjs/ContractStore.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/DcbEventStore.d.ts +2 -2
- package/dist/{dts → dts-cjs}/DcbEventStore.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/Event.d.ts +31 -20
- package/dist/dts-cjs/Event.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/Query.d.ts +5 -5
- package/dist/{dts → dts-cjs}/SubscribeMachine.d.ts +11 -15
- package/dist/dts-cjs/SubscribeMachine.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/inMemory.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/index.d.ts +1 -1
- package/dist/dts-cjs/package.json +3 -0
- package/dist/{dts → dts-cjs}/retryOnConflict.d.ts +1 -7
- package/dist/dts-cjs/retryOnConflict.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/testing.d.ts +1 -1
- package/dist/dts-esm/AppendCondition.d.ts +21 -0
- package/dist/dts-esm/AppendCondition.d.ts.map +1 -0
- package/dist/dts-esm/AppendIsOwned.d.ts +78 -0
- package/dist/dts-esm/AppendIsOwned.d.ts.map +1 -0
- package/dist/dts-esm/ContractStore.d.ts +62 -0
- package/dist/dts-esm/ContractStore.d.ts.map +1 -0
- package/dist/dts-esm/DcbEventStore.d.ts +64 -0
- package/dist/dts-esm/DcbEventStore.d.ts.map +1 -0
- package/dist/dts-esm/Event.d.ts +174 -0
- package/dist/dts-esm/Event.d.ts.map +1 -0
- package/dist/dts-esm/Projection.d.ts +124 -0
- package/dist/dts-esm/Projection.d.ts.map +1 -0
- package/dist/dts-esm/Query.d.ts +180 -0
- package/dist/dts-esm/Query.d.ts.map +1 -0
- package/dist/dts-esm/SubscribeMachine.d.ts +175 -0
- package/dist/dts-esm/SubscribeMachine.d.ts.map +1 -0
- package/dist/dts-esm/errors.d.ts +17 -0
- package/dist/dts-esm/errors.d.ts.map +1 -0
- package/dist/dts-esm/inMemory.d.ts +76 -0
- package/dist/dts-esm/inMemory.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +40 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/retryOnConflict.d.ts +64 -0
- package/dist/dts-esm/retryOnConflict.d.ts.map +1 -0
- package/dist/dts-esm/testing.d.ts +39 -0
- package/dist/dts-esm/testing.d.ts.map +1 -0
- package/dist/esm/AppendCondition.js +1 -3
- package/dist/esm/AppendCondition.js.map +1 -1
- package/dist/esm/AppendIsOwned.js +6 -6
- package/dist/esm/AppendIsOwned.js.map +1 -1
- package/dist/esm/ContractStore.js +2 -2
- package/dist/esm/ContractStore.js.map +1 -1
- package/dist/esm/DcbEventStore.js +1 -1
- package/dist/esm/DcbEventStore.js.map +1 -1
- package/dist/esm/Event.js +30 -14
- package/dist/esm/Event.js.map +1 -1
- package/dist/esm/Query.js +1 -1
- package/dist/esm/Query.js.map +1 -1
- package/dist/esm/SubscribeMachine.js +21 -23
- package/dist/esm/SubscribeMachine.js.map +1 -1
- package/dist/esm/inMemory.js +5 -5
- package/dist/esm/inMemory.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/retryOnConflict.js +4 -10
- package/dist/esm/retryOnConflict.js.map +1 -1
- package/dist/esm/testing.js +2 -2
- package/dist/esm/testing.js.map +1 -1
- package/package.json +21 -11
- package/src/AppendCondition.ts +1 -1
- package/src/AppendIsOwned.ts +7 -7
- package/src/ContractStore.ts +5 -4
- package/src/DcbEventStore.ts +3 -3
- package/src/Event.ts +49 -14
- package/src/Query.ts +1 -1
- package/src/SubscribeMachine.ts +38 -31
- package/src/inMemory.ts +6 -6
- package/src/index.ts +1 -1
- package/src/retryOnConflict.ts +6 -11
- package/src/testing.ts +2 -2
- package/testing/package.json +1 -1
- package/dist/dts/ContractStore.d.ts.map +0 -1
- package/dist/dts/Event.d.ts.map +0 -1
- package/dist/dts/SubscribeMachine.d.ts.map +0 -1
- package/dist/dts/retryOnConflict.d.ts.map +0 -1
- /package/dist/{dts → dts-cjs}/Projection.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/Projection.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/Query.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/errors.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/errors.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/inMemory.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/index.d.ts.map +0 -0
- /package/dist/{dts → dts-cjs}/testing.d.ts.map +0 -0
package/src/DcbEventStore.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `DcbEventStore` contract: a `Context.
|
|
2
|
+
* The `DcbEventStore` contract: a `Context.Service` with no bundled implementation.
|
|
3
3
|
* Consumers supply a `Layer` (the in-memory one lives in this package; each SQL
|
|
4
4
|
* engine is a package of its own — `@kairos-es/store-postgres` and
|
|
5
5
|
* `@kairos-es/store-sqlite`).
|
|
@@ -42,7 +42,7 @@ export interface ReadResponse {
|
|
|
42
42
|
readonly head: Position
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export class DcbEventStore extends Context.
|
|
45
|
+
export class DcbEventStore extends Context.Service<
|
|
46
46
|
DcbEventStore,
|
|
47
47
|
{
|
|
48
48
|
/**
|
|
@@ -74,4 +74,4 @@ export class DcbEventStore extends Context.Tag('@kairos-es/core/DcbEventStore')<
|
|
|
74
74
|
after?: Position,
|
|
75
75
|
) => Stream.Stream<SequencedEvent, never, Scope.Scope>
|
|
76
76
|
}
|
|
77
|
-
>() {}
|
|
77
|
+
>()('@kairos-es/core/DcbEventStore') {}
|
package/src/Event.ts
CHANGED
|
@@ -6,19 +6,55 @@
|
|
|
6
6
|
* payloads into these bytes. Ordering is by `position` alone; `occurredAt` is
|
|
7
7
|
* producer-supplied domain time and is informational only.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
BigInt as BigInt_,
|
|
11
|
+
Effect,
|
|
12
|
+
Option,
|
|
13
|
+
Schema,
|
|
14
|
+
SchemaIssue,
|
|
15
|
+
SchemaTransformation,
|
|
16
|
+
} from 'effect'
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* A position in the single global event log: a branded, non-negative `bigint`.
|
|
13
20
|
*
|
|
14
21
|
* The log is 64-bit (`bigserial` in Postgres) and exceeds JS `number` safety at
|
|
15
22
|
* 2^53, so positions are a `bigint`. They are compared, never arithmetic'd.
|
|
16
|
-
*
|
|
23
|
+
* Encoded, a position is a decimal string (JSON-safe, `String(b)`); decoded, a
|
|
17
24
|
* `bigint`. `0n` is the "before the first event" sentinel used by `after` and by
|
|
18
25
|
* an empty store's head.
|
|
26
|
+
*
|
|
27
|
+
* The string-to-`bigint` step is hand-built rather than Effect 4's
|
|
28
|
+
* `Schema.BigIntFromString`, because that schema accepts only `^-?\d+$` and so
|
|
29
|
+
* refuses inputs the encoded side has always admitted — `" 5"`, `"+5"`,
|
|
30
|
+
* `"0x10"`. The decode here is the global `BigInt(s)` behind a blank-string
|
|
31
|
+
* refusal, which is `BigInt.fromString` and is exactly what Effect 3's
|
|
32
|
+
* `Schema.BigInt` ran, so a position string any earlier release decoded still
|
|
33
|
+
* decodes to the same value. The non-negative check then runs on the `bigint`,
|
|
34
|
+
* as it always did.
|
|
19
35
|
*/
|
|
20
|
-
export const Position = Schema.
|
|
21
|
-
Schema.
|
|
36
|
+
export const Position = Schema.String.pipe(
|
|
37
|
+
Schema.decodeTo(
|
|
38
|
+
Schema.BigInt,
|
|
39
|
+
SchemaTransformation.transformEffect<bigint, string>({
|
|
40
|
+
decode: (s, options) =>
|
|
41
|
+
Option.match(BigInt_.fromString(s), {
|
|
42
|
+
onNone: () =>
|
|
43
|
+
Effect.fail(
|
|
44
|
+
new SchemaIssue.InvalidValue(
|
|
45
|
+
{
|
|
46
|
+
message: `Unable to decode ${JSON.stringify(s)} into a bigint`,
|
|
47
|
+
},
|
|
48
|
+
s,
|
|
49
|
+
options,
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
onSome: Effect.succeed,
|
|
53
|
+
}),
|
|
54
|
+
encode: (b) => Effect.succeed(String(b)),
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
Schema.check(Schema.isGreaterThanOrEqualToBigInt(0n)),
|
|
22
58
|
Schema.brand('Position'),
|
|
23
59
|
)
|
|
24
60
|
export type Position = typeof Position.Type
|
|
@@ -33,7 +69,7 @@ export const ORIGIN: Position = Position.make(0n)
|
|
|
33
69
|
* item's tags (superset test).
|
|
34
70
|
*/
|
|
35
71
|
export const Tag = Schema.String.pipe(
|
|
36
|
-
Schema.
|
|
72
|
+
Schema.check(Schema.isPattern(/^[^:]+:.+/, { identifier: 'Tag' })),
|
|
37
73
|
Schema.brand('Tag'),
|
|
38
74
|
)
|
|
39
75
|
export type Tag = typeof Tag.Type
|
|
@@ -95,7 +131,7 @@ export const tagIdentity = (key: string): TagIdentity => {
|
|
|
95
131
|
* `ReadLimit.make(n)`.
|
|
96
132
|
*/
|
|
97
133
|
export const ReadLimit = Schema.Int.pipe(
|
|
98
|
-
Schema.
|
|
134
|
+
Schema.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
99
135
|
Schema.brand('ReadLimit'),
|
|
100
136
|
)
|
|
101
137
|
export type ReadLimit = typeof ReadLimit.Type
|
|
@@ -105,7 +141,7 @@ export type ReadLimit = typeof ReadLimit.Type
|
|
|
105
141
|
* in the name (e.g. `course-registered.v2`); only non-emptiness is enforced.
|
|
106
142
|
*/
|
|
107
143
|
export const EventType = Schema.String.pipe(
|
|
108
|
-
Schema.
|
|
144
|
+
Schema.check(Schema.isNonEmpty()),
|
|
109
145
|
Schema.brand('EventType'),
|
|
110
146
|
)
|
|
111
147
|
export type EventType = typeof EventType.Type
|
|
@@ -159,10 +195,7 @@ export type EventType = typeof EventType.Type
|
|
|
159
195
|
* alone: it would make an event the store accepted unreadable for ever, so a
|
|
160
196
|
* value check belongs at write or nowhere.
|
|
161
197
|
*/
|
|
162
|
-
export const Metadata = Schema.Record(
|
|
163
|
-
key: Schema.String,
|
|
164
|
-
value: Schema.String,
|
|
165
|
-
})
|
|
198
|
+
export const Metadata = Schema.Record(Schema.String, Schema.String)
|
|
166
199
|
export type Metadata = typeof Metadata.Type
|
|
167
200
|
|
|
168
201
|
/**
|
|
@@ -172,6 +205,8 @@ export type Metadata = typeof Metadata.Type
|
|
|
172
205
|
*
|
|
173
206
|
* `data` is `Uint8Array` (opaque bytes); the store does not interpret it.
|
|
174
207
|
* `uuid` is producer-supplied to keep `core` deterministic (no RNG in core).
|
|
208
|
+
* It is checked for UUID SHAPE only (`isGUID`), not for a version nibble: a
|
|
209
|
+
* stricter check would refuse ids an existing log already holds.
|
|
175
210
|
* `occurredAt` is informational only; ordering is by position alone.
|
|
176
211
|
* `metadata` mirrors `tags` rather than `data`: REQUIRED, with an empty bag
|
|
177
212
|
* legal. Required because an absent-versus-empty divergence between engines is
|
|
@@ -181,10 +216,10 @@ export type Metadata = typeof Metadata.Type
|
|
|
181
216
|
*/
|
|
182
217
|
export const DcbEvent = Schema.Struct({
|
|
183
218
|
type: EventType,
|
|
184
|
-
data: Schema.
|
|
219
|
+
data: Schema.Uint8Array,
|
|
185
220
|
tags: Schema.Array(Tag),
|
|
186
|
-
uuid: Schema.
|
|
187
|
-
occurredAt: Schema.
|
|
221
|
+
uuid: Schema.String.check(Schema.isGUID()),
|
|
222
|
+
occurredAt: Schema.DateTimeUtcFromString,
|
|
188
223
|
metadata: Metadata,
|
|
189
224
|
})
|
|
190
225
|
export type DcbEvent = typeof DcbEvent.Type
|
package/src/Query.ts
CHANGED
|
@@ -273,7 +273,7 @@ export const classifyServableQuery = (query: Query): ServableQueryShape => {
|
|
|
273
273
|
}
|
|
274
274
|
const only = query.items.length === 1 ? query.items[0] : undefined
|
|
275
275
|
if (only !== undefined && only.tags.length === 0) {
|
|
276
|
-
return Arr.
|
|
276
|
+
return Arr.isReadonlyArrayNonEmpty(only.types)
|
|
277
277
|
? { kind: 'typeOnly', types: only.types }
|
|
278
278
|
: { kind: 'wildcard' }
|
|
279
279
|
}
|
package/src/SubscribeMachine.ts
CHANGED
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
* where an ephemeral view re-catches-up the entire log on every boot.
|
|
70
70
|
*/
|
|
71
71
|
import {
|
|
72
|
-
Chunk,
|
|
73
72
|
Effect,
|
|
74
73
|
Option,
|
|
75
74
|
Queue,
|
|
@@ -120,12 +119,12 @@ export const DEFAULT_POLL_INTERVAL_MILLIS = 1000
|
|
|
120
119
|
* the key NAME, the VALIDATION and the DEFAULT from one place, so a caller who has
|
|
121
120
|
* tuned one engine's subscribe reads another's without translating, and the two
|
|
122
121
|
* cannot drift in what they ACCEPT any more than in what they assume. Two engines
|
|
123
|
-
* hand-authoring the identical `Schema.
|
|
122
|
+
* hand-authoring the identical `Schema.optionalKey(…)` twice were identical only
|
|
124
123
|
* by inspection.
|
|
125
124
|
*
|
|
126
125
|
* The bounds differ between the two because the quantities do. `pollInterval` is a
|
|
127
|
-
* duration, so a fraction of a millisecond is meaningful and only `
|
|
128
|
-
* `
|
|
126
|
+
* duration, so a fraction of a millisecond is meaningful and only `isGreaterThan(0)`
|
|
127
|
+
* and `isFinite` matter: at zero or below the loop would spin without ever waiting for a
|
|
129
128
|
* wake, and at `Infinity` the periodic re-read — the half that carries delivery —
|
|
130
129
|
* would never come, which silently converts the wake source from an optimisation
|
|
131
130
|
* into the guarantee. `catchUpPageSize` is a COUNT, so it must be an integer
|
|
@@ -133,18 +132,17 @@ export const DEFAULT_POLL_INTERVAL_MILLIS = 1000
|
|
|
133
132
|
* length against it to decide the tail is drained, and a page of zero would report
|
|
134
133
|
* "drained" forever without reading anything.
|
|
135
134
|
*
|
|
136
|
-
* `
|
|
135
|
+
* `optionalKey` on both keeps the decoded type at `key?: T` rather than
|
|
137
136
|
* `T | undefined`, matching the repo's `exactOptionalPropertyTypes` discipline — an
|
|
138
137
|
* absent key, never an explicit `undefined`.
|
|
139
138
|
*/
|
|
140
139
|
export const subscriptionTuningFields = {
|
|
141
|
-
pollInterval: Schema.
|
|
142
|
-
Schema.Number.
|
|
143
|
-
|
|
140
|
+
pollInterval: Schema.optionalKey(
|
|
141
|
+
Schema.Number.check(Schema.isGreaterThan(0), Schema.isFinite()),
|
|
142
|
+
),
|
|
143
|
+
catchUpPageSize: Schema.optionalKey(
|
|
144
|
+
Schema.Int.check(Schema.isGreaterThan(0)),
|
|
144
145
|
),
|
|
145
|
-
catchUpPageSize: Schema.optionalWith(Schema.Int.pipe(Schema.positive()), {
|
|
146
|
-
exact: true,
|
|
147
|
-
}),
|
|
148
146
|
} as const
|
|
149
147
|
|
|
150
148
|
/**
|
|
@@ -154,10 +152,13 @@ export const subscriptionTuningFields = {
|
|
|
154
152
|
* past the cap propagates (the decorator dies it) — infrastructure stays off the
|
|
155
153
|
* contract channel while the stream survives ordinary transients.
|
|
156
154
|
*/
|
|
157
|
-
const POLL_RETRY_SCHEDULE = Schedule.
|
|
158
|
-
Schedule.
|
|
159
|
-
|
|
160
|
-
)
|
|
155
|
+
const POLL_RETRY_SCHEDULE = Schedule.max([
|
|
156
|
+
Schedule.min([
|
|
157
|
+
Schedule.exponential('100 millis', 2),
|
|
158
|
+
Schedule.spaced('2 seconds'),
|
|
159
|
+
]),
|
|
160
|
+
Schedule.recurs(20),
|
|
161
|
+
])
|
|
161
162
|
|
|
162
163
|
/**
|
|
163
164
|
* The two seams the subscribe machine runs over, plus the two tuning knobs. `E` is
|
|
@@ -166,7 +167,7 @@ const POLL_RETRY_SCHEDULE = Schedule.exponential('100 millis', 2).pipe(
|
|
|
166
167
|
*
|
|
167
168
|
* ONE type parameter and not two, deliberately: a wake source with an error type of
|
|
168
169
|
* its own is `Stream.orDie`d by the caller, yielding a `Stream<unknown, never>`
|
|
169
|
-
* that is assignable here under Effect
|
|
170
|
+
* that is assignable here under Effect's covariant error channel. A second
|
|
170
171
|
* parameter would buy nothing and would have to be spelled at every call site. Nor
|
|
171
172
|
* is there an `R`: neither seam contributes a requirement, and unused generality is
|
|
172
173
|
* not free.
|
|
@@ -199,17 +200,17 @@ export interface SubscriptionDeps<E> {
|
|
|
199
200
|
|
|
200
201
|
/**
|
|
201
202
|
* Build the subscription `Stream`: catch up from `after`, then track the live tail.
|
|
202
|
-
* The cursor threads through the `
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* on close. An `E` mid-stream stays on the channel; `makeContractStore` dies it
|
|
203
|
+
* The cursor threads through the `Stream.paginate` state, starting at `after` and
|
|
204
|
+
* advancing to the last delivered position each batch; the stream never completes
|
|
205
|
+
* on its own (the next state is always `Option.some`), so the enclosing scope
|
|
206
|
+
* interrupts it on close. An `E` mid-stream stays on the channel; `makeContractStore` dies it
|
|
206
207
|
* (subscribe is `E = never` on the contract).
|
|
207
208
|
*/
|
|
208
209
|
export const subscribeStream = <E>(
|
|
209
210
|
deps: SubscriptionDeps<E>,
|
|
210
211
|
after: Position | undefined,
|
|
211
212
|
): Stream.Stream<SequencedEvent, E, Scope.Scope> =>
|
|
212
|
-
Stream.
|
|
213
|
+
Stream.unwrap(
|
|
213
214
|
Effect.gen(function* () {
|
|
214
215
|
// The wake primitive: a sliding(1) queue holding at most one pending
|
|
215
216
|
// signal. The wake source `offer`s an opaque wake on every notification;
|
|
@@ -230,9 +231,10 @@ export const subscribeStream = <E>(
|
|
|
230
231
|
const listener = (deps.listen ?? Stream.never).pipe(
|
|
231
232
|
Stream.tap(() => Queue.offer(wake, undefined)),
|
|
232
233
|
Stream.retry(
|
|
233
|
-
Schedule.
|
|
234
|
-
Schedule.
|
|
235
|
-
|
|
234
|
+
Schedule.min([
|
|
235
|
+
Schedule.exponential('50 millis'),
|
|
236
|
+
Schedule.spaced('1 seconds'),
|
|
237
|
+
]),
|
|
236
238
|
),
|
|
237
239
|
)
|
|
238
240
|
// Drain the wake source into the wake queue on a background fibre; the
|
|
@@ -280,7 +282,7 @@ export const subscribeStream = <E>(
|
|
|
280
282
|
|
|
281
283
|
/**
|
|
282
284
|
* Produce the NEXT non-empty batch from a cursor position, together with
|
|
283
|
-
* the advanced cursor — a pure `Position → Effect<[
|
|
285
|
+
* the advanced cursor — a pure `Position → Effect<[events, next]>`. The
|
|
284
286
|
* `> boundary` de-dup is enforced by `fetch`'s exclusive lower bound, so a
|
|
285
287
|
* live re-read after a wake never re-emits an already-delivered position.
|
|
286
288
|
*
|
|
@@ -288,18 +290,20 @@ export const subscribeStream = <E>(
|
|
|
288
290
|
* the cursor. If a drain yields nothing, wait for a wake but only up to the
|
|
289
291
|
* poll interval, then loop and re-read ANYWAY — the periodic position-based
|
|
290
292
|
* poll that makes delivery correct independently of any wake. This effect
|
|
291
|
-
* only completes by returning a non-empty
|
|
293
|
+
* only completes by returning a non-empty batch; the scope interrupts it on
|
|
292
294
|
* close.
|
|
293
295
|
*/
|
|
294
296
|
const nextBatch = (
|
|
295
297
|
from: Position,
|
|
296
|
-
): Effect.Effect<readonly [
|
|
298
|
+
): Effect.Effect<readonly [ReadonlyArray<SequencedEvent>, Position], E> =>
|
|
297
299
|
Effect.gen(function* () {
|
|
298
300
|
while (true) {
|
|
299
|
-
|
|
301
|
+
// `Queue.clear` and not `Queue.takeAll`: the drain must NOT wait, and
|
|
302
|
+
// `takeAll` suspends until at least one signal is present.
|
|
303
|
+
yield* Queue.clear(wake)
|
|
300
304
|
const { events, last } = yield* drainFrom(from)
|
|
301
305
|
if (events.length > 0) {
|
|
302
|
-
return [
|
|
306
|
+
return [events, last] as const
|
|
303
307
|
}
|
|
304
308
|
// Nothing new: block on a wake, but only up to the poll interval — on
|
|
305
309
|
// timeout we fall through and the loop re-reads regardless.
|
|
@@ -309,8 +313,11 @@ export const subscribeStream = <E>(
|
|
|
309
313
|
}
|
|
310
314
|
})
|
|
311
315
|
|
|
312
|
-
return Stream.
|
|
313
|
-
Effect.map(
|
|
316
|
+
return Stream.paginate(after ?? ORIGIN, (from) =>
|
|
317
|
+
Effect.map(
|
|
318
|
+
nextBatch(from),
|
|
319
|
+
([events, last]) => [events, Option.some(last)] as const,
|
|
320
|
+
),
|
|
314
321
|
)
|
|
315
322
|
}),
|
|
316
323
|
)
|
package/src/inMemory.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The log is a `Ref<ReadonlyArray<SequencedEvent>>` in ascending position
|
|
6
6
|
* order. The whole check-then-append is run inside a single
|
|
7
|
-
* `
|
|
7
|
+
* `Semaphore.make(1)`+`withPermits(1)` critical section: the conditional
|
|
8
8
|
* read and the commit must not be separable, and that atomicity is the entire
|
|
9
9
|
* concurrency guarantee (exactly-one-wins under a race). Positions come from a
|
|
10
10
|
* monotonic counter starting at 1n (contiguous here; the contract only promises
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* discharges the ascending-commit-order precondition `SubscribeMachine.ts` states.
|
|
26
26
|
*/
|
|
27
27
|
import type { Array as Arr, Scope } from 'effect'
|
|
28
|
-
import { DateTime, Effect, Layer, PubSub, Ref, Stream } from 'effect'
|
|
28
|
+
import { DateTime, Effect, Layer, PubSub, Ref, Semaphore, Stream } from 'effect'
|
|
29
29
|
import type { AppendCondition } from './AppendCondition'
|
|
30
30
|
import { makeContractStore, type RawDcbEventStore } from './ContractStore'
|
|
31
31
|
import type { ReadOptions, ReadResponse } from './DcbEventStore'
|
|
@@ -73,7 +73,7 @@ const copyEvent = (event: DcbEvent): DcbEvent => ({
|
|
|
73
73
|
data: new Uint8Array(event.data),
|
|
74
74
|
tags: [...event.tags],
|
|
75
75
|
uuid: event.uuid,
|
|
76
|
-
occurredAt: DateTime.
|
|
76
|
+
occurredAt: DateTime.makeUnsafe(DateTime.toEpochMillis(event.occurredAt)),
|
|
77
77
|
metadata: { ...event.metadata },
|
|
78
78
|
})
|
|
79
79
|
|
|
@@ -288,7 +288,7 @@ const make = (
|
|
|
288
288
|
options?.catchUpPageSize ?? DEFAULT_CATCH_UP_PAGE_SIZE
|
|
289
289
|
const pollInterval = options?.pollInterval ?? DEFAULT_POLL_INTERVAL_MILLIS
|
|
290
290
|
const log = yield* Ref.make<ReadonlyArray<SequencedEvent>>([])
|
|
291
|
-
const mutex = yield*
|
|
291
|
+
const mutex = yield* Semaphore.make(1)
|
|
292
292
|
const hub = yield* PubSub.unbounded<SequencedEvent>()
|
|
293
293
|
const nextPosition = yield* Ref.make<bigint>(1n)
|
|
294
294
|
|
|
@@ -381,7 +381,7 @@ const make = (
|
|
|
381
381
|
// exclusive cursor is what makes the catch-up/live seam exactly-once, so there
|
|
382
382
|
// is nothing left here for a critical section to protect. `append` keeps its
|
|
383
383
|
// permit, which is the whole exactly-one-wins guarantee.
|
|
384
|
-
Stream.
|
|
384
|
+
Stream.unwrap(
|
|
385
385
|
Effect.gen(function* () {
|
|
386
386
|
// Taken when the subscription starts, as the SQL engines compile theirs
|
|
387
387
|
// (see `copyQuery`).
|
|
@@ -393,7 +393,7 @@ const make = (
|
|
|
393
393
|
Effect.map(Ref.get(log), (snapshot) =>
|
|
394
394
|
fetchPage(snapshot, judged, from, limit),
|
|
395
395
|
),
|
|
396
|
-
listen: Stream.
|
|
396
|
+
listen: Stream.fromSubscription(wakes),
|
|
397
397
|
pollInterval,
|
|
398
398
|
catchUpPageSize,
|
|
399
399
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @kairos-es/core: DCB-first event-sourcing contract and in-memory store for
|
|
3
|
-
* Effect
|
|
3
|
+
* Effect.
|
|
4
4
|
*
|
|
5
5
|
* Exposes the domain types (`DcbEvent`, `SequencedEvent`, `Position`, `Tag`,
|
|
6
6
|
* `EventType`), the selection language (`Query`, `QueryItem`, the pure match
|
package/src/retryOnConflict.ts
CHANGED
|
@@ -35,12 +35,6 @@
|
|
|
35
35
|
* jitter draws from `Random.next`, a default service on the same footing as the
|
|
36
36
|
* clock rather than a host `Math.random`. No `node:*`, no wall-clock reading, no
|
|
37
37
|
* ungoverned RNG.
|
|
38
|
-
*
|
|
39
|
-
* Effect v3 caveat: the `Schedule` API below is the THREE-type-parameter
|
|
40
|
-
* `Schedule<Out, In, R>` that `effect@3` exports, with `Schedule.intersect`. The
|
|
41
|
-
* currently-published docs show a newer four-type-parameter `Schedule` without
|
|
42
|
-
* `intersect` — do not copy them; this file is written against the installed
|
|
43
|
-
* `effect@3` type definitions.
|
|
44
38
|
*/
|
|
45
39
|
import { Effect, Predicate, Schedule } from 'effect'
|
|
46
40
|
import type { AppendConditionFailed } from './errors'
|
|
@@ -49,9 +43,9 @@ import type { AppendConditionFailed } from './errors'
|
|
|
49
43
|
* The default conflict backoff: exponential from 20ms, jittered, capped at five
|
|
50
44
|
* retries.
|
|
51
45
|
*
|
|
52
|
-
* `Schedule.
|
|
53
|
-
* `recurs(5)` arm is what bounds the attempts and the
|
|
54
|
-
* spaces them. Jitter matters more here than in a typical backoff: contending
|
|
46
|
+
* `Schedule.max` continues only while BOTH schedules continue, waiting the longer
|
|
47
|
+
* of their two delays, so the `recurs(5)` arm is what bounds the attempts and the
|
|
48
|
+
* exponential arm is what spaces them. Jitter matters more here than in a typical backoff: contending
|
|
55
49
|
* deciders conflict precisely because they read the same boundary at the same
|
|
56
50
|
* time, so an unjittered backoff would re-synchronise them into the same conflict
|
|
57
51
|
* on every round. The figures are the conventional starting point (Emmett,
|
|
@@ -59,10 +53,10 @@ import type { AppendConditionFailed } from './errors'
|
|
|
59
53
|
* command with a tighter latency budget, or one contending harder, should pass its
|
|
60
54
|
* own schedule.
|
|
61
55
|
*/
|
|
62
|
-
const conflictBackoff = Schedule.
|
|
56
|
+
const conflictBackoff = Schedule.max([
|
|
63
57
|
Schedule.jittered(Schedule.exponential('20 millis', 2)),
|
|
64
58
|
Schedule.recurs(5),
|
|
65
|
-
)
|
|
59
|
+
])
|
|
66
60
|
|
|
67
61
|
/**
|
|
68
62
|
* Retry `command` while — and only while — it fails `AppendConditionFailed`.
|
|
@@ -91,6 +85,7 @@ export const retryOnConflict = <A, E, R>(
|
|
|
91
85
|
schedule: Schedule.Schedule<
|
|
92
86
|
unknown,
|
|
93
87
|
AppendConditionFailed | E,
|
|
88
|
+
never,
|
|
94
89
|
R
|
|
95
90
|
> = conflictBackoff,
|
|
96
91
|
): Effect.Effect<A, AppendConditionFailed | E, R> =>
|
package/src/testing.ts
CHANGED
|
@@ -21,10 +21,10 @@ import { DateTime } from 'effect'
|
|
|
21
21
|
*
|
|
22
22
|
* Ordering in the log is by `position` alone, so an event's `occurredAt` is
|
|
23
23
|
* informational; pinning it to the epoch keeps serialised bytes and comparisons
|
|
24
|
-
* stable across runs. Built via `DateTime.
|
|
24
|
+
* stable across runs. Built via `DateTime.makeUnsafe(0)` (the value is a
|
|
25
25
|
* compile-time constant, not read from the host clock).
|
|
26
26
|
*/
|
|
27
|
-
export const EPOCH = DateTime.
|
|
27
|
+
export const EPOCH = DateTime.makeUnsafe(0)
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Build a fresh, deterministic uuid generator: a zero-based counter producing
|
package/testing/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ContractStore.d.ts","sourceRoot":"","sources":["../../src/ContractStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;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,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,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,CAoGrB,CAAA"}
|
package/dist/dts/Event.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,QAAQ,4DAGnB,CAAA;AACF,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ;;;;;;;EAOnB,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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SubscribeMachine.d.ts","sourceRoot":"","sources":["../../src/SubscribeMachine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,OAAO,EAEL,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;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,wBAAwB;;;;;;;CAQ3B,CAAA;AAcV;;;;;;;;;;;GAWG;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;;;;;;;GAOG;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,CAyG5C,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"retryOnConflict.d.ts","sourceRoot":"","sources":["../../src/retryOnConflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAAE,MAAM,EAAa,QAAQ,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAqBrD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EACrC,SAAS,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,qBAAqB,GAAG,CAAC,EAAE,CAAC,CAAC,EACvD,WAAU,QAAQ,CAAC,QAAQ,CACzB,OAAO,EACP,qBAAqB,GAAG,CAAC,EACzB,CAAC,CACgB,KAClB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,qBAAqB,GAAG,CAAC,EAAE,CAAC,CAI5C,CAAA"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|