@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
package/dist/s2/store.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { AppendInput, AppendRecord, S2, S2Endpoints, S2Error } from '@s2-dev/streamstore';
|
|
2
|
+
import { Effect, Layer, Option, Predicate, Queue, Schema, Stream } from 'effect';
|
|
3
|
+
import { createKeyedSerializer, EventStoreConflictError, EventStoreError, EventStoreFencedError, EventStoreTag, EventEnvelopeSchema, fromWireEvent, withSnapshotStore, } from '@swirls/looms/core';
|
|
4
|
+
import { S2ConfigSchema, streamNameForRun } from './config.js';
|
|
5
|
+
import { s2SnapshotStore } from './snapshot-store.js';
|
|
6
|
+
function toStoreError(cause, message) {
|
|
7
|
+
if (cause instanceof EventStoreError) {
|
|
8
|
+
return cause;
|
|
9
|
+
}
|
|
10
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
11
|
+
return new EventStoreError(`${message}: ${detail}`, cause);
|
|
12
|
+
}
|
|
13
|
+
function isUnsatisfiableCause(cause) {
|
|
14
|
+
if (cause instanceof S2Error && (cause.status === 404 || cause.status === 416)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (Predicate.isReadonlyObject(cause) && (cause.status === 404 || cause.status === 416)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const text = cause instanceof Error ? cause.message : String(cause);
|
|
21
|
+
return text.includes('out of range') || text.includes('Range not satisfiable');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Paginate an S2 stream lazily using Effect Streams.
|
|
25
|
+
*
|
|
26
|
+
* Pulls pages on demand until the stream tail is reached, an empty page is returned,
|
|
27
|
+
* or the caller's optional limit is satisfied.
|
|
28
|
+
*/
|
|
29
|
+
export function paginateS2Stream(fetchPage, options) {
|
|
30
|
+
const fromSeq = options?.fromSeq ?? 0;
|
|
31
|
+
const userLimit = options?.limit;
|
|
32
|
+
const maxPageSize = options?.maxPageSize ?? 1000;
|
|
33
|
+
if (userLimit !== undefined && userLimit <= 0) {
|
|
34
|
+
return Stream.empty;
|
|
35
|
+
}
|
|
36
|
+
const baseStream = Stream.paginate({ cursor: fromSeq, remaining: userLimit }, (state) => Effect.gen(function* () {
|
|
37
|
+
if (state.remaining !== undefined && state.remaining <= 0) {
|
|
38
|
+
return [[], Option.none()];
|
|
39
|
+
}
|
|
40
|
+
const count = state.remaining !== undefined ? Math.min(maxPageSize, state.remaining) : maxPageSize;
|
|
41
|
+
const page = yield* fetchPage(state.cursor, count);
|
|
42
|
+
if (!page || page.records.length === 0) {
|
|
43
|
+
return [[], Option.none()];
|
|
44
|
+
}
|
|
45
|
+
const records = page.records.filter((r) => r.seqNum >= fromSeq);
|
|
46
|
+
const nextRemaining = state.remaining !== undefined ? state.remaining - records.length : undefined;
|
|
47
|
+
const lastRecord = page.records[page.records.length - 1];
|
|
48
|
+
if (!lastRecord || (nextRemaining !== undefined && nextRemaining <= 0)) {
|
|
49
|
+
return [records, Option.none()];
|
|
50
|
+
}
|
|
51
|
+
const nextCursor = Math.max(state.cursor + 1, lastRecord.seqNum + 1);
|
|
52
|
+
if (nextCursor <= state.cursor) {
|
|
53
|
+
return [records, Option.none()];
|
|
54
|
+
}
|
|
55
|
+
// A byte-capped page is shorter than `count`. Do not trust `page.tail`
|
|
56
|
+
// there — S2 may report the batch end, not the stream tail.
|
|
57
|
+
const shortPage = page.records.length < count;
|
|
58
|
+
const reachedTail = !shortPage && page.tail?.seqNum !== undefined && nextCursor >= page.tail.seqNum;
|
|
59
|
+
if (reachedTail) {
|
|
60
|
+
return [records, Option.none()];
|
|
61
|
+
}
|
|
62
|
+
return [records, Option.some({ cursor: nextCursor, remaining: nextRemaining })];
|
|
63
|
+
}));
|
|
64
|
+
return userLimit !== undefined ? baseStream.pipe(Stream.take(userLimit)) : baseStream;
|
|
65
|
+
}
|
|
66
|
+
export async function readAllPages(fetchPage, options) {
|
|
67
|
+
const fetchPageEffect = (cursor, count) => Effect.tryPromise({
|
|
68
|
+
try: () => fetchPage(cursor, count),
|
|
69
|
+
catch: (cause) => toStoreError(cause, 'S2 page fetch failed'),
|
|
70
|
+
});
|
|
71
|
+
const stream = paginateS2Stream(fetchPageEffect, options);
|
|
72
|
+
return Effect.runPromise(Stream.runCollect(stream));
|
|
73
|
+
}
|
|
74
|
+
function parseEvent(body, seqNum) {
|
|
75
|
+
const raw = JSON.parse(body);
|
|
76
|
+
const decoded = Schema.decodeUnknownSync(EventEnvelopeSchema)(raw);
|
|
77
|
+
return fromWireEvent({ ...decoded, seq: seqNum + 1 });
|
|
78
|
+
}
|
|
79
|
+
function createClient(config) {
|
|
80
|
+
if (Schema.is(Schema.String)(config.endpoint)) {
|
|
81
|
+
return new S2({
|
|
82
|
+
accessToken: config.accessToken,
|
|
83
|
+
endpoints: new S2Endpoints({
|
|
84
|
+
account: config.endpoint,
|
|
85
|
+
basin: config.endpoint,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (config.endpoint) {
|
|
90
|
+
return new S2({
|
|
91
|
+
accessToken: config.accessToken,
|
|
92
|
+
endpoints: new S2Endpoints(config.endpoint),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return new S2({ accessToken: config.accessToken });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Synchronous constructor for an S2-backed EventStore.
|
|
99
|
+
*
|
|
100
|
+
* Basin creation is deferred and memoized lazily on first stream access.
|
|
101
|
+
* Expected tail mismatches during `append` map to `EventStoreConflictError`.
|
|
102
|
+
*/
|
|
103
|
+
export function s2(config) {
|
|
104
|
+
const parsed = Schema.decodeUnknownSync(S2ConfigSchema)(config);
|
|
105
|
+
const client = createClient(parsed);
|
|
106
|
+
const basin = client.basin(parsed.basin);
|
|
107
|
+
const ensured = new Set();
|
|
108
|
+
const appends = createKeyedSerializer();
|
|
109
|
+
let basinEnsured = false;
|
|
110
|
+
const ensureBasin = Effect.tryPromise({
|
|
111
|
+
try: async () => {
|
|
112
|
+
if (basinEnsured) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await client.basins.create({ basin: parsed.basin });
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
// 409 = already exists
|
|
120
|
+
if (!(err instanceof S2Error && err.status === 409)) {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
basinEnsured = true;
|
|
125
|
+
},
|
|
126
|
+
catch: (cause) => toStoreError(cause, `Failed to ensure basin ${parsed.basin}`),
|
|
127
|
+
});
|
|
128
|
+
const ensureStream = (runId) => Effect.gen(function* () {
|
|
129
|
+
yield* ensureBasin;
|
|
130
|
+
return yield* Effect.tryPromise({
|
|
131
|
+
try: async () => {
|
|
132
|
+
const name = streamNameForRun(runId);
|
|
133
|
+
if (ensured.has(name)) {
|
|
134
|
+
return basin.stream(name);
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await basin.streams.create({
|
|
138
|
+
stream: name,
|
|
139
|
+
config: parsed.streamConfig,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
// 409 = already exists
|
|
144
|
+
if (!(err instanceof S2Error && err.status === 409)) {
|
|
145
|
+
// Some local servers may not support create; try using the stream anyway.
|
|
146
|
+
if (!(err instanceof S2Error && (err.status === 404 || err.status === 405))) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
ensured.add(name);
|
|
152
|
+
return basin.stream(name);
|
|
153
|
+
},
|
|
154
|
+
catch: (cause) => toStoreError(cause, `Failed to open stream for ${runId}`),
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
const service = {
|
|
158
|
+
append: (runId, events, options) => Effect.gen(function* () {
|
|
159
|
+
if (events.length === 0 && options?.fence === undefined) {
|
|
160
|
+
const tail = yield* service.tail(runId);
|
|
161
|
+
return { sequences: [], tail };
|
|
162
|
+
}
|
|
163
|
+
const stream = yield* ensureStream(runId);
|
|
164
|
+
const fenced = options?.fence !== undefined;
|
|
165
|
+
const eventRecords = events.map((partial) => {
|
|
166
|
+
const body = JSON.stringify({
|
|
167
|
+
...partial,
|
|
168
|
+
runId,
|
|
169
|
+
seq: partial.seq ?? 0,
|
|
170
|
+
});
|
|
171
|
+
return AppendRecord.string({
|
|
172
|
+
body,
|
|
173
|
+
headers: [['content-type', 'application/json']],
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
const records = fenced ? [AppendRecord.fence(options.fence), ...eventRecords] : eventRecords;
|
|
177
|
+
const input = AppendInput.create(records, {
|
|
178
|
+
matchSeqNum: options?.expectedTail,
|
|
179
|
+
fencingToken: options?.fencingToken,
|
|
180
|
+
});
|
|
181
|
+
const appendResult = yield* Effect.promise(() => appends.run(runId, () => stream.append(input).then((ack) => ({ ok: true, ack }), (err) => ({ ok: false, err }))));
|
|
182
|
+
if (!appendResult.ok) {
|
|
183
|
+
const cause = appendResult.err;
|
|
184
|
+
const message = cause instanceof Error ? cause.message.toLowerCase() : String(cause);
|
|
185
|
+
const isFence = cause instanceof S2Error &&
|
|
186
|
+
cause.status === 412 &&
|
|
187
|
+
(message.includes('fenc') || message.includes('token'));
|
|
188
|
+
if (isFence) {
|
|
189
|
+
return yield* Effect.fail(new EventStoreFencedError(runId, options?.fencingToken ?? options?.fence ?? ''));
|
|
190
|
+
}
|
|
191
|
+
if (cause instanceof S2Error &&
|
|
192
|
+
(cause.status === 409 ||
|
|
193
|
+
cause.status === 412 ||
|
|
194
|
+
message.includes('conflict') ||
|
|
195
|
+
message.includes('match'))) {
|
|
196
|
+
const tailCheck = yield* Effect.promise(() => stream.checkTail().catch(() => null));
|
|
197
|
+
const actualTail = tailCheck?.tail?.seqNum ?? 0;
|
|
198
|
+
return yield* Effect.fail(new EventStoreConflictError(runId, options?.expectedTail ?? 0, actualTail));
|
|
199
|
+
}
|
|
200
|
+
return yield* Effect.fail(toStoreError(cause, `Append failed for ${runId}`));
|
|
201
|
+
}
|
|
202
|
+
const ack = appendResult.ack;
|
|
203
|
+
const sequences = [];
|
|
204
|
+
const eventOffset = fenced ? 1 : 0;
|
|
205
|
+
for (let i = 0; i < events.length; i++) {
|
|
206
|
+
sequences.push(ack.start.seqNum + eventOffset + i + 1);
|
|
207
|
+
}
|
|
208
|
+
return { sequences, tail: ack.tail.seqNum };
|
|
209
|
+
}),
|
|
210
|
+
readStream: (runId, options) => {
|
|
211
|
+
const fromSeq = options?.fromSeq ?? 1;
|
|
212
|
+
const s2From = Math.max(0, fromSeq - 1);
|
|
213
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
214
|
+
const stream = yield* ensureStream(runId);
|
|
215
|
+
const fetchPage = (cursor, count) => Effect.tryPromise({
|
|
216
|
+
try: async () => {
|
|
217
|
+
try {
|
|
218
|
+
return await stream.read({
|
|
219
|
+
start: { from: { seqNum: cursor }, clamp: true },
|
|
220
|
+
stop: { limits: { count } },
|
|
221
|
+
ignoreCommandRecords: true,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (cause) {
|
|
225
|
+
if (isUnsatisfiableCause(cause)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
throw cause;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
catch: (cause) => toStoreError(cause, `Read failed for ${runId}`),
|
|
232
|
+
});
|
|
233
|
+
const recordStream = paginateS2Stream(fetchPage, {
|
|
234
|
+
fromSeq: s2From,
|
|
235
|
+
limit: options?.limit,
|
|
236
|
+
});
|
|
237
|
+
return recordStream.pipe(Stream.map((r) => parseEvent(r.body, r.seqNum)));
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
read: (runId, options) => Effect.gen(function* () {
|
|
241
|
+
const stream = service.readStream(runId, options);
|
|
242
|
+
return yield* Stream.runCollect(stream);
|
|
243
|
+
}),
|
|
244
|
+
tail: (runId) => Effect.gen(function* () {
|
|
245
|
+
const stream = yield* ensureStream(runId);
|
|
246
|
+
const res = yield* Effect.tryPromise({
|
|
247
|
+
try: () => stream.checkTail(),
|
|
248
|
+
catch: (cause) => toStoreError(cause, `Tail failed for ${runId}`),
|
|
249
|
+
}).pipe(Effect.catch((err) => err.cause instanceof S2Error && err.cause.status === 404
|
|
250
|
+
? Effect.succeed({ tail: { seqNum: 0, timestamp: new Date(0) } })
|
|
251
|
+
: Effect.fail(err)));
|
|
252
|
+
return res.tail.seqNum;
|
|
253
|
+
}),
|
|
254
|
+
bounds: (runId) => Effect.gen(function* () {
|
|
255
|
+
const stream = yield* ensureStream(runId);
|
|
256
|
+
const page = yield* Effect.tryPromise({
|
|
257
|
+
try: async () => {
|
|
258
|
+
try {
|
|
259
|
+
return await stream.read({
|
|
260
|
+
start: { from: { seqNum: 0 }, clamp: true },
|
|
261
|
+
stop: { limits: { count: 1 } },
|
|
262
|
+
ignoreCommandRecords: false,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (cause) {
|
|
266
|
+
if (isUnsatisfiableCause(cause)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
throw cause;
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
catch: (cause) => toStoreError(cause, `Bounds failed for ${runId}`),
|
|
273
|
+
});
|
|
274
|
+
const tail = page?.tail?.seqNum ?? 0;
|
|
275
|
+
const first = page?.records[0];
|
|
276
|
+
const head = first ? first.seqNum + 1 : tail + 1;
|
|
277
|
+
return { head, tail };
|
|
278
|
+
}),
|
|
279
|
+
trim: (runId, beforeSeq) => Effect.gen(function* () {
|
|
280
|
+
const stream = yield* ensureStream(runId);
|
|
281
|
+
const trimTo = Math.max(0, beforeSeq - 1);
|
|
282
|
+
yield* Effect.tryPromise({
|
|
283
|
+
try: () => appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(trimTo)]))),
|
|
284
|
+
catch: (cause) => toStoreError(cause, `Trim failed for ${runId}`),
|
|
285
|
+
});
|
|
286
|
+
}),
|
|
287
|
+
subscribe: (runId, options) => Stream.callback((queue) => Effect.callback((resume) => {
|
|
288
|
+
const fromSeq = options?.fromSeq ?? 1;
|
|
289
|
+
const s2From = Math.max(0, fromSeq - 1);
|
|
290
|
+
let stopped = false;
|
|
291
|
+
const run = async () => {
|
|
292
|
+
try {
|
|
293
|
+
const stream = await Effect.runPromise(ensureStream(runId));
|
|
294
|
+
const session = await stream.readSession({
|
|
295
|
+
start: { from: { seqNum: s2From }, clamp: true },
|
|
296
|
+
ignoreCommandRecords: true,
|
|
297
|
+
});
|
|
298
|
+
for await (const record of session) {
|
|
299
|
+
if (stopped) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (record.seqNum < s2From) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
Queue.offerUnsafe(queue, parseEvent(record.body, record.seqNum));
|
|
306
|
+
}
|
|
307
|
+
Queue.endUnsafe(queue);
|
|
308
|
+
resume(Effect.void);
|
|
309
|
+
}
|
|
310
|
+
catch (cause) {
|
|
311
|
+
await Effect.runPromise(Queue.fail(queue, toStoreError(cause, `Subscribe failed for ${runId}`)));
|
|
312
|
+
resume(Effect.void);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
void run();
|
|
316
|
+
return Effect.sync(() => {
|
|
317
|
+
stopped = true;
|
|
318
|
+
});
|
|
319
|
+
})),
|
|
320
|
+
listRuns: () => Effect.tryPromise({
|
|
321
|
+
try: async () => {
|
|
322
|
+
const listed = await basin.streams.list({ prefix: 'runs/' });
|
|
323
|
+
return listed.streams
|
|
324
|
+
.map((s) => s.name)
|
|
325
|
+
.filter((name) => name.startsWith('runs/'))
|
|
326
|
+
.map((name) => name.slice('runs/'.length));
|
|
327
|
+
},
|
|
328
|
+
catch: (cause) => toStoreError(cause, 'listRuns failed'),
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
return withSnapshotStore(service, s2SnapshotStore(parsed));
|
|
332
|
+
}
|
|
333
|
+
export const makeS2EventStore = (config) => Effect.try({
|
|
334
|
+
try: () => s2(config),
|
|
335
|
+
catch: (cause) => new EventStoreError('Failed to initialize S2 event store', cause),
|
|
336
|
+
});
|
|
337
|
+
export const S2EventStoreLive = (config) => Layer.effect(EventStoreTag, makeS2EventStore(config));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { EventStoreTag, type AnyRuntimeModule, type EventStore } from '@swirls/looms/core';
|
|
3
|
+
import { type LoomsRuntime } from '@swirls/looms/runtime';
|
|
4
|
+
export interface TestRuntime {
|
|
5
|
+
readonly runtime: LoomsRuntime;
|
|
6
|
+
readonly store: EventStore;
|
|
7
|
+
run<A, E>(effect: Effect.Effect<A, E, EventStoreTag>): Promise<A>;
|
|
8
|
+
}
|
|
9
|
+
export declare function createTestRuntime(modules: readonly AnyRuntimeModule[]): Promise<TestRuntime>;
|
|
10
|
+
export declare function assertReplayDeterministic(test: TestRuntime, runId: string): Promise<void>;
|
|
11
|
+
export declare function moduleConformance(module: AnyRuntimeModule): string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Effect, Predicate } from 'effect';
|
|
2
|
+
import { composeModules, EventStoreTag, foldRun, makeMemoryEventStore, ModuleCompositionError, } from '@swirls/looms/core';
|
|
3
|
+
import { createRuntime } from '@swirls/looms/runtime';
|
|
4
|
+
function namedEffects(effects) {
|
|
5
|
+
const listed = [];
|
|
6
|
+
for (const key of Object.keys(effects)) {
|
|
7
|
+
const value = effects[key];
|
|
8
|
+
if (!value || !Predicate.isString(value.type)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
listed.push({
|
|
12
|
+
type: value.type,
|
|
13
|
+
hasExecute: 'execute' in value,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return listed;
|
|
17
|
+
}
|
|
18
|
+
export async function createTestRuntime(modules) {
|
|
19
|
+
const store = await Effect.runPromise(makeMemoryEventStore);
|
|
20
|
+
const runtime = createRuntime({ modules, store });
|
|
21
|
+
return {
|
|
22
|
+
runtime,
|
|
23
|
+
store,
|
|
24
|
+
run: (effect) => Effect.runPromise(Effect.provideService(effect, EventStoreTag, store)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function assertReplayDeterministic(test, runId) {
|
|
28
|
+
const events = await test.run(test.runtime.getEvents(runId));
|
|
29
|
+
const first = foldRun(events, test.runtime.registry, { runId });
|
|
30
|
+
const second = foldRun(events, test.runtime.registry, { runId });
|
|
31
|
+
if (JSON.stringify(first) !== JSON.stringify(second)) {
|
|
32
|
+
throw new Error(`Replay is not deterministic for run ${runId}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function moduleConformance(module) {
|
|
36
|
+
const errors = [];
|
|
37
|
+
if (!module.namespace) {
|
|
38
|
+
errors.push('namespace required');
|
|
39
|
+
}
|
|
40
|
+
if (!module.protocolVersion) {
|
|
41
|
+
errors.push('protocolVersion required');
|
|
42
|
+
}
|
|
43
|
+
if (module.events && module.events.namespace !== module.namespace) {
|
|
44
|
+
errors.push(`catalog namespace ${module.events.namespace} !== module ${module.namespace}`);
|
|
45
|
+
}
|
|
46
|
+
if (module.effects) {
|
|
47
|
+
for (const effect of namedEffects(module.effects)) {
|
|
48
|
+
if (!effect.type.startsWith(`${module.namespace}.`)) {
|
|
49
|
+
errors.push(`effect ${effect.type} is not namespaced under ${module.namespace}`);
|
|
50
|
+
}
|
|
51
|
+
if (!effect.hasExecute) {
|
|
52
|
+
errors.push(`effect ${effect.type} is missing execute`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
composeModules([module]);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
errors.push(err instanceof ModuleCompositionError ? err.message : String(err));
|
|
61
|
+
}
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Context, Layer } from 'effect';
|
|
2
|
+
import type { WorkflowDefinition } from './definitions';
|
|
3
|
+
export interface WorkflowDefinitionStore {
|
|
4
|
+
get(name: string): WorkflowDefinition | undefined;
|
|
5
|
+
}
|
|
6
|
+
declare const WorkflowDefinitionsTag_base: Context.ServiceClass<WorkflowDefinitionsTag, "looms/WorkflowDefinitions", WorkflowDefinitionStore>;
|
|
7
|
+
export declare class WorkflowDefinitionsTag extends WorkflowDefinitionsTag_base {
|
|
8
|
+
}
|
|
9
|
+
export declare function makeWorkflowDefinitionStore(definitions: ReadonlyArray<WorkflowDefinition>): WorkflowDefinitionStore;
|
|
10
|
+
export declare const WorkflowDefinitionsLive: (definitions: ReadonlyArray<WorkflowDefinition>) => Layer.Layer<WorkflowDefinitionsTag, never, never>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context, Layer } from 'effect';
|
|
2
|
+
export class WorkflowDefinitionsTag extends Context.Service()('looms/WorkflowDefinitions') {
|
|
3
|
+
}
|
|
4
|
+
export function makeWorkflowDefinitionStore(definitions) {
|
|
5
|
+
const map = new Map(definitions.map((def) => [def.name, def]));
|
|
6
|
+
return { get: (name) => map.get(name) };
|
|
7
|
+
}
|
|
8
|
+
export const WorkflowDefinitionsLive = (definitions) => Layer.succeed(WorkflowDefinitionsTag, makeWorkflowDefinitionStore(definitions));
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { Schema } from 'effect';
|
|
3
|
+
import type { InferDefinedSchema, JsonValue, RuntimeEffect } from '@swirls/looms/core';
|
|
4
|
+
export declare const NodeStatusSchema: Schema.Union<readonly [Schema.Literal<"pending">, Schema.Literal<"running">, Schema.Literal<"completed">, Schema.Literal<"failed">, Schema.Literal<"skipped">]>;
|
|
5
|
+
export type NodeStatus = Schema.Schema.Type<typeof NodeStatusSchema>;
|
|
6
|
+
export declare const NodeStateSchema: Schema.Struct<{
|
|
7
|
+
readonly status: Schema.Union<readonly [Schema.Literal<"pending">, Schema.Literal<"running">, Schema.Literal<"completed">, Schema.Literal<"failed">, Schema.Literal<"skipped">]>;
|
|
8
|
+
readonly result: Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
9
|
+
readonly error: Schema.NullOr<Schema.String>;
|
|
10
|
+
}>;
|
|
11
|
+
export type NodeState = Schema.Schema.Type<typeof NodeStateSchema>;
|
|
12
|
+
export interface WorkflowNodeContext<TInput = JsonValue> {
|
|
13
|
+
readonly threadId: string;
|
|
14
|
+
readonly nodeId: string;
|
|
15
|
+
readonly input: TInput;
|
|
16
|
+
readonly results: {
|
|
17
|
+
[nodeId: string]: JsonValue | null;
|
|
18
|
+
};
|
|
19
|
+
spawn(child: {
|
|
20
|
+
kind: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}, input: JsonValue): NodeResult;
|
|
23
|
+
sleep(ms: number): NodeResult;
|
|
24
|
+
effects(effects: RuntimeEffect[]): NodeResult;
|
|
25
|
+
}
|
|
26
|
+
export type NodeResult = {
|
|
27
|
+
type: 'value';
|
|
28
|
+
value: JsonValue;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'spawn';
|
|
31
|
+
kind: string;
|
|
32
|
+
name: string;
|
|
33
|
+
input: JsonValue;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'sleep';
|
|
36
|
+
ms: number;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'effects';
|
|
39
|
+
effects: RuntimeEffect[];
|
|
40
|
+
};
|
|
41
|
+
export interface WorkflowNodeDefinition<TInput = JsonValue> {
|
|
42
|
+
id: string;
|
|
43
|
+
deps?: string[];
|
|
44
|
+
run(ctx: WorkflowNodeContext<TInput>): Promise<JsonValue | NodeResult> | JsonValue | NodeResult;
|
|
45
|
+
}
|
|
46
|
+
export interface WorkflowDefinition<TName extends string = string, TInput = JsonValue, TOutput extends JsonValue = JsonValue> {
|
|
47
|
+
readonly kind: 'workflow';
|
|
48
|
+
readonly name: TName;
|
|
49
|
+
readonly description?: string;
|
|
50
|
+
readonly input?: StandardSchemaV1<any, TInput> | Schema.Schema<TInput>;
|
|
51
|
+
readonly nodes: WorkflowNodeDefinition<TInput>[];
|
|
52
|
+
readonly concurrency?: number;
|
|
53
|
+
output?(ctx: {
|
|
54
|
+
input: TInput;
|
|
55
|
+
results: {
|
|
56
|
+
[nodeId: string]: JsonValue | null;
|
|
57
|
+
};
|
|
58
|
+
}): TOutput;
|
|
59
|
+
}
|
|
60
|
+
export type AnyWorkflowDefinition = WorkflowDefinition;
|
|
61
|
+
export declare function defineWorkflow<TName extends string, TSchema = undefined, TInput = InferDefinedSchema<TSchema>, TOutput extends JsonValue = JsonValue>(def: {
|
|
62
|
+
name: TName;
|
|
63
|
+
description?: string;
|
|
64
|
+
input?: TSchema;
|
|
65
|
+
nodes: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
deps?: string[];
|
|
68
|
+
run: (ctx: WorkflowNodeContext<TInput>) => Promise<JsonValue | NodeResult> | JsonValue | NodeResult;
|
|
69
|
+
}>;
|
|
70
|
+
concurrency?: number;
|
|
71
|
+
output?: (ctx: {
|
|
72
|
+
input: TInput;
|
|
73
|
+
results: {
|
|
74
|
+
[nodeId: string]: JsonValue | null;
|
|
75
|
+
};
|
|
76
|
+
}) => TOutput;
|
|
77
|
+
}): WorkflowDefinition<TName, TInput, TOutput>;
|
|
78
|
+
export declare function readyNodes(workflow: WorkflowDefinition, nodeStates: {
|
|
79
|
+
[nodeId: string]: {
|
|
80
|
+
status: string;
|
|
81
|
+
};
|
|
82
|
+
}): string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
export const NodeStatusSchema = Schema.Union([
|
|
3
|
+
Schema.Literal('pending'),
|
|
4
|
+
Schema.Literal('running'),
|
|
5
|
+
Schema.Literal('completed'),
|
|
6
|
+
Schema.Literal('failed'),
|
|
7
|
+
Schema.Literal('skipped'),
|
|
8
|
+
]);
|
|
9
|
+
export const NodeStateSchema = Schema.Struct({
|
|
10
|
+
status: NodeStatusSchema,
|
|
11
|
+
result: Schema.NullOr(Schema.Json),
|
|
12
|
+
error: Schema.NullOr(Schema.String),
|
|
13
|
+
});
|
|
14
|
+
export function defineWorkflow(def) {
|
|
15
|
+
// SAFETY: factory fields match WorkflowDefinition.
|
|
16
|
+
return { kind: 'workflow', ...def, input: def.input };
|
|
17
|
+
}
|
|
18
|
+
export function readyNodes(workflow, nodeStates) {
|
|
19
|
+
return workflow.nodes
|
|
20
|
+
.filter((node) => {
|
|
21
|
+
const state = nodeStates[node.id];
|
|
22
|
+
if (state && state.status !== 'pending') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const deps = node.deps ?? [];
|
|
26
|
+
return deps.every((depId) => {
|
|
27
|
+
const dep = nodeStates[depId];
|
|
28
|
+
return dep?.status === 'completed' || dep?.status === 'skipped';
|
|
29
|
+
});
|
|
30
|
+
})
|
|
31
|
+
.map((node) => node.id);
|
|
32
|
+
}
|