@kairos-es/core 1.0.0-alpha.3 → 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 +13 -4
- 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 +87 -11
- package/dist/cjs/Event.js.map +1 -1
- package/dist/cjs/Projection.js +15 -9
- package/dist/cjs/Projection.js.map +1 -1
- package/dist/cjs/Query.js +59 -3
- 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 +113 -43
- 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 +7 -4
- 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-cjs/Event.d.ts +174 -0
- package/dist/dts-cjs/Event.d.ts.map +1 -0
- package/dist/{dts → dts-cjs}/Projection.d.ts +1 -1
- package/dist/{dts → dts-cjs}/Projection.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/Query.d.ts +19 -5
- package/dist/dts-cjs/Query.d.ts.map +1 -0
- 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 +13 -4
- 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 +87 -11
- package/dist/esm/Event.js.map +1 -1
- package/dist/esm/Projection.js +15 -9
- package/dist/esm/Projection.js.map +1 -1
- package/dist/esm/Query.js +56 -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 +114 -44
- 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 +25 -8
- package/src/DcbEventStore.ts +3 -3
- package/src/Event.ts +108 -11
- package/src/Projection.ts +19 -8
- package/src/Query.ts +74 -1
- package/src/SubscribeMachine.ts +38 -31
- package/src/inMemory.ts +123 -51
- 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 +0 -104
- package/dist/dts/Event.d.ts.map +0 -1
- package/dist/dts/Query.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}/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/dist/esm/inMemory.js
CHANGED
|
@@ -1,10 +1,71 @@
|
|
|
1
|
-
import { Effect, Layer, PubSub, Ref, Stream } from 'effect';
|
|
1
|
+
import { DateTime, Effect, Layer, PubSub, Ref, Semaphore, Stream } from 'effect';
|
|
2
2
|
import { makeContractStore } from "./ContractStore.js";
|
|
3
3
|
import { DcbEventStore } from "./DcbEventStore.js";
|
|
4
4
|
import { ORIGIN } from "./Event.js";
|
|
5
5
|
import { AppendConditionFailed } from "./errors.js";
|
|
6
6
|
import { matchesSequenced } from "./Query.js";
|
|
7
7
|
import { DEFAULT_CATCH_UP_PAGE_SIZE, DEFAULT_POLL_INTERVAL_MILLIS, subscribeStream } from "./SubscribeMachine.js";
|
|
8
|
+
/**
|
|
9
|
+
* A copy of `event` that shares no mutable structure with it: fresh `data` bytes, a
|
|
10
|
+
* fresh `tags` array, a fresh `metadata` bag and a fresh `occurredAt`. Only `type`
|
|
11
|
+
* and `uuid`, which are strings, are carried as they are.
|
|
12
|
+
*
|
|
13
|
+
* Each copy is chosen for what it does on EVERY input the type admits, not on the
|
|
14
|
+
* common one. `data` is copied through the `Uint8Array` constructor and not with
|
|
15
|
+
* `slice`, because `DcbEvent` admits any `Uint8Array` subclass and a subclass may
|
|
16
|
+
* answer `slice` with a VIEW of the same memory — Node's `Buffer` does. A
|
|
17
|
+
* `DateTime.Utc` is an ordinary object whose fields can be written past its
|
|
18
|
+
* `readonly` type, so `occurredAt` is rebuilt from its instant rather than shared.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the oracle copies at BOTH edges
|
|
21
|
+
*
|
|
22
|
+
* This store is the reference the SQL engines are judged against, and a SQL engine
|
|
23
|
+
* shares nothing with its caller: it serialises on the way in and builds fresh objects
|
|
24
|
+
* on the way out. A store that kept the caller's objects instead would let a caller
|
|
25
|
+
* rewrite the log AFTER the append — mutate a `Uint8Array`, push to a `tags` array,
|
|
26
|
+
* assign into a `metadata` bag — and every later read would see the rewritten event
|
|
27
|
+
* with no append, no position and no wake behind it. A reference that can be edited
|
|
28
|
+
* after the fact judges nothing.
|
|
29
|
+
*
|
|
30
|
+
* So `append` stores a copy (the log never holds a caller's object) and every read
|
|
31
|
+
* path hands out a copy (a reader cannot reach the log through what it was given).
|
|
32
|
+
* Copying on the way in alone would close the first route and leave the second open:
|
|
33
|
+
* the same object would then be shared by every reader of the log.
|
|
34
|
+
*/
|
|
35
|
+
const copyEvent = event => ({
|
|
36
|
+
type: event.type,
|
|
37
|
+
data: new Uint8Array(event.data),
|
|
38
|
+
tags: [...event.tags],
|
|
39
|
+
uuid: event.uuid,
|
|
40
|
+
occurredAt: DateTime.makeUnsafe(DateTime.toEpochMillis(event.occurredAt)),
|
|
41
|
+
metadata: {
|
|
42
|
+
...event.metadata
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* A copy of `query` that shares no array with it, taking only the keys the grammar
|
|
47
|
+
* defines.
|
|
48
|
+
*
|
|
49
|
+
* `subscribe` is the one verb that holds a query beyond its own call: the fetch seam
|
|
50
|
+
* judges it again on every page for as long as the subscription runs. Each SQL engine
|
|
51
|
+
* compiles its plan once, when the subscription starts, so an edit the caller makes
|
|
52
|
+
* to its own object afterwards reaches nothing there. The oracle has to answer the
|
|
53
|
+
* same, for the reason it copies events (above): a reference that the caller can
|
|
54
|
+
* change mid-stream judges nothing. Only `types` and `tags` are carried because they
|
|
55
|
+
* are all `matchesQueryItem` reads, and a key the grammar does not define was
|
|
56
|
+
* already refused at the contract decorator before this runs.
|
|
57
|
+
*/
|
|
58
|
+
const copyQuery = query => ({
|
|
59
|
+
items: query.items.map(item => ({
|
|
60
|
+
types: [...item.types],
|
|
61
|
+
tags: [...item.tags]
|
|
62
|
+
}))
|
|
63
|
+
});
|
|
64
|
+
/** A `SequencedEvent` whose event is a `copyEvent` of the stored one. */
|
|
65
|
+
const copySequenced = sequenced => ({
|
|
66
|
+
event: copyEvent(sequenced.event),
|
|
67
|
+
position: sequenced.position
|
|
68
|
+
});
|
|
8
69
|
/** Filter a snapshot by `query` and `after` (strict `position > after`). */
|
|
9
70
|
const filterLog = (log, query, after) => log.filter(sequenced => (after === undefined || sequenced.position > after) && matchesSequenced(sequenced, query));
|
|
10
71
|
/** The position of the last event in a slice, or `undefined` when empty. */
|
|
@@ -22,7 +83,7 @@ const readSnapshot = (log, query, options) => {
|
|
|
22
83
|
// when there is nothing to report.
|
|
23
84
|
const head = (options?.limit === undefined ? lastPosition(log) : lastPosition(limited)) ?? ORIGIN;
|
|
24
85
|
return {
|
|
25
|
-
events: Stream.fromIterable(limited),
|
|
86
|
+
events: Stream.fromIterable(limited.map(copySequenced)),
|
|
26
87
|
head
|
|
27
88
|
};
|
|
28
89
|
};
|
|
@@ -74,7 +135,7 @@ const fetchPage = (log, query, from, limit) => {
|
|
|
74
135
|
for (let index = firstIndexAfter(log, from); index < log.length && page.length < limit; index += 1) {
|
|
75
136
|
const sequenced = log[index];
|
|
76
137
|
if (matchesSequenced(sequenced, query)) {
|
|
77
|
-
page.push(sequenced);
|
|
138
|
+
page.push(copySequenced(sequenced));
|
|
78
139
|
}
|
|
79
140
|
}
|
|
80
141
|
return page;
|
|
@@ -91,7 +152,7 @@ const make = options => Effect.gen(function* () {
|
|
|
91
152
|
const catchUpPageSize = options?.catchUpPageSize ?? DEFAULT_CATCH_UP_PAGE_SIZE;
|
|
92
153
|
const pollInterval = options?.pollInterval ?? DEFAULT_POLL_INTERVAL_MILLIS;
|
|
93
154
|
const log = yield* Ref.make([]);
|
|
94
|
-
const mutex = yield*
|
|
155
|
+
const mutex = yield* Semaphore.make(1);
|
|
95
156
|
const hub = yield* PubSub.unbounded();
|
|
96
157
|
const nextPosition = yield* Ref.make(1n);
|
|
97
158
|
const read = (query, options) =>
|
|
@@ -99,45 +160,51 @@ const make = options => Effect.gen(function* () {
|
|
|
99
160
|
// is reentrant, so append's inner read re-enters. Here append inlines the
|
|
100
161
|
// read logic instead, so a plain lock suffices.
|
|
101
162
|
mutex.withPermits(1)(Effect.map(Ref.get(log), snapshot => readSnapshot(snapshot, query, options)));
|
|
102
|
-
const append = (events, condition) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
163
|
+
const append = (events, condition) => Effect.suspend(() => {
|
|
164
|
+
// The events are copied when the append STARTS, before it waits for the
|
|
165
|
+
// permit, as each SQL engine serialises them before its first suspension:
|
|
166
|
+
// an edit the caller makes while this append is queued must not reach the
|
|
167
|
+
// log here when it could not reach theirs.
|
|
168
|
+
const owned = events.map(copyEvent);
|
|
169
|
+
// Critical section: check + commit are atomic (the exactly-one-wins guarantee).
|
|
170
|
+
return mutex.withPermits(1)(Effect.gen(function* () {
|
|
171
|
+
const snapshot = yield* Ref.get(log);
|
|
172
|
+
if (condition !== undefined) {
|
|
173
|
+
const conflicts = filterLog(snapshot, condition.failIfEventsMatch, condition.after);
|
|
174
|
+
// Detect by presence: any matching event after `condition.after`
|
|
175
|
+
// fails the append. The error carries only the condition (see
|
|
176
|
+
// `AppendConditionFailed`); a correct retry re-reads to the new head.
|
|
177
|
+
if (conflicts.length > 0) {
|
|
178
|
+
return yield* new AppendConditionFailed({
|
|
179
|
+
condition
|
|
180
|
+
});
|
|
181
|
+
}
|
|
115
182
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
183
|
+
// Optional test seam between the conflict-check and the commit (see
|
|
184
|
+
// `InMemoryStoreOptions.beforeCommit`). Defaults to `Effect.void`, a
|
|
185
|
+
// synchronous no-op, so the shipped critical section stays atomic by
|
|
186
|
+
// construction; the concurrency contract test injects `Effect.yieldNow()`
|
|
187
|
+
// here to make the exactly-one-wins guard genuinely depend on the mutex.
|
|
188
|
+
yield* beforeCommit;
|
|
189
|
+
const start = yield* Ref.get(nextPosition);
|
|
190
|
+
const appended = owned.map((event, index) => ({
|
|
191
|
+
event,
|
|
192
|
+
position: start + BigInt(index)
|
|
193
|
+
}));
|
|
194
|
+
const end = start + BigInt(events.length);
|
|
195
|
+
yield* Ref.set(log, [...snapshot, ...appended]);
|
|
196
|
+
yield* Ref.set(nextPosition, end);
|
|
197
|
+
// The hub is a WAKE SOURCE and nothing else: the subscribe machine never
|
|
198
|
+
// reads what it carries, and its poll delivers with or without one. That
|
|
199
|
+
// is why suppressing the publish is a legitimate test seam rather than a
|
|
200
|
+
// broken store (see `InMemoryStoreOptions.suppressWake`).
|
|
201
|
+
if (!suppressWake) {
|
|
202
|
+
yield* PubSub.publishAll(hub, appended);
|
|
203
|
+
}
|
|
204
|
+
// `events` is non-empty, so the last new position is `end - 1`.
|
|
205
|
+
return end - 1n;
|
|
127
206
|
}));
|
|
128
|
-
|
|
129
|
-
yield* Ref.set(log, [...snapshot, ...appended]);
|
|
130
|
-
yield* Ref.set(nextPosition, end);
|
|
131
|
-
// The hub is a WAKE SOURCE and nothing else: the subscribe machine never
|
|
132
|
-
// reads what it carries, and its poll delivers with or without one. That
|
|
133
|
-
// is why suppressing the publish is a legitimate test seam rather than a
|
|
134
|
-
// broken store (see `InMemoryStoreOptions.suppressWake`).
|
|
135
|
-
if (!suppressWake) {
|
|
136
|
-
yield* PubSub.publishAll(hub, appended);
|
|
137
|
-
}
|
|
138
|
-
// `events` is non-empty, so the last new position is `end - 1`.
|
|
139
|
-
return end - 1n;
|
|
140
|
-
}));
|
|
207
|
+
});
|
|
141
208
|
const subscribe = (query, after) =>
|
|
142
209
|
// Take the hub seat HERE, before handing the machine its wake source, so the
|
|
143
210
|
// subscription is established no later than the machine's first `fetch`. That
|
|
@@ -152,11 +219,14 @@ const make = options => Effect.gen(function* () {
|
|
|
152
219
|
// exclusive cursor is what makes the catch-up/live seam exactly-once, so there
|
|
153
220
|
// is nothing left here for a critical section to protect. `append` keeps its
|
|
154
221
|
// permit, which is the whole exactly-one-wins guarantee.
|
|
155
|
-
Stream.
|
|
222
|
+
Stream.unwrap(Effect.gen(function* () {
|
|
223
|
+
// Taken when the subscription starts, as the SQL engines compile theirs
|
|
224
|
+
// (see `copyQuery`).
|
|
225
|
+
const judged = copyQuery(query);
|
|
156
226
|
const wakes = yield* PubSub.subscribe(hub);
|
|
157
227
|
return subscribeStream({
|
|
158
|
-
fetch: (from, limit) => Effect.map(Ref.get(log), snapshot => fetchPage(snapshot,
|
|
159
|
-
listen: Stream.
|
|
228
|
+
fetch: (from, limit) => Effect.map(Ref.get(log), snapshot => fetchPage(snapshot, judged, from, limit)),
|
|
229
|
+
listen: Stream.fromSubscription(wakes),
|
|
160
230
|
pollInterval,
|
|
161
231
|
catchUpPageSize
|
|
162
232
|
}, after);
|
package/dist/esm/inMemory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inMemory.js","names":["Effect","Layer","PubSub","Ref","Stream","makeContractStore","DcbEventStore","ORIGIN","AppendConditionFailed","matchesSequenced","DEFAULT_CATCH_UP_PAGE_SIZE","DEFAULT_POLL_INTERVAL_MILLIS","subscribeStream","
|
|
1
|
+
{"version":3,"file":"inMemory.js","names":["DateTime","Effect","Layer","PubSub","Ref","Semaphore","Stream","makeContractStore","DcbEventStore","ORIGIN","AppendConditionFailed","matchesSequenced","DEFAULT_CATCH_UP_PAGE_SIZE","DEFAULT_POLL_INTERVAL_MILLIS","subscribeStream","copyEvent","event","type","data","Uint8Array","tags","uuid","occurredAt","makeUnsafe","toEpochMillis","metadata","copyQuery","query","items","map","item","types","copySequenced","sequenced","position","filterLog","log","after","filter","undefined","lastPosition","events","at","readSnapshot","options","matched","limited","limit","slice","head","fromIterable","firstIndexAfter","low","high","length","mid","fetchPage","from","page","index","push","make","gen","beforeCommit","void","suppressWake","catchUpPageSize","pollInterval","mutex","hub","unbounded","nextPosition","read","withPermits","get","snapshot","append","condition","suspend","owned","conflicts","failIfEventsMatch","start","appended","BigInt","end","set","publishAll","subscribe","unwrap","judged","wakes","fetch","listen","fromSubscription","DcbEventStoreInMemory","effect","makeInMemoryTestLayer"],"sources":["../../src/inMemory.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
package/dist/esm/index.js
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
|
|
@@ -35,21 +35,15 @@
|
|
|
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
|
/**
|
|
47
41
|
* The default conflict backoff: exponential from 20ms, jittered, capped at five
|
|
48
42
|
* retries.
|
|
49
43
|
*
|
|
50
|
-
* `Schedule.
|
|
51
|
-
* `recurs(5)` arm is what bounds the attempts and the
|
|
52
|
-
* spaces them. Jitter matters more here than in a typical backoff: contending
|
|
44
|
+
* `Schedule.max` continues only while BOTH schedules continue, waiting the longer
|
|
45
|
+
* of their two delays, so the `recurs(5)` arm is what bounds the attempts and the
|
|
46
|
+
* exponential arm is what spaces them. Jitter matters more here than in a typical backoff: contending
|
|
53
47
|
* deciders conflict precisely because they read the same boundary at the same
|
|
54
48
|
* time, so an unjittered backoff would re-synchronise them into the same conflict
|
|
55
49
|
* on every round. The figures are the conventional starting point (Emmett,
|
|
@@ -57,7 +51,7 @@ import { Effect, Predicate, Schedule } from 'effect';
|
|
|
57
51
|
* command with a tighter latency budget, or one contending harder, should pass its
|
|
58
52
|
* own schedule.
|
|
59
53
|
*/
|
|
60
|
-
const conflictBackoff = /*#__PURE__*/Schedule.
|
|
54
|
+
const conflictBackoff = /*#__PURE__*/Schedule.max([/*#__PURE__*/Schedule.jittered(/*#__PURE__*/Schedule.exponential('20 millis', 2)), /*#__PURE__*/Schedule.recurs(5)]);
|
|
61
55
|
/**
|
|
62
56
|
* Retry `command` while — and only while — it fails `AppendConditionFailed`.
|
|
63
57
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retryOnConflict.js","names":["Effect","Predicate","Schedule","conflictBackoff","
|
|
1
|
+
{"version":3,"file":"retryOnConflict.js","names":["Effect","Predicate","Schedule","conflictBackoff","max","jittered","exponential","recurs","retryOnConflict","command","schedule","retry","while","isTagged"],"sources":["../../src/retryOnConflict.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAASA,MAAM,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAGpD;;;;;;;;;;;;;;AAcA,MAAMC,eAAe,gBAAGD,QAAQ,CAACE,GAAG,CAAC,cACnCF,QAAQ,CAACG,QAAQ,cAACH,QAAQ,CAACI,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,eACvDJ,QAAQ,CAACK,MAAM,CAAC,CAAC,CAAC,CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;AAsBA,OAAO,MAAMC,eAAe,GAAGA,CAC7BC,OAAuD,EACvDC,QAAA,GAKIP,eAAe,KAEnBH,MAAM,CAACW,KAAK,CAACF,OAAO,EAAE;EACpBC,QAAQ;EACRE,KAAK,EAAEX,SAAS,CAACY,QAAQ,CAAC,uBAAuB;CAClD,CAAC","ignoreList":[]}
|
package/dist/esm/testing.js
CHANGED
|
@@ -20,10 +20,10 @@ import { DateTime } from 'effect';
|
|
|
20
20
|
*
|
|
21
21
|
* Ordering in the log is by `position` alone, so an event's `occurredAt` is
|
|
22
22
|
* informational; pinning it to the epoch keeps serialised bytes and comparisons
|
|
23
|
-
* stable across runs. Built via `DateTime.
|
|
23
|
+
* stable across runs. Built via `DateTime.makeUnsafe(0)` (the value is a
|
|
24
24
|
* compile-time constant, not read from the host clock).
|
|
25
25
|
*/
|
|
26
|
-
export const EPOCH = /*#__PURE__*/DateTime.
|
|
26
|
+
export const EPOCH = /*#__PURE__*/DateTime.makeUnsafe(0);
|
|
27
27
|
/**
|
|
28
28
|
* Build a fresh, deterministic uuid generator: a zero-based counter producing
|
|
29
29
|
* RFC-4122 v4-shaped ids `00000000-0000-4000-8000-<counter as 12 hex digits>`,
|
package/dist/esm/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":["DateTime","EPOCH","
|
|
1
|
+
{"version":3,"file":"testing.js","names":["DateTime","EPOCH","makeUnsafe","makeSequentialUuid","counter","suffix","toString","padStart"],"sources":["../../src/testing.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;AAgBA,SAASA,QAAQ,QAAQ,QAAQ;AAEjC;;;;;;;;AAQA,OAAO,MAAMC,KAAK,gBAAGD,QAAQ,CAACE,UAAU,CAAC,CAAC,CAAC;AAE3C;;;;;;;;;;;AAWA,OAAO,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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kairos-es/core",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
4
|
-
"description": "DCB-first event-sourcing contract and in-memory store for Effect
|
|
3
|
+
"version": "1.0.0-alpha.5",
|
|
4
|
+
"description": "DCB-first event-sourcing contract and in-memory store for Effect 4",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -11,28 +11,38 @@
|
|
|
11
11
|
"sideEffects": [],
|
|
12
12
|
"homepage": "https://github.com/neverbland/kairos-es",
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"effect": "^
|
|
14
|
+
"effect": "^4.0.5"
|
|
15
15
|
},
|
|
16
16
|
"main": "./dist/cjs/index.js",
|
|
17
17
|
"module": "./dist/esm/index.js",
|
|
18
|
-
"types": "./dist/dts/index.d.ts",
|
|
18
|
+
"types": "./dist/dts-cjs/index.d.ts",
|
|
19
19
|
"exports": {
|
|
20
20
|
".": {
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/dts-esm/index.d.ts",
|
|
23
|
+
"default": "./dist/esm/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/dts-cjs/index.d.ts",
|
|
27
|
+
"default": "./dist/cjs/index.js"
|
|
28
|
+
}
|
|
24
29
|
},
|
|
25
30
|
"./testing": {
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/dts-esm/testing.d.ts",
|
|
33
|
+
"default": "./dist/esm/testing.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/dts-cjs/testing.d.ts",
|
|
37
|
+
"default": "./dist/cjs/testing.js"
|
|
38
|
+
}
|
|
29
39
|
},
|
|
30
40
|
"./package.json": "./package.json"
|
|
31
41
|
},
|
|
32
42
|
"typesVersions": {
|
|
33
43
|
"*": {
|
|
34
44
|
"testing": [
|
|
35
|
-
"./dist/dts/testing.d.ts"
|
|
45
|
+
"./dist/dts-cjs/testing.d.ts"
|
|
36
46
|
]
|
|
37
47
|
}
|
|
38
48
|
}
|
package/src/AppendCondition.ts
CHANGED
|
@@ -13,6 +13,6 @@ import { Query } from './Query'
|
|
|
13
13
|
|
|
14
14
|
export const AppendCondition = Schema.Struct({
|
|
15
15
|
failIfEventsMatch: Query,
|
|
16
|
-
after: Schema.
|
|
16
|
+
after: Schema.optionalKey(Position),
|
|
17
17
|
})
|
|
18
18
|
export type AppendCondition = typeof AppendCondition.Type
|
package/src/AppendIsOwned.ts
CHANGED
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
*
|
|
31
31
|
* ## Present means refused, so a sanctioned append REMOVES the marker
|
|
32
32
|
*
|
|
33
|
-
* The check reads the
|
|
33
|
+
* The check reads the service through `Effect.serviceOption`, so its ABSENCE is the
|
|
34
34
|
* normal case and costs the `append` signature nothing — `DcbEventStore.append`
|
|
35
35
|
* keeps `R = never` and no caller provides anything. Presence is what refuses.
|
|
36
36
|
*
|
|
37
37
|
* That shape decides how the two legitimate appends inside a bracket get through,
|
|
38
38
|
* and there is only one route: run with the marker STRIPPED from the context
|
|
39
|
-
* (`Effect.
|
|
39
|
+
* (`Effect.updateContext` over `Context.omit(AppendIsOwned)`). The bracket's own
|
|
40
40
|
* closing append is issued outside the scope it provided the marker in, and the
|
|
41
|
-
* codec's escape hatch strips it explicitly. Neither needs a second
|
|
41
|
+
* codec's escape hatch strips it explicitly. Neither needs a second service, a
|
|
42
42
|
* sanctioning flag or a mutable cell, and there is no way to be exempt by
|
|
43
43
|
* accident: an append is either lexically outside the marked region or it says in
|
|
44
44
|
* as many words that it is removing the marker.
|
|
@@ -50,13 +50,13 @@ import { Context, Effect } from 'effect'
|
|
|
50
50
|
* owns it.
|
|
51
51
|
*
|
|
52
52
|
* `by` is a human-readable owner name and exists for the defect message alone —
|
|
53
|
-
* the refusal is decided by the
|
|
53
|
+
* the refusal is decided by the service's PRESENCE, never by this value, so nothing
|
|
54
54
|
* branches on it and no caller need agree on a vocabulary for it.
|
|
55
55
|
*/
|
|
56
|
-
export class AppendIsOwned extends Context.
|
|
56
|
+
export class AppendIsOwned extends Context.Service<
|
|
57
57
|
AppendIsOwned,
|
|
58
58
|
{ readonly by: string }
|
|
59
|
-
>() {}
|
|
59
|
+
>()('@kairos-es/core/AppendIsOwned') {}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Die if an append is already owned here; do nothing otherwise.
|
|
@@ -70,7 +70,7 @@ export class AppendIsOwned extends Context.Tag('@kairos-es/core/AppendIsOwned')<
|
|
|
70
70
|
* its escape hatch for a deliberate second append — which is what makes a defect
|
|
71
71
|
* affordable here.
|
|
72
72
|
*
|
|
73
|
-
* `Effect.serviceOption` is what keeps this free: it reads the
|
|
73
|
+
* `Effect.serviceOption` is what keeps this free: it reads the service if present and
|
|
74
74
|
* adds NOTHING to the requirement channel, so every existing append is unchanged
|
|
75
75
|
* and unprovisioned.
|
|
76
76
|
*/
|
package/src/ContractStore.ts
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
* channel (`never` for the pure in-memory engine; `SqlError` for either SQL one).
|
|
9
9
|
* The decorator wraps those mechanics with the policy the contract demands — the
|
|
10
10
|
* policy the first two engines hand-duplicated, with the Postgres one even
|
|
11
|
-
* needing a bespoke `catchAll`/`instanceof
|
|
11
|
+
* needing a bespoke `catchAll`/`instanceof` (Effect 3's `catchAll`, which Effect 4
|
|
12
|
+
* names `catch`):
|
|
12
13
|
*
|
|
13
14
|
* - `assertServableQuery` at every verb's entry (read, the append condition's
|
|
14
|
-
* guard query, subscribe) — a non-servable query
|
|
15
|
+
* guard query, subscribe) — a non-servable query, or one carrying a key the
|
|
16
|
+
* grammar does not define, is a DEFECT in every engine, and an append condition
|
|
17
|
+
* carrying a key `AppendCondition` does not define is refused alongside it;
|
|
15
18
|
* - `ReadLimit` decode on `read`'s `limit` — a negative/non-integer limit is a
|
|
16
19
|
* DEFECT (so `read` keeps `E = never`), validated through the branded schema
|
|
17
20
|
* even at an untyped boundary that reaches the store as `unknown`;
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
*/
|
|
35
38
|
import type { Array as Arr, Scope } from 'effect'
|
|
36
39
|
import { Effect, Predicate, Schema, Stream } from 'effect'
|
|
37
|
-
import
|
|
40
|
+
import { AppendCondition } from './AppendCondition'
|
|
38
41
|
import { assertAppendNotOwned } from './AppendIsOwned'
|
|
39
42
|
import type { DcbEventStore, ReadOptions, ReadResponse } from './DcbEventStore'
|
|
40
43
|
import type { DcbEvent, Position, SequencedEvent } from './Event'
|
|
@@ -73,7 +76,7 @@ export interface RawDcbEventStore<E> {
|
|
|
73
76
|
*/
|
|
74
77
|
export const makeContractStore = <E>(
|
|
75
78
|
raw: RawDcbEventStore<E>,
|
|
76
|
-
): DcbEventStore['
|
|
79
|
+
): DcbEventStore['Service'] => ({
|
|
77
80
|
read: (query, options) =>
|
|
78
81
|
Effect.suspend(() => {
|
|
79
82
|
// Defects (surface when the Effect runs), keeping `read` at `E = never`.
|
|
@@ -87,8 +90,8 @@ export const makeContractStore = <E>(
|
|
|
87
90
|
|
|
88
91
|
append: (events, condition) =>
|
|
89
92
|
// The ownership refusal runs FIRST and sits OUTSIDE the suspend, because it is
|
|
90
|
-
// the only one of append's
|
|
91
|
-
// `Context.
|
|
93
|
+
// the only one of append's checks that is EFFECTFUL: it consults a
|
|
94
|
+
// `Context.Service` rather than inspecting the arguments. `Effect.flatMap` over an
|
|
92
95
|
// already-lazy effect defers it exactly as the suspend defers the throws below,
|
|
93
96
|
// so nothing runs at the call site either way; what it buys is that the
|
|
94
97
|
// "you are inside a bracket" defect arrives before any argument is judged, which
|
|
@@ -113,8 +116,22 @@ export const makeContractStore = <E>(
|
|
|
113
116
|
'plain-data caller bypassing the type — a defect, not a conflict.',
|
|
114
117
|
)
|
|
115
118
|
}
|
|
116
|
-
// A non-servable guard query is a defect in every engine
|
|
119
|
+
// A non-servable guard query is a defect in every engine, and so is a
|
|
120
|
+
// condition carrying a key `AppendCondition` does not define: an author
|
|
121
|
+
// who wrote one believes they added a condition, and every engine would
|
|
122
|
+
// ignore it. Known keys come off the schema, as `servableQueryFault`
|
|
123
|
+
// reads its own (`Query.ts` owns the argument).
|
|
117
124
|
if (condition !== undefined) {
|
|
125
|
+
const unknown = Object.keys(condition).find(
|
|
126
|
+
(key) => !Object.hasOwn(AppendCondition.fields, key),
|
|
127
|
+
)
|
|
128
|
+
if (unknown !== undefined) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`makeContractStore.append: the append condition carries the key ${JSON.stringify(unknown)}, ` +
|
|
131
|
+
'which `AppendCondition` does not define; an unknown key would be ' +
|
|
132
|
+
'silently ignored by every engine rather than honoured, so it is refused',
|
|
133
|
+
)
|
|
134
|
+
}
|
|
118
135
|
assertServableQuery(condition.failIfEventsMatch)
|
|
119
136
|
}
|
|
120
137
|
// Only `AppendConditionFailed` belongs on the contract error channel;
|
|
@@ -140,7 +157,7 @@ export const makeContractStore = <E>(
|
|
|
140
157
|
// engine's channel simply answers false and dies as it should. It also
|
|
141
158
|
// narrows the raw `AppendConditionFailed | E` to the contract's single
|
|
142
159
|
// error, which is what keeps `append` at `E = AppendConditionFailed`.
|
|
143
|
-
return Effect.
|
|
160
|
+
return Effect.catch(raw.append(events, condition), (error) =>
|
|
144
161
|
Predicate.isTagged('AppendConditionFailed')(error)
|
|
145
162
|
? Effect.fail(error)
|
|
146
163
|
: Effect.die(error),
|
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') {}
|