@swirls/looms 0.0.1
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/LICENSE +190 -0
- package/README.md +9 -0
- package/dist/actor/cell.d.ts +27 -0
- package/dist/actor/cell.js +41 -0
- package/dist/actor/host.d.ts +27 -0
- package/dist/actor/host.js +82 -0
- package/dist/actor/index.d.ts +3 -0
- package/dist/actor/index.js +3 -0
- package/dist/actor/routing.d.ts +15 -0
- package/dist/actor/routing.js +80 -0
- package/dist/agent/definitions-store.d.ts +11 -0
- package/dist/agent/definitions-store.js +10 -0
- package/dist/agent/definitions.d.ts +152 -0
- package/dist/agent/definitions.js +74 -0
- package/dist/agent/effects.d.ts +425 -0
- package/dist/agent/effects.js +318 -0
- package/dist/agent/events.d.ts +170 -0
- package/dist/agent/events.js +61 -0
- package/dist/agent/index.d.ts +13 -0
- package/dist/agent/index.js +13 -0
- package/dist/agent/llm.d.ts +35 -0
- package/dist/agent/llm.js +48 -0
- package/dist/agent/module.d.ts +1037 -0
- package/dist/agent/module.js +27 -0
- package/dist/agent/projections.d.ts +17 -0
- package/dist/agent/projections.js +81 -0
- package/dist/agent/scope.d.ts +171 -0
- package/dist/agent/scope.js +7 -0
- package/dist/agent/signals.d.ts +17 -0
- package/dist/agent/signals.js +19 -0
- package/dist/agent/stop-when.d.ts +12 -0
- package/dist/agent/stop-when.js +6 -0
- package/dist/agent/threads.d.ts +34 -0
- package/dist/agent/threads.js +407 -0
- package/dist/agent/tool-schema.d.ts +21 -0
- package/dist/agent/tool-schema.js +67 -0
- package/dist/agent/types.d.ts +87 -0
- package/dist/agent/types.js +32 -0
- package/dist/ai-vercel/index.d.ts +2 -0
- package/dist/ai-vercel/index.js +2 -0
- package/dist/ai-vercel/messages.d.ts +9 -0
- package/dist/ai-vercel/messages.js +68 -0
- package/dist/ai-vercel/vercel-llm.d.ts +8 -0
- package/dist/ai-vercel/vercel-llm.js +98 -0
- package/dist/approval/effects.d.ts +133 -0
- package/dist/approval/effects.js +13 -0
- package/dist/approval/events.d.ts +52 -0
- package/dist/approval/events.js +28 -0
- package/dist/approval/index.d.ts +6 -0
- package/dist/approval/index.js +6 -0
- package/dist/approval/module.d.ts +342 -0
- package/dist/approval/module.js +10 -0
- package/dist/approval/projections.d.ts +42 -0
- package/dist/approval/projections.js +57 -0
- package/dist/approval/scope.d.ts +45 -0
- package/dist/approval/scope.js +7 -0
- package/dist/approval/signals.d.ts +19 -0
- package/dist/approval/signals.js +34 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +6 -0
- package/dist/cli/commands.d.ts +13 -0
- package/dist/cli/commands.js +133 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/client/client.d.ts +69 -0
- package/dist/client/client.js +179 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client/sse.d.ts +10 -0
- package/dist/client/sse.js +103 -0
- package/dist/cloudflare/alarm-scheduler.d.ts +9 -0
- package/dist/cloudflare/alarm-scheduler.js +13 -0
- package/dist/cloudflare/durable-object.d.ts +24 -0
- package/dist/cloudflare/durable-object.js +43 -0
- package/dist/cloudflare/event-store.d.ts +10 -0
- package/dist/cloudflare/event-store.js +18 -0
- package/dist/cloudflare/index.d.ts +4 -0
- package/dist/cloudflare/index.js +4 -0
- package/dist/cloudflare/router.d.ts +12 -0
- package/dist/cloudflare/router.js +26 -0
- package/dist/core/bun-sqlite.d.ts +13 -0
- package/dist/core/bun-sqlite.js +19 -0
- package/dist/core/catalog.d.ts +49 -0
- package/dist/core/catalog.js +39 -0
- package/dist/core/coalesce.d.ts +21 -0
- package/dist/core/coalesce.js +178 -0
- package/dist/core/compose.d.ts +59 -0
- package/dist/core/compose.js +97 -0
- package/dist/core/cursor.d.ts +16 -0
- package/dist/core/cursor.js +46 -0
- package/dist/core/effects.d.ts +110 -0
- package/dist/core/effects.js +99 -0
- package/dist/core/envelope.d.ts +79 -0
- package/dist/core/envelope.js +68 -0
- package/dist/core/event-codec.d.ts +8 -0
- package/dist/core/event-codec.js +82 -0
- package/dist/core/event-index.d.ts +28 -0
- package/dist/core/event-index.js +86 -0
- package/dist/core/fold.d.ts +19 -0
- package/dist/core/fold.js +362 -0
- package/dist/core/ids.d.ts +11 -0
- package/dist/core/ids.js +47 -0
- package/dist/core/index.d.ts +26 -0
- package/dist/core/index.js +25 -0
- package/dist/core/match.d.ts +8 -0
- package/dist/core/match.js +58 -0
- package/dist/core/module-scope.d.ts +99 -0
- package/dist/core/module-scope.js +72 -0
- package/dist/core/module.d.ts +74 -0
- package/dist/core/module.js +3 -0
- package/dist/core/projection.d.ts +83 -0
- package/dist/core/projection.js +241 -0
- package/dist/core/protocol.d.ts +99 -0
- package/dist/core/protocol.js +101 -0
- package/dist/core/replay.d.ts +13 -0
- package/dist/core/replay.js +34 -0
- package/dist/core/schema.d.ts +35 -0
- package/dist/core/schema.js +130 -0
- package/dist/core/serialize.d.ts +14 -0
- package/dist/core/serialize.js +42 -0
- package/dist/core/snapshot-store.d.ts +39 -0
- package/dist/core/snapshot-store.js +53 -0
- package/dist/core/snapshots.d.ts +26 -0
- package/dist/core/snapshots.js +100 -0
- package/dist/core/sqlite-store.d.ts +31 -0
- package/dist/core/sqlite-store.js +328 -0
- package/dist/core/state.d.ts +43 -0
- package/dist/core/state.js +31 -0
- package/dist/core/store.d.ts +146 -0
- package/dist/core/store.js +229 -0
- package/dist/core/thread.d.ts +41 -0
- package/dist/core/thread.js +13 -0
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.js +42 -0
- package/dist/core/validate-event.d.ts +19 -0
- package/dist/core/validate-event.js +42 -0
- package/dist/debugger/components/debugger-split.d.ts +23 -0
- package/dist/debugger/components/debugger-split.js +25 -0
- package/dist/debugger/components/event-inspector.d.ts +5 -0
- package/dist/debugger/components/event-inspector.js +38 -0
- package/dist/debugger/components/event-stream.d.ts +13 -0
- package/dist/debugger/components/event-stream.js +119 -0
- package/dist/debugger/components/json-view.d.ts +5 -0
- package/dist/debugger/components/json-view.js +5 -0
- package/dist/debugger/components/run-tree.d.ts +14 -0
- package/dist/debugger/components/run-tree.js +38 -0
- package/dist/debugger/components/status-dot.d.ts +3 -0
- package/dist/debugger/components/status-dot.js +5 -0
- package/dist/debugger/contracts.d.ts +22 -0
- package/dist/debugger/contracts.js +1 -0
- package/dist/debugger/events/catalog.d.ts +6 -0
- package/dist/debugger/events/catalog.js +27 -0
- package/dist/debugger/events/domain-summarize.d.ts +3 -0
- package/dist/debugger/events/domain-summarize.js +107 -0
- package/dist/debugger/events/family.d.ts +8 -0
- package/dist/debugger/events/family.js +44 -0
- package/dist/debugger/events/protocol-summarize.d.ts +3 -0
- package/dist/debugger/events/protocol-summarize.js +70 -0
- package/dist/debugger/index.d.ts +17 -0
- package/dist/debugger/index.js +16 -0
- package/dist/debugger/lib/cn.d.ts +5 -0
- package/dist/debugger/lib/cn.js +17 -0
- package/dist/debugger/lib/event-counts.d.ts +5 -0
- package/dist/debugger/lib/event-counts.js +23 -0
- package/dist/debugger/lib/event-meta.d.ts +8 -0
- package/dist/debugger/lib/event-meta.js +20 -0
- package/dist/debugger/lib/project-run.d.ts +9 -0
- package/dist/debugger/lib/project-run.js +14 -0
- package/dist/debugger/lib/status.d.ts +3 -0
- package/dist/debugger/lib/status.js +31 -0
- package/dist/debugger/lib/visible-events.d.ts +8 -0
- package/dist/debugger/lib/visible-events.js +21 -0
- package/dist/debugger/styles.css +71 -0
- package/dist/debugger/ui/checkbox.d.ts +4 -0
- package/dist/debugger/ui/checkbox.js +9 -0
- package/dist/debugger/ui/input.d.ts +3 -0
- package/dist/debugger/ui/input.js +7 -0
- package/dist/debugger/ui/scroll-area.d.ts +9 -0
- package/dist/debugger/ui/scroll-area.js +10 -0
- package/dist/debugger/ui/tabs.d.ts +7 -0
- package/dist/debugger/ui/tabs.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/projectors/index-model.d.ts +56 -0
- package/dist/projectors/index-model.js +117 -0
- package/dist/projectors/index.d.ts +4 -0
- package/dist/projectors/index.js +3 -0
- package/dist/projectors/memory.d.ts +5 -0
- package/dist/projectors/memory.js +77 -0
- package/dist/projectors/postgres.d.ts +9 -0
- package/dist/projectors/postgres.js +108 -0
- package/dist/projectors/projector.d.ts +11 -0
- package/dist/projectors/projector.js +1 -0
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +43 -0
- package/dist/projectors/sqlite.d.ts +9 -0
- package/dist/projectors/sqlite.js +103 -0
- package/dist/projectors/with-projectors.d.ts +17 -0
- package/dist/projectors/with-projectors.js +55 -0
- package/dist/react/derived.d.ts +19 -0
- package/dist/react/derived.js +50 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +5 -0
- package/dist/react/materialize.d.ts +3 -0
- package/dist/react/materialize.js +86 -0
- package/dist/react/notify.d.ts +18 -0
- package/dist/react/notify.js +80 -0
- package/dist/react/react.d.ts +61 -0
- package/dist/react/react.js +187 -0
- package/dist/react/register.d.ts +31 -0
- package/dist/react/register.js +1 -0
- package/dist/react/sse.d.ts +1 -0
- package/dist/react/sse.js +1 -0
- package/dist/react/store.d.ts +56 -0
- package/dist/react/store.js +340 -0
- package/dist/react/tables.d.ts +21 -0
- package/dist/react/tables.js +6 -0
- package/dist/runtime/dispatch-effect.d.ts +3 -0
- package/dist/runtime/dispatch-effect.js +113 -0
- package/dist/runtime/events-api.d.ts +4 -0
- package/dist/runtime/events-api.js +84 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/looms.d.ts +74 -0
- package/dist/runtime/looms.js +109 -0
- package/dist/runtime/runtime-helpers.d.ts +51 -0
- package/dist/runtime/runtime-helpers.js +230 -0
- package/dist/runtime/runtime.d.ts +91 -0
- package/dist/runtime/runtime.js +531 -0
- package/dist/runtime/server.d.ts +17 -0
- package/dist/runtime/server.js +226 -0
- package/dist/runtime/sse.d.ts +15 -0
- package/dist/runtime/sse.js +96 -0
- package/dist/runtime/wake-scheduler.d.ts +8 -0
- package/dist/runtime/wake-scheduler.js +38 -0
- package/dist/s2/config.d.ts +27 -0
- package/dist/s2/config.js +54 -0
- package/dist/s2/index.d.ts +6 -0
- package/dist/s2/index.js +6 -0
- package/dist/s2/lite.d.ts +26 -0
- package/dist/s2/lite.js +150 -0
- package/dist/s2/projector.d.ts +8 -0
- package/dist/s2/projector.js +28 -0
- package/dist/s2/serialize.d.ts +1 -0
- package/dist/s2/serialize.js +1 -0
- package/dist/s2/snapshot-store.d.ts +17 -0
- package/dist/s2/snapshot-store.js +320 -0
- package/dist/s2/store.d.ts +35 -0
- package/dist/s2/store.js +337 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +63 -0
- package/dist/workflow/definitions-store.d.ts +11 -0
- package/dist/workflow/definitions-store.js +8 -0
- package/dist/workflow/definitions.d.ts +82 -0
- package/dist/workflow/definitions.js +32 -0
- package/dist/workflow/effects.d.ts +305 -0
- package/dist/workflow/effects.js +208 -0
- package/dist/workflow/events.d.ts +60 -0
- package/dist/workflow/events.js +39 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/module.d.ts +926 -0
- package/dist/workflow/module.js +19 -0
- package/dist/workflow/projections.d.ts +30 -0
- package/dist/workflow/projections.js +43 -0
- package/dist/workflow/scope.d.ts +61 -0
- package/dist/workflow/scope.js +7 -0
- package/dist/workflow/threads.d.ts +171 -0
- package/dist/workflow/threads.js +287 -0
- package/package.json +183 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { S2ConfigSchema, s2ConfigFromEnv, streamNameForRun } from './config.js';
|
|
3
|
+
import { s2 } from './store.js';
|
|
4
|
+
export { S2ConfigSchema, s2ConfigFromEnv, streamNameForRun };
|
|
5
|
+
/**
|
|
6
|
+
* Asynchronous S2 projector. The actor's local EventStore stays authoritative;
|
|
7
|
+
* this fans committed events out to `runs/{runId}` for the global stream lake.
|
|
8
|
+
*/
|
|
9
|
+
export function s2Projector(config) {
|
|
10
|
+
const store = s2(config);
|
|
11
|
+
return {
|
|
12
|
+
name: 's2',
|
|
13
|
+
project: async (events) => {
|
|
14
|
+
if (events.length === 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const byRun = new Map();
|
|
18
|
+
for (const event of events) {
|
|
19
|
+
const batch = byRun.get(event.runId) ?? [];
|
|
20
|
+
batch.push(event);
|
|
21
|
+
byRun.set(event.runId, batch);
|
|
22
|
+
}
|
|
23
|
+
for (const [runId, batch] of byRun) {
|
|
24
|
+
await Effect.runPromise(store.append(runId, batch));
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createKeyedSerializer, type KeyedSerializer } from '@swirls/looms/core';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createKeyedSerializer } from '@swirls/looms/core';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Layer } from 'effect';
|
|
2
|
+
import { SnapshotStoreTag, type RunSnapshot, type SnapshotStore } from '@swirls/looms/core';
|
|
3
|
+
import { type S2Config } from './config';
|
|
4
|
+
export interface SnapshotFrame {
|
|
5
|
+
readonly v: 1;
|
|
6
|
+
readonly runId: string;
|
|
7
|
+
readonly cursor: number;
|
|
8
|
+
readonly stateHash: string;
|
|
9
|
+
readonly takenAt: number;
|
|
10
|
+
readonly chunk: number;
|
|
11
|
+
readonly chunks: number;
|
|
12
|
+
readonly data: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function frameSnapshot(snapshot: RunSnapshot, maxChunkBytes?: number): SnapshotFrame[];
|
|
15
|
+
export declare function assembleSnapshot(frames: readonly SnapshotFrame[]): RunSnapshot;
|
|
16
|
+
export declare function s2SnapshotStore(config: S2Config): SnapshotStore;
|
|
17
|
+
export declare function S2SnapshotStoreLive(config: S2Config): Layer.Layer<SnapshotStoreTag, never, never>;
|
|
@@ -0,0 +1,320 @@
|
|
|
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';
|
|
4
|
+
import { S2ConfigSchema } from './config.js';
|
|
5
|
+
const DEFAULT_PREFIX = 'snapshots';
|
|
6
|
+
const DEFAULT_MAX_CHUNK_BYTES = 900 * 1024;
|
|
7
|
+
const SnapshotFrameSchema = Schema.Struct({
|
|
8
|
+
v: Schema.Literal(1),
|
|
9
|
+
runId: Schema.String,
|
|
10
|
+
cursor: Schema.Number,
|
|
11
|
+
stateHash: Schema.String,
|
|
12
|
+
takenAt: Schema.Number,
|
|
13
|
+
chunk: Schema.Number,
|
|
14
|
+
chunks: Schema.Number,
|
|
15
|
+
data: Schema.String,
|
|
16
|
+
});
|
|
17
|
+
export function frameSnapshot(snapshot, maxChunkBytes = DEFAULT_MAX_CHUNK_BYTES) {
|
|
18
|
+
const json = JSON.stringify(snapshot.state);
|
|
19
|
+
const chunks = Math.max(1, Math.ceil(json.length / maxChunkBytes));
|
|
20
|
+
const frames = [];
|
|
21
|
+
for (let i = 0; i < chunks; i++) {
|
|
22
|
+
frames.push({
|
|
23
|
+
v: 1,
|
|
24
|
+
runId: snapshot.runId,
|
|
25
|
+
cursor: snapshot.cursor,
|
|
26
|
+
stateHash: snapshot.stateHash,
|
|
27
|
+
takenAt: snapshot.takenAt,
|
|
28
|
+
chunk: i,
|
|
29
|
+
chunks,
|
|
30
|
+
data: json.slice(i * maxChunkBytes, (i + 1) * maxChunkBytes),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return frames;
|
|
34
|
+
}
|
|
35
|
+
export function assembleSnapshot(frames) {
|
|
36
|
+
if (frames.length === 0) {
|
|
37
|
+
throw new SnapshotStoreError('Cannot assemble snapshot from empty frames');
|
|
38
|
+
}
|
|
39
|
+
const first = frames[0];
|
|
40
|
+
const ordered = [...frames].sort((a, b) => a.chunk - b.chunk);
|
|
41
|
+
for (const frame of ordered) {
|
|
42
|
+
if (frame.cursor !== first.cursor ||
|
|
43
|
+
frame.chunks !== first.chunks ||
|
|
44
|
+
frame.runId !== first.runId) {
|
|
45
|
+
throw new SnapshotStoreError('Snapshot frames are inconsistent');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (ordered.length !== first.chunks) {
|
|
49
|
+
throw new SnapshotStoreError(`Incomplete snapshot: got ${ordered.length} of ${first.chunks} frames`);
|
|
50
|
+
}
|
|
51
|
+
for (let i = 0; i < first.chunks; i++) {
|
|
52
|
+
if (ordered[i]?.chunk !== i) {
|
|
53
|
+
throw new SnapshotStoreError(`Missing snapshot chunk ${i}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const json = ordered.map((frame) => frame.data).join('');
|
|
57
|
+
// SAFETY: frames are produced by `frameSnapshot` from a serialized RunState.
|
|
58
|
+
const state = JSON.parse(json);
|
|
59
|
+
return {
|
|
60
|
+
runId: first.runId,
|
|
61
|
+
cursor: first.cursor,
|
|
62
|
+
stateHash: first.stateHash,
|
|
63
|
+
takenAt: first.takenAt,
|
|
64
|
+
state,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function createClient(config) {
|
|
68
|
+
if (Schema.is(Schema.String)(config.endpoint)) {
|
|
69
|
+
return new S2({
|
|
70
|
+
accessToken: config.accessToken,
|
|
71
|
+
endpoints: new S2Endpoints({
|
|
72
|
+
account: config.endpoint,
|
|
73
|
+
basin: config.endpoint,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (config.endpoint) {
|
|
78
|
+
return new S2({
|
|
79
|
+
accessToken: config.accessToken,
|
|
80
|
+
endpoints: new S2Endpoints(config.endpoint),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return new S2({ accessToken: config.accessToken });
|
|
84
|
+
}
|
|
85
|
+
function parseFrame(body) {
|
|
86
|
+
try {
|
|
87
|
+
return Schema.decodeUnknownSync(SnapshotFrameSchema)(JSON.parse(body));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Trailing command records (the trim written by `prune`) are skipped by
|
|
95
|
+
* `ignoreCommandRecords`, so a `tailOffset: 1` read can come back empty.
|
|
96
|
+
* Probe a few records from the tail instead.
|
|
97
|
+
*/
|
|
98
|
+
const TAIL_PROBE = 4;
|
|
99
|
+
async function readFromTail(stream, tailOffset) {
|
|
100
|
+
try {
|
|
101
|
+
const page = await stream.read({
|
|
102
|
+
start: { from: { tailOffset } },
|
|
103
|
+
ignoreCommandRecords: true,
|
|
104
|
+
});
|
|
105
|
+
return [...page.records];
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
if (cause instanceof S2Error && (cause.status === 404 || cause.status === 416)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
throw cause;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Latest complete data frame near the tail, with its record, or undefined. */
|
|
115
|
+
async function readLatestFrame(stream) {
|
|
116
|
+
const records = (await readFromTail(stream, TAIL_PROBE)) ?? [];
|
|
117
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
118
|
+
const record = records[i];
|
|
119
|
+
const frame = parseFrame(record.body);
|
|
120
|
+
if (frame) {
|
|
121
|
+
return { record, frame };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
async function readAllSnapshotRecords(stream) {
|
|
127
|
+
const records = [];
|
|
128
|
+
let cursor = 0;
|
|
129
|
+
while (true) {
|
|
130
|
+
const page = await stream
|
|
131
|
+
.read({
|
|
132
|
+
start: { from: { seqNum: cursor }, clamp: true },
|
|
133
|
+
stop: { limits: { count: 1000 } },
|
|
134
|
+
ignoreCommandRecords: true,
|
|
135
|
+
})
|
|
136
|
+
.catch((cause) => {
|
|
137
|
+
// 404: no stream yet. 416: nothing at or past `cursor` (e.g. only a trim record).
|
|
138
|
+
if (cause instanceof S2Error && (cause.status === 404 || cause.status === 416)) {
|
|
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;
|
|
155
|
+
}
|
|
156
|
+
function groupCompleteSnapshots(records) {
|
|
157
|
+
const byCursor = new Map();
|
|
158
|
+
for (const record of records) {
|
|
159
|
+
const frame = parseFrame(record.body);
|
|
160
|
+
if (!frame) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const existing = byCursor.get(frame.cursor) ?? [];
|
|
164
|
+
existing.push(frame);
|
|
165
|
+
byCursor.set(frame.cursor, existing);
|
|
166
|
+
}
|
|
167
|
+
const assembled = [];
|
|
168
|
+
for (const frames of byCursor.values()) {
|
|
169
|
+
try {
|
|
170
|
+
assembled.push(assembleSnapshot(frames));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return assembled.sort((a, b) => a.cursor - b.cursor);
|
|
177
|
+
}
|
|
178
|
+
export function s2SnapshotStore(config) {
|
|
179
|
+
const parsed = Schema.decodeUnknownSync(S2ConfigSchema)(config);
|
|
180
|
+
const client = createClient(parsed);
|
|
181
|
+
const basin = client.basin(parsed.basin);
|
|
182
|
+
const prefix = parsed.snapshotPrefix ?? DEFAULT_PREFIX;
|
|
183
|
+
const appends = createKeyedSerializer();
|
|
184
|
+
const ensured = new Set();
|
|
185
|
+
let basinEnsured = false;
|
|
186
|
+
const streamName = (runId) => `${prefix}/${runId}`;
|
|
187
|
+
const ensureBasin = async () => {
|
|
188
|
+
if (basinEnsured) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await client.basins.create({ basin: parsed.basin });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (!(err instanceof S2Error && err.status === 409)) {
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
basinEnsured = true;
|
|
200
|
+
};
|
|
201
|
+
const ensureStream = async (runId) => {
|
|
202
|
+
await ensureBasin();
|
|
203
|
+
const name = streamName(runId);
|
|
204
|
+
if (!ensured.has(name)) {
|
|
205
|
+
try {
|
|
206
|
+
await basin.streams.create({
|
|
207
|
+
stream: name,
|
|
208
|
+
config: { retentionPolicy: { infinite: {} } },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (!(err instanceof S2Error &&
|
|
213
|
+
(err.status === 409 || err.status === 404 || err.status === 405))) {
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
ensured.add(name);
|
|
218
|
+
}
|
|
219
|
+
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);
|
|
224
|
+
const service = {
|
|
225
|
+
save: (snapshot) => Effect.tryPromise({
|
|
226
|
+
try: async () => {
|
|
227
|
+
const stream = await ensureStream(snapshot.runId);
|
|
228
|
+
const frames = frameSnapshot(snapshot);
|
|
229
|
+
const records = frames.map((frame) => AppendRecord.string({
|
|
230
|
+
body: JSON.stringify(frame),
|
|
231
|
+
headers: [
|
|
232
|
+
['looms-cursor', String(frame.cursor)],
|
|
233
|
+
['looms-chunk', `${frame.chunk}/${frame.chunks}`],
|
|
234
|
+
],
|
|
235
|
+
}));
|
|
236
|
+
await appends.run(snapshot.runId, async () => {
|
|
237
|
+
if (records.length === 1) {
|
|
238
|
+
await stream.append(AppendInput.create(records));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const record of records) {
|
|
242
|
+
await stream.append(AppendInput.create([record]));
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
catch: (cause) => toError(cause, `Failed to save snapshot for ${snapshot.runId}`),
|
|
247
|
+
}),
|
|
248
|
+
loadLatest: (runId) => Effect.tryPromise({
|
|
249
|
+
try: async () => {
|
|
250
|
+
const stream = await ensureStream(runId);
|
|
251
|
+
const tailFrame = await readLatestFrame(stream);
|
|
252
|
+
if (!tailFrame) {
|
|
253
|
+
return Option.none();
|
|
254
|
+
}
|
|
255
|
+
if (tailFrame.frame.chunk === tailFrame.frame.chunks - 1) {
|
|
256
|
+
const lastFrame = tailFrame.frame;
|
|
257
|
+
const records = (await readFromTail(stream, lastFrame.chunks + TAIL_PROBE)) ?? [];
|
|
258
|
+
const frames = records
|
|
259
|
+
.map((record) => parseFrame(record.body))
|
|
260
|
+
.filter((frame) => frame !== undefined)
|
|
261
|
+
.filter((frame) => frame.cursor === lastFrame.cursor);
|
|
262
|
+
if (frames.length === lastFrame.chunks) {
|
|
263
|
+
return Option.some(assembleSnapshot(frames));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Incomplete or interleaved tail: fall back to scanning the retained stream.
|
|
267
|
+
const records = await readAllSnapshotRecords(stream);
|
|
268
|
+
const complete = groupCompleteSnapshots(records);
|
|
269
|
+
const newest = complete[complete.length - 1];
|
|
270
|
+
return newest ? Option.some(newest) : Option.none();
|
|
271
|
+
},
|
|
272
|
+
catch: (cause) => toError(cause, `Failed to load snapshot for ${runId}`),
|
|
273
|
+
}),
|
|
274
|
+
listCursors: (runId) => Effect.tryPromise({
|
|
275
|
+
try: async () => {
|
|
276
|
+
const stream = await ensureStream(runId);
|
|
277
|
+
const records = await readAllSnapshotRecords(stream);
|
|
278
|
+
return groupCompleteSnapshots(records).map((snapshot) => snapshot.cursor);
|
|
279
|
+
},
|
|
280
|
+
catch: (cause) => toError(cause, `Failed to list snapshot cursors for ${runId}`),
|
|
281
|
+
}),
|
|
282
|
+
prune: (runId, keepLatest) => Effect.tryPromise({
|
|
283
|
+
try: async () => {
|
|
284
|
+
const stream = await ensureStream(runId);
|
|
285
|
+
if (keepLatest <= 1) {
|
|
286
|
+
// Fast path: the latest complete frame tells us where the latest snapshot starts.
|
|
287
|
+
const latest = await readLatestFrame(stream);
|
|
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) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const firstKept = records.find((record) => parseFrame(record.body)?.cursor === keep.cursor);
|
|
308
|
+
if (!firstKept) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
await appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(firstKept.seqNum)])));
|
|
312
|
+
},
|
|
313
|
+
catch: (cause) => toError(cause, `Failed to prune snapshots for ${runId}`),
|
|
314
|
+
}),
|
|
315
|
+
};
|
|
316
|
+
return service;
|
|
317
|
+
}
|
|
318
|
+
export function S2SnapshotStoreLive(config) {
|
|
319
|
+
return Layer.succeed(SnapshotStoreTag, s2SnapshotStore(config));
|
|
320
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Effect, Layer, Stream } from 'effect';
|
|
2
|
+
import { EventStoreError, EventStoreTag, type EventStore } from '@swirls/looms/core';
|
|
3
|
+
import { type S2Config } from './config';
|
|
4
|
+
export interface S2ReadBatchLike<T> {
|
|
5
|
+
readonly records: ReadonlyArray<T>;
|
|
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>;
|
|
24
|
+
export declare function readAllPages<T extends {
|
|
25
|
+
readonly seqNum: number;
|
|
26
|
+
}>(fetchPage: (cursor: number, count: number) => Promise<S2ReadBatchLike<T> | null>, options?: ReadAllPagesOptions): Promise<T[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Synchronous constructor for an S2-backed EventStore.
|
|
29
|
+
*
|
|
30
|
+
* Basin creation is deferred and memoized lazily on first stream access.
|
|
31
|
+
* Expected tail mismatches during `append` map to `EventStoreConflictError`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function s2(config: S2Config): EventStore;
|
|
34
|
+
export declare const makeS2EventStore: (config: S2Config) => Effect.Effect<EventStore, EventStoreError>;
|
|
35
|
+
export declare const S2EventStoreLive: (config: S2Config) => Layer.Layer<EventStoreTag, EventStoreError, never>;
|