@swirls/looms 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -2
- package/dist/actor/cell.d.ts +9 -4
- package/dist/actor/cell.js +7 -11
- package/dist/actor/host.d.ts +7 -3
- package/dist/actor/host.js +55 -32
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +12 -1
- package/dist/actor/routing.js +57 -51
- package/dist/agent/agent-session.d.ts +5 -0
- package/dist/agent/agent-session.js +270 -0
- package/dist/agent/definitions-store.d.ts +2 -3
- package/dist/agent/definitions-store.js +2 -4
- package/dist/agent/definitions.d.ts +46 -20
- package/dist/agent/definitions.js +36 -8
- package/dist/agent/effects.d.ts +359 -2
- package/dist/agent/effects.js +60 -34
- package/dist/agent/events.d.ts +175 -19
- package/dist/agent/events.js +45 -10
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/llm.d.ts +11 -2
- package/dist/agent/llm.js +13 -3
- package/dist/agent/module.d.ts +812 -78
- package/dist/agent/module.js +16 -4
- package/dist/agent/scope.d.ts +174 -18
- package/dist/agent/signals.d.ts +24 -1
- package/dist/agent/signals.js +77 -0
- package/dist/agent/threads.d.ts +3 -33
- package/dist/agent/threads.js +20 -25
- package/dist/agent/tool-schema.js +0 -1
- package/dist/agent/types.d.ts +49 -4
- package/dist/agent/types.js +5 -5
- package/dist/ai-vercel/index.d.ts +1 -0
- package/dist/ai-vercel/index.js +1 -0
- package/dist/ai-vercel/messages.js +22 -7
- package/dist/ai-vercel/vercel-evaluator.d.ts +23 -0
- package/dist/ai-vercel/vercel-evaluator.js +68 -0
- package/dist/ai-vercel/vercel-llm.js +64 -44
- package/dist/approval/effects.d.ts +89 -0
- package/dist/approval/module.d.ts +179 -29
- package/dist/approval/projections.d.ts +0 -14
- package/dist/approval/projections.js +20 -3
- package/dist/approval/signals.d.ts +2 -0
- package/dist/approval/signals.js +18 -2
- package/dist/cli/cli.js +2 -0
- package/dist/cli/commands.js +12 -16
- package/dist/client/client.d.ts +1195 -18
- package/dist/client/client.js +118 -120
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/sse.d.ts +13 -0
- package/dist/client/sse.js +38 -44
- package/dist/cloudflare/alarm-scheduler.d.ts +12 -2
- package/dist/cloudflare/alarm-scheduler.js +34 -7
- package/dist/cloudflare/durable-object.d.ts +10 -2
- package/dist/cloudflare/durable-object.js +59 -28
- package/dist/cloudflare/event-store.d.ts +7 -2
- package/dist/cloudflare/event-store.js +8 -7
- package/dist/cloudflare/index.d.ts +2 -2
- package/dist/cloudflare/index.js +2 -2
- package/dist/cloudflare/router.js +13 -14
- package/dist/core/catalog.d.ts +3 -3
- package/dist/core/catalog.js +2 -5
- package/dist/core/coalesce.js +5 -3
- package/dist/core/compose.d.ts +2 -1
- package/dist/core/compose.js +18 -2
- package/dist/core/definition-store.d.ts +11 -0
- package/dist/core/definition-store.js +6 -0
- package/dist/core/effects.d.ts +127 -15
- package/dist/core/effects.js +89 -17
- package/dist/core/envelope.d.ts +27 -7
- package/dist/core/envelope.js +20 -6
- package/dist/core/event-codec.d.ts +4 -2
- package/dist/core/event-codec.js +10 -5
- package/dist/core/event-index.js +1 -3
- package/dist/core/fold.d.ts +3 -0
- package/dist/core/fold.js +171 -24
- package/dist/core/ids.d.ts +5 -0
- package/dist/core/ids.js +35 -2
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +5 -1
- package/dist/core/kind.d.ts +22 -0
- package/dist/core/kind.js +17 -0
- package/dist/core/limits.d.ts +34 -0
- package/dist/core/limits.js +46 -0
- package/dist/core/match.d.ts +1 -0
- package/dist/core/match.js +10 -1
- package/dist/core/module-scope.d.ts +22 -20
- package/dist/core/module-scope.js +38 -28
- package/dist/core/module.d.ts +16 -5
- package/dist/core/module.js +4 -0
- package/dist/core/projection.d.ts +13 -13
- package/dist/core/projection.js +9 -5
- package/dist/core/protocol.d.ts +68 -11
- package/dist/core/protocol.js +82 -11
- package/dist/core/replay.d.ts +312 -3
- package/dist/core/replay.js +11 -1
- package/dist/core/schema.d.ts +8 -19
- package/dist/core/schema.js +25 -80
- package/dist/core/serialize.js +2 -4
- package/dist/core/snapshot-store.d.ts +2 -1
- package/dist/core/snapshot-store.js +8 -2
- package/dist/core/snapshots.d.ts +5 -1
- package/dist/core/snapshots.js +10 -15
- package/dist/core/sqlite-store.js +56 -11
- package/dist/core/state.d.ts +142 -4
- package/dist/core/state.js +78 -0
- package/dist/core/store.d.ts +38 -3
- package/dist/core/store.js +77 -22
- package/dist/core/thread.d.ts +10 -9
- package/dist/core/types.d.ts +5 -8
- package/dist/core/types.js +28 -24
- package/dist/core/validate-event.js +14 -4
- package/dist/core/worker-callback.d.ts +17 -0
- package/dist/core/worker-callback.js +1 -0
- package/dist/debugger/components/event-stream.js +9 -10
- package/dist/debugger/events/protocol-summarize.js +26 -0
- package/dist/debugger/lib/event-meta.js +1 -2
- package/dist/evaluator/definitions-store.d.ts +11 -0
- package/dist/evaluator/definitions-store.js +8 -0
- package/dist/evaluator/definitions.d.ts +18 -0
- package/dist/evaluator/definitions.js +4 -0
- package/dist/evaluator/effects.d.ts +226 -0
- package/dist/evaluator/effects.js +132 -0
- package/dist/evaluator/evaluator.d.ts +24 -0
- package/dist/evaluator/evaluator.js +67 -0
- package/dist/evaluator/events.d.ts +59 -0
- package/dist/evaluator/events.js +21 -0
- package/dist/evaluator/index.d.ts +11 -0
- package/dist/evaluator/index.js +11 -0
- package/dist/evaluator/module.d.ts +606 -0
- package/dist/evaluator/module.js +25 -0
- package/dist/evaluator/projections.d.ts +50 -0
- package/dist/evaluator/projections.js +20 -0
- package/dist/evaluator/scope.d.ts +59 -0
- package/dist/evaluator/scope.js +7 -0
- package/dist/evaluator/signals.d.ts +3 -0
- package/dist/evaluator/signals.js +17 -0
- package/dist/evaluator/threads.d.ts +62 -0
- package/dist/evaluator/threads.js +117 -0
- package/dist/evaluator/types.d.ts +125 -0
- package/dist/evaluator/types.js +51 -0
- package/dist/projectors/delivery.d.ts +83 -0
- package/dist/projectors/delivery.js +182 -0
- package/dist/projectors/index-model.d.ts +1 -1
- package/dist/projectors/index-model.js +4 -6
- package/dist/projectors/index.d.ts +2 -0
- package/dist/projectors/index.js +2 -0
- package/dist/projectors/memory.d.ts +1 -1
- package/dist/projectors/memory.js +10 -8
- package/dist/projectors/postgres.js +44 -58
- package/dist/projectors/projector.d.ts +17 -0
- package/dist/projectors/projector.js +26 -1
- package/dist/projectors/sqlite-delivery.d.ts +4 -0
- package/dist/projectors/sqlite-delivery.js +147 -0
- package/dist/projectors/sqlite.js +23 -23
- package/dist/projectors/with-projectors.d.ts +7 -2
- package/dist/projectors/with-projectors.js +27 -28
- package/dist/react/notify.js +11 -8
- package/dist/react/react.d.ts +2 -3
- package/dist/react/react.js +14 -30
- package/dist/react/sse.d.ts +1 -1
- package/dist/react/sse.js +1 -1
- package/dist/react/store.d.ts +2 -2
- package/dist/react/store.js +109 -119
- package/dist/runtime/cancellation.d.ts +16 -0
- package/dist/runtime/cancellation.js +129 -0
- package/dist/runtime/deadline-transition-planner.d.ts +31 -0
- package/dist/runtime/deadline-transition-planner.js +139 -0
- package/dist/runtime/dispatch-effect.d.ts +3 -2
- package/dist/runtime/dispatch-effect.js +7 -13
- package/dist/runtime/errors.d.ts +51 -0
- package/dist/runtime/errors.js +39 -0
- package/dist/runtime/{runtime-helpers.d.ts → helpers.d.ts} +24 -4
- package/dist/runtime/{runtime-helpers.js → helpers.js} +56 -19
- package/dist/runtime/index.d.ts +3 -3
- package/dist/runtime/index.js +3 -3
- package/dist/runtime/ingress.d.ts +15 -0
- package/dist/runtime/ingress.js +42 -0
- package/dist/runtime/local-effect-executor.d.ts +21 -0
- package/dist/runtime/local-effect-executor.js +137 -0
- package/dist/runtime/looms.d.ts +22 -4
- package/dist/runtime/looms.js +54 -36
- package/dist/runtime/observer.d.ts +45 -0
- package/dist/runtime/observer.js +11 -0
- package/dist/runtime/repository.d.ts +55 -0
- package/dist/runtime/repository.js +278 -0
- package/dist/runtime/retry-policy.d.ts +16 -0
- package/dist/runtime/retry-policy.js +29 -0
- package/dist/runtime/run-execution-context.d.ts +30 -0
- package/dist/runtime/run-execution-context.js +56 -0
- package/dist/runtime/run-start.d.ts +4 -0
- package/dist/runtime/run-start.js +44 -0
- package/dist/runtime/runtime.d.ts +6 -87
- package/dist/runtime/runtime.js +233 -470
- package/dist/runtime/server/auth.d.ts +40 -0
- package/dist/runtime/server/auth.js +40 -0
- package/dist/runtime/server/fetch-handler.d.ts +14 -0
- package/dist/runtime/server/fetch-handler.js +51 -0
- package/dist/runtime/server/http.d.ts +37 -0
- package/dist/runtime/server/http.js +62 -0
- package/dist/runtime/server/route-table.d.ts +55 -0
- package/dist/runtime/server/route-table.js +74 -0
- package/dist/runtime/server/routes/effects.d.ts +3 -0
- package/dist/runtime/server/routes/effects.js +59 -0
- package/dist/runtime/server/routes/operations.d.ts +3 -0
- package/dist/runtime/server/routes/operations.js +14 -0
- package/dist/runtime/server/routes/runs.d.ts +3 -0
- package/dist/runtime/server/routes/runs.js +192 -0
- package/dist/runtime/server.d.ts +2 -8
- package/dist/runtime/server.js +4 -211
- package/dist/runtime/sse.d.ts +2 -1
- package/dist/runtime/sse.js +24 -17
- package/dist/runtime/types.d.ts +115 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/wake-engine.d.ts +20 -0
- package/dist/runtime/wake-engine.js +311 -0
- package/dist/runtime/wake-scheduler.d.ts +3 -4
- package/dist/runtime/wake-scheduler.js +25 -31
- package/dist/runtime/worker-callbacks.d.ts +16 -0
- package/dist/runtime/worker-callbacks.js +167 -0
- package/dist/runtime/worker-effect-dispatch.d.ts +36 -0
- package/dist/runtime/worker-effect-dispatch.js +126 -0
- package/dist/s2/config.d.ts +1 -1
- package/dist/s2/config.js +1 -1
- package/dist/s2/lite.js +70 -82
- package/dist/s2/pagination.d.ts +15 -0
- package/dist/s2/pagination.js +36 -0
- package/dist/s2/projector.js +6 -5
- package/dist/s2/snapshot-store.js +148 -163
- package/dist/s2/store.d.ts +3 -21
- package/dist/s2/store.js +71 -165
- package/dist/testing/index.d.ts +15 -0
- package/dist/testing/index.js +53 -16
- package/dist/workflow/definitions-store.d.ts +2 -3
- package/dist/workflow/definitions-store.js +2 -2
- package/dist/workflow/definitions.d.ts +78 -22
- package/dist/workflow/definitions.js +80 -14
- package/dist/workflow/effects.d.ts +453 -107
- package/dist/workflow/effects.js +214 -115
- package/dist/workflow/events.d.ts +149 -5
- package/dist/workflow/events.js +27 -5
- package/dist/workflow/graph.d.ts +56 -0
- package/dist/workflow/graph.js +207 -0
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +1 -0
- package/dist/workflow/module.d.ts +1047 -266
- package/dist/workflow/projections.d.ts +14 -12
- package/dist/workflow/projections.js +30 -8
- package/dist/workflow/scope.d.ts +146 -4
- package/dist/workflow/threads.d.ts +383 -155
- package/dist/workflow/threads.js +308 -110
- package/package.json +7 -2
- package/dist/runtime/events-api.d.ts +0 -4
- package/dist/runtime/events-api.js +0 -84
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { AppendInput, AppendRecord, S2, S2Endpoints, S2Error } from '@s2-dev/streamstore';
|
|
2
|
-
import { Effect, Layer, Option, Schema } from 'effect';
|
|
3
|
-
import { createKeyedSerializer, SnapshotStoreError, SnapshotStoreTag, } from '@swirls/looms/core';
|
|
2
|
+
import { Effect, Layer, Option, Schema, Stream } from 'effect';
|
|
3
|
+
import { createKeyedSerializer, assertSnapshotByteLimit, RunStateSchema, SnapshotStoreError, SnapshotPayloadTooLargeError, SnapshotStoreTag, } from '@swirls/looms/core';
|
|
4
4
|
import { S2ConfigSchema } from './config.js';
|
|
5
|
+
import { paginateS2Stream } from './pagination.js';
|
|
5
6
|
const DEFAULT_PREFIX = 'snapshots';
|
|
6
7
|
const DEFAULT_MAX_CHUNK_BYTES = 900 * 1024;
|
|
7
8
|
const SnapshotFrameSchema = Schema.Struct({
|
|
8
9
|
v: Schema.Literal(1),
|
|
9
10
|
runId: Schema.String,
|
|
10
|
-
cursor: Schema.
|
|
11
|
+
cursor: Schema.Finite,
|
|
11
12
|
stateHash: Schema.String,
|
|
12
|
-
takenAt: Schema.
|
|
13
|
-
chunk: Schema.
|
|
14
|
-
chunks: Schema.
|
|
13
|
+
takenAt: Schema.Finite,
|
|
14
|
+
chunk: Schema.Finite,
|
|
15
|
+
chunks: Schema.Finite,
|
|
15
16
|
data: Schema.String,
|
|
16
17
|
});
|
|
18
|
+
const SnapshotFrameJson = Schema.fromJsonString(SnapshotFrameSchema);
|
|
19
|
+
const decodeFrame = Schema.decodeSync(SnapshotFrameJson);
|
|
20
|
+
const encodeFrame = Schema.encodeSync(SnapshotFrameJson);
|
|
17
21
|
export function frameSnapshot(snapshot, maxChunkBytes = DEFAULT_MAX_CHUNK_BYTES) {
|
|
18
22
|
const json = JSON.stringify(snapshot.state);
|
|
19
23
|
const chunks = Math.max(1, Math.ceil(json.length / maxChunkBytes));
|
|
@@ -37,6 +41,8 @@ export function assembleSnapshot(frames) {
|
|
|
37
41
|
throw new SnapshotStoreError('Cannot assemble snapshot from empty frames');
|
|
38
42
|
}
|
|
39
43
|
const first = frames[0];
|
|
44
|
+
// Oxlint's immutable alternative requires ES2023; sorting a fresh copy is safe on ES2022.
|
|
45
|
+
// oxlint-disable-next-line unicorn/no-array-sort
|
|
40
46
|
const ordered = [...frames].sort((a, b) => a.chunk - b.chunk);
|
|
41
47
|
for (const frame of ordered) {
|
|
42
48
|
if (frame.cursor !== first.cursor ||
|
|
@@ -54,8 +60,7 @@ export function assembleSnapshot(frames) {
|
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
const json = ordered.map((frame) => frame.data).join('');
|
|
57
|
-
|
|
58
|
-
const state = JSON.parse(json);
|
|
63
|
+
const state = Schema.decodeSync(Schema.fromJsonString(RunStateSchema))(json);
|
|
59
64
|
return {
|
|
60
65
|
runId: first.runId,
|
|
61
66
|
cursor: first.cursor,
|
|
@@ -84,74 +89,56 @@ function createClient(config) {
|
|
|
84
89
|
}
|
|
85
90
|
function parseFrame(body) {
|
|
86
91
|
try {
|
|
87
|
-
return
|
|
92
|
+
return decodeFrame(body);
|
|
88
93
|
}
|
|
89
94
|
catch {
|
|
90
95
|
return undefined;
|
|
91
96
|
}
|
|
92
97
|
}
|
|
98
|
+
function toSnapshotError(cause, message) {
|
|
99
|
+
return cause instanceof SnapshotStoreError
|
|
100
|
+
? cause
|
|
101
|
+
: new SnapshotStoreError(`${message}: ${cause instanceof Error ? cause.message : String(cause)}`, cause);
|
|
102
|
+
}
|
|
93
103
|
/**
|
|
94
104
|
* Trailing command records (the trim written by `prune`) are skipped by
|
|
95
105
|
* `ignoreCommandRecords`, so a `tailOffset: 1` read can come back empty.
|
|
96
106
|
* Probe a few records from the tail instead.
|
|
97
107
|
*/
|
|
98
108
|
const TAIL_PROBE = 4;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
function readFromTail(stream, tailOffset) {
|
|
110
|
+
return Effect.tryPromise({
|
|
111
|
+
try: () => stream.read({
|
|
102
112
|
start: { from: { tailOffset } },
|
|
103
113
|
ignoreCommandRecords: true,
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (cause instanceof S2Error && (cause.status === 404 || cause.status === 416)) {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
throw cause;
|
|
112
|
-
}
|
|
114
|
+
}),
|
|
115
|
+
catch: (cause) => toSnapshotError(cause, 'Failed to read snapshot stream tail'),
|
|
116
|
+
}).pipe(Effect.map((page) => [...page.records]), Effect.catchIf((error) => error.cause instanceof S2Error &&
|
|
117
|
+
(error.cause.status === 404 || error.cause.status === 416), () => Effect.succeed([])));
|
|
113
118
|
}
|
|
114
119
|
/** Latest complete data frame near the tail, with its record, or undefined. */
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
function readLatestFrame(stream) {
|
|
121
|
+
return readFromTail(stream, TAIL_PROBE).pipe(Effect.map((records) => {
|
|
122
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
123
|
+
const record = records[i];
|
|
124
|
+
const frame = parseFrame(record.body);
|
|
125
|
+
if (frame) {
|
|
126
|
+
return { record, frame };
|
|
127
|
+
}
|
|
122
128
|
}
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
return undefined;
|
|
130
|
+
}));
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
while (true) {
|
|
130
|
-
const page = await stream
|
|
131
|
-
.read({
|
|
132
|
+
function readAllSnapshotRecords(stream) {
|
|
133
|
+
return paginateS2Stream((cursor, count) => Effect.tryPromise({
|
|
134
|
+
try: () => stream.read({
|
|
132
135
|
start: { from: { seqNum: cursor }, clamp: true },
|
|
133
|
-
stop: { limits: { count
|
|
136
|
+
stop: { limits: { count } },
|
|
134
137
|
ignoreCommandRecords: true,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
throw cause;
|
|
142
|
-
});
|
|
143
|
-
if (!page || page.records.length === 0) {
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
records.push(...page.records);
|
|
147
|
-
const last = page.records[page.records.length - 1];
|
|
148
|
-
const next = last.seqNum + 1;
|
|
149
|
-
if (page.tail?.seqNum !== undefined && next >= page.tail.seqNum) {
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
cursor = next;
|
|
153
|
-
}
|
|
154
|
-
return records;
|
|
138
|
+
}),
|
|
139
|
+
catch: (cause) => toSnapshotError(cause, 'Failed to scan snapshot stream'),
|
|
140
|
+
}).pipe(Effect.catchIf((error) => error.cause instanceof S2Error &&
|
|
141
|
+
(error.cause.status === 404 || error.cause.status === 416), () => Effect.succeed(null)))).pipe(Stream.runCollect);
|
|
155
142
|
}
|
|
156
143
|
function groupCompleteSnapshots(records) {
|
|
157
144
|
const byCursor = new Map();
|
|
@@ -173,10 +160,12 @@ function groupCompleteSnapshots(records) {
|
|
|
173
160
|
continue;
|
|
174
161
|
}
|
|
175
162
|
}
|
|
163
|
+
// `assembled` is function-local, so in-place sorting cannot mutate caller-owned data.
|
|
164
|
+
// oxlint-disable-next-line unicorn/no-array-sort
|
|
176
165
|
return assembled.sort((a, b) => a.cursor - b.cursor);
|
|
177
166
|
}
|
|
178
167
|
export function s2SnapshotStore(config) {
|
|
179
|
-
const parsed = Schema.
|
|
168
|
+
const parsed = Schema.decodeSync(S2ConfigSchema)(config);
|
|
180
169
|
const client = createClient(parsed);
|
|
181
170
|
const basin = client.basin(parsed.basin);
|
|
182
171
|
const prefix = parsed.snapshotPrefix ?? DEFAULT_PREFIX;
|
|
@@ -184,133 +173,129 @@ export function s2SnapshotStore(config) {
|
|
|
184
173
|
const ensured = new Set();
|
|
185
174
|
let basinEnsured = false;
|
|
186
175
|
const streamName = (runId) => `${prefix}/${runId}`;
|
|
187
|
-
const ensureBasin =
|
|
176
|
+
const ensureBasin = Effect.suspend(() => {
|
|
188
177
|
if (basinEnsured) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
await client.basins.create({ basin: parsed.basin });
|
|
178
|
+
return Effect.void;
|
|
193
179
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
const ensureStream =
|
|
202
|
-
|
|
180
|
+
return Effect.tryPromise({
|
|
181
|
+
try: () => client.basins.create({ basin: parsed.basin }),
|
|
182
|
+
catch: (cause) => toSnapshotError(cause, `Failed to ensure basin ${parsed.basin}`),
|
|
183
|
+
}).pipe(Effect.catchIf((error) => error.cause instanceof S2Error && error.cause.status === 409, () => Effect.void), Effect.tap(() => Effect.sync(() => {
|
|
184
|
+
basinEnsured = true;
|
|
185
|
+
})), Effect.asVoid);
|
|
186
|
+
});
|
|
187
|
+
const ensureStream = (runId) => Effect.gen(function* () {
|
|
188
|
+
yield* ensureBasin;
|
|
203
189
|
const name = streamName(runId);
|
|
204
190
|
if (!ensured.has(name)) {
|
|
205
|
-
|
|
206
|
-
|
|
191
|
+
yield* Effect.tryPromise({
|
|
192
|
+
try: () => basin.streams.create({
|
|
207
193
|
stream: name,
|
|
208
194
|
config: { retentionPolicy: { infinite: {} } },
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
}
|
|
195
|
+
}),
|
|
196
|
+
catch: (cause) => toSnapshotError(cause, `Failed to ensure stream ${name}`),
|
|
197
|
+
}).pipe(Effect.catchIf((error) => error.cause instanceof S2Error &&
|
|
198
|
+
(error.cause.status === 409 ||
|
|
199
|
+
error.cause.status === 404 ||
|
|
200
|
+
error.cause.status === 405), () => Effect.void));
|
|
217
201
|
ensured.add(name);
|
|
218
202
|
}
|
|
219
203
|
return basin.stream(name);
|
|
220
|
-
};
|
|
221
|
-
const toError = (cause, message) => cause instanceof SnapshotStoreError
|
|
222
|
-
? cause
|
|
223
|
-
: new SnapshotStoreError(`${message}: ${cause instanceof Error ? cause.message : String(cause)}`, cause);
|
|
204
|
+
});
|
|
224
205
|
const service = {
|
|
225
|
-
save: (snapshot) => Effect.
|
|
226
|
-
try
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
206
|
+
save: (snapshot) => Effect.gen(function* () {
|
|
207
|
+
yield* Effect.try({
|
|
208
|
+
try: () => assertSnapshotByteLimit(snapshot),
|
|
209
|
+
catch: (cause) => cause instanceof SnapshotPayloadTooLargeError
|
|
210
|
+
? cause
|
|
211
|
+
: new SnapshotStoreError('snapshot size validation failed', cause),
|
|
212
|
+
});
|
|
213
|
+
const stream = yield* ensureStream(snapshot.runId);
|
|
214
|
+
const frames = frameSnapshot(snapshot);
|
|
215
|
+
const records = frames.map((frame) => AppendRecord.string({
|
|
216
|
+
body: encodeFrame(frame),
|
|
217
|
+
headers: [
|
|
218
|
+
['looms-cursor', String(frame.cursor)],
|
|
219
|
+
['looms-chunk', `${frame.chunk}/${frame.chunks}`],
|
|
220
|
+
],
|
|
221
|
+
}));
|
|
222
|
+
yield* Effect.tryPromise({
|
|
223
|
+
try: () => appends.run(snapshot.runId, () => {
|
|
237
224
|
if (records.length === 1) {
|
|
238
|
-
|
|
239
|
-
return;
|
|
225
|
+
return stream.append(AppendInput.create(records)).then(() => undefined);
|
|
240
226
|
}
|
|
227
|
+
let pending = Promise.resolve();
|
|
241
228
|
for (const record of records) {
|
|
242
|
-
|
|
229
|
+
pending = pending.then(() => stream.append(AppendInput.create([record])).then(() => undefined));
|
|
243
230
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
231
|
+
return pending;
|
|
232
|
+
}),
|
|
233
|
+
catch: (cause) => toSnapshotError(cause, `Failed to save snapshot for ${snapshot.runId}`),
|
|
234
|
+
});
|
|
247
235
|
}),
|
|
248
|
-
loadLatest: (runId) => Effect.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return Option.some(assembleSnapshot(frames));
|
|
264
|
-
}
|
|
236
|
+
loadLatest: (runId) => Effect.gen(function* () {
|
|
237
|
+
const stream = yield* ensureStream(runId);
|
|
238
|
+
const tailFrame = yield* readLatestFrame(stream);
|
|
239
|
+
if (!tailFrame) {
|
|
240
|
+
return Option.none();
|
|
241
|
+
}
|
|
242
|
+
if (tailFrame.frame.chunk === tailFrame.frame.chunks - 1) {
|
|
243
|
+
const lastFrame = tailFrame.frame;
|
|
244
|
+
const records = (yield* readFromTail(stream, lastFrame.chunks + TAIL_PROBE)) ?? [];
|
|
245
|
+
const frames = records
|
|
246
|
+
.map((record) => parseFrame(record.body))
|
|
247
|
+
.filter((frame) => frame !== undefined)
|
|
248
|
+
.filter((frame) => frame.cursor === lastFrame.cursor);
|
|
249
|
+
if (frames.length === lastFrame.chunks) {
|
|
250
|
+
return Option.some(assembleSnapshot(frames));
|
|
265
251
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
catch: (cause) => toError(cause, `Failed to load snapshot for ${runId}`),
|
|
252
|
+
}
|
|
253
|
+
// Incomplete or interleaved tail: fall back to scanning the retained stream.
|
|
254
|
+
const records = yield* readAllSnapshotRecords(stream);
|
|
255
|
+
const complete = groupCompleteSnapshots(records);
|
|
256
|
+
const newest = complete[complete.length - 1];
|
|
257
|
+
return newest ? Option.some(newest) : Option.none();
|
|
273
258
|
}),
|
|
274
|
-
listCursors: (runId) => Effect.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return groupCompleteSnapshots(records).map((snapshot) => snapshot.cursor);
|
|
279
|
-
},
|
|
280
|
-
catch: (cause) => toError(cause, `Failed to list snapshot cursors for ${runId}`),
|
|
259
|
+
listCursors: (runId) => Effect.gen(function* () {
|
|
260
|
+
const stream = yield* ensureStream(runId);
|
|
261
|
+
const records = yield* readAllSnapshotRecords(stream);
|
|
262
|
+
return groupCompleteSnapshots(records).map((snapshot) => snapshot.cursor);
|
|
281
263
|
}),
|
|
282
|
-
prune: (runId, keepLatest) => Effect.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (!latest || latest.frame.chunk !== latest.frame.chunks - 1) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
const start = latest.record.seqNum - latest.frame.chunk;
|
|
292
|
-
if (start <= 0) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
await appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(start)])));
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
const records = await readAllSnapshotRecords(stream);
|
|
299
|
-
const complete = groupCompleteSnapshots(records);
|
|
300
|
-
if (complete.length <= keepLatest) {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const keep = complete[complete.length - keepLatest];
|
|
304
|
-
if (!keep) {
|
|
264
|
+
prune: (runId, keepLatest) => Effect.gen(function* () {
|
|
265
|
+
const stream = yield* ensureStream(runId);
|
|
266
|
+
if (keepLatest <= 1) {
|
|
267
|
+
// Fast path: the latest complete frame tells us where the latest snapshot starts.
|
|
268
|
+
const latest = yield* readLatestFrame(stream);
|
|
269
|
+
if (!latest || latest.frame.chunk !== latest.frame.chunks - 1) {
|
|
305
270
|
return;
|
|
306
271
|
}
|
|
307
|
-
const
|
|
308
|
-
if (
|
|
272
|
+
const start = latest.record.seqNum - latest.frame.chunk;
|
|
273
|
+
if (start <= 0) {
|
|
309
274
|
return;
|
|
310
275
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
276
|
+
yield* Effect.tryPromise({
|
|
277
|
+
try: () => appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(start)]))),
|
|
278
|
+
catch: (cause) => toSnapshotError(cause, `Failed to prune snapshots for ${runId}`),
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const records = yield* readAllSnapshotRecords(stream);
|
|
283
|
+
const complete = groupCompleteSnapshots(records);
|
|
284
|
+
if (complete.length <= keepLatest) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const keep = complete[complete.length - keepLatest];
|
|
288
|
+
if (!keep) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const firstKept = records.find((record) => parseFrame(record.body)?.cursor === keep.cursor);
|
|
292
|
+
if (!firstKept) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
yield* Effect.tryPromise({
|
|
296
|
+
try: () => appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(firstKept.seqNum)]))),
|
|
297
|
+
catch: (cause) => toSnapshotError(cause, `Failed to prune snapshots for ${runId}`),
|
|
298
|
+
});
|
|
314
299
|
}),
|
|
315
300
|
};
|
|
316
301
|
return service;
|
package/dist/s2/store.d.ts
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
import { Effect, Layer
|
|
1
|
+
import { Effect, Layer } from 'effect';
|
|
2
2
|
import { EventStoreError, EventStoreTag, type EventStore } from '@swirls/looms/core';
|
|
3
3
|
import { type S2Config } from './config';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
readonly tail?: {
|
|
7
|
-
readonly seqNum: number;
|
|
8
|
-
} | null;
|
|
9
|
-
}
|
|
10
|
-
export interface ReadAllPagesOptions {
|
|
11
|
-
readonly fromSeq?: number;
|
|
12
|
-
readonly limit?: number;
|
|
13
|
-
readonly maxPageSize?: number;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Paginate an S2 stream lazily using Effect Streams.
|
|
17
|
-
*
|
|
18
|
-
* Pulls pages on demand until the stream tail is reached, an empty page is returned,
|
|
19
|
-
* or the caller's optional limit is satisfied.
|
|
20
|
-
*/
|
|
21
|
-
export declare function paginateS2Stream<T extends {
|
|
22
|
-
readonly seqNum: number;
|
|
23
|
-
}>(fetchPage: (cursor: number, count: number) => Effect.Effect<S2ReadBatchLike<T> | null, EventStoreError>, options?: ReadAllPagesOptions): Stream.Stream<T, EventStoreError>;
|
|
4
|
+
import { paginateS2Stream, type ReadAllPagesOptions, type S2ReadBatchLike } from './pagination';
|
|
5
|
+
export { paginateS2Stream, type ReadAllPagesOptions, type S2ReadBatchLike };
|
|
24
6
|
export declare function readAllPages<T extends {
|
|
25
7
|
readonly seqNum: number;
|
|
26
8
|
}>(fetchPage: (cursor: number, count: number) => Promise<S2ReadBatchLike<T> | null>, options?: ReadAllPagesOptions): Promise<T[]>;
|