@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,226 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import { createRunId, EventStoreTag, InvalidEventError, InvalidInputError, JsonValueSchema, } from '@swirls/looms/core';
|
|
3
|
+
import { handleEventsApi } from './events-api.js';
|
|
4
|
+
import { createEventStreamResponse } from './sse.js';
|
|
5
|
+
const StartRunBodySchema = Schema.Struct({
|
|
6
|
+
kind: Schema.optional(Schema.String),
|
|
7
|
+
definitionName: Schema.optional(Schema.String),
|
|
8
|
+
name: Schema.optional(Schema.String),
|
|
9
|
+
input: Schema.optional(JsonValueSchema),
|
|
10
|
+
runId: Schema.optional(Schema.String),
|
|
11
|
+
idempotencyKey: Schema.optional(Schema.String),
|
|
12
|
+
});
|
|
13
|
+
const SignalBodySchema = Schema.Struct({
|
|
14
|
+
events: Schema.optional(Schema.Array(Schema.Struct({
|
|
15
|
+
type: Schema.String,
|
|
16
|
+
payload: JsonValueSchema,
|
|
17
|
+
threadId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
18
|
+
}))),
|
|
19
|
+
type: Schema.optional(Schema.String),
|
|
20
|
+
payload: Schema.optional(JsonValueSchema),
|
|
21
|
+
threadId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
22
|
+
idempotencyKey: Schema.optional(Schema.String),
|
|
23
|
+
});
|
|
24
|
+
function extractIdempotencyKey(req, bodyKey) {
|
|
25
|
+
return (bodyKey ??
|
|
26
|
+
req.headers.get('idempotency-key') ??
|
|
27
|
+
req.headers.get('x-idempotency-key') ??
|
|
28
|
+
undefined);
|
|
29
|
+
}
|
|
30
|
+
function run(effect, store) {
|
|
31
|
+
return Effect.runPromise(Effect.provideService(effect, EventStoreTag, store));
|
|
32
|
+
}
|
|
33
|
+
function wantsRunStream(req, url) {
|
|
34
|
+
const accept = req.headers.get('accept') ?? '';
|
|
35
|
+
return url.searchParams.get('stream') === 'true' || accept.includes('text/event-stream');
|
|
36
|
+
}
|
|
37
|
+
function streamStartRun(req, runtime, store, args) {
|
|
38
|
+
return createEventStreamResponse({
|
|
39
|
+
signal: req.signal,
|
|
40
|
+
request: req,
|
|
41
|
+
store,
|
|
42
|
+
runId: args.runId,
|
|
43
|
+
fromSeq: 1,
|
|
44
|
+
onStart: async (write, close) => {
|
|
45
|
+
try {
|
|
46
|
+
const result = await run(runtime.startRun({
|
|
47
|
+
kind: args.kind,
|
|
48
|
+
definitionName: args.definitionName,
|
|
49
|
+
input: args.input,
|
|
50
|
+
runId: args.runId,
|
|
51
|
+
idempotencyKey: args.idempotencyKey,
|
|
52
|
+
}), store);
|
|
53
|
+
write(`event: done\ndata: ${JSON.stringify(result)}\n\n`);
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
const error = cause instanceof Error ? cause.message : String(cause);
|
|
57
|
+
write(`event: error\ndata: ${JSON.stringify({ error })}\n\n`);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
close();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function readJson(req) {
|
|
66
|
+
try {
|
|
67
|
+
const value = await req.json();
|
|
68
|
+
return Schema.decodeUnknownSync(JsonValueSchema)(value);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function isLoomsApiPath(path) {
|
|
75
|
+
return path === '/health' || path.startsWith('/api/events') || path.startsWith('/runs');
|
|
76
|
+
}
|
|
77
|
+
function toErrorResponse(err) {
|
|
78
|
+
if (err instanceof InvalidInputError || err instanceof InvalidEventError) {
|
|
79
|
+
return Response.json({ error: err.message, issues: err.issues }, { status: 400 });
|
|
80
|
+
}
|
|
81
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
82
|
+
}
|
|
83
|
+
export function createFetchHandler(options) {
|
|
84
|
+
const { runtime, store } = options;
|
|
85
|
+
return async (req) => {
|
|
86
|
+
const url = new URL(req.url);
|
|
87
|
+
const path = url.pathname;
|
|
88
|
+
if (!isLoomsApiPath(path)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const accept = req.headers.get('accept') ?? '';
|
|
92
|
+
if (req.method === 'GET' && accept.includes('text/html') && !path.startsWith('/api/')) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (path === '/health') {
|
|
96
|
+
return Response.json({ ok: true });
|
|
97
|
+
}
|
|
98
|
+
if (path.startsWith('/api/events')) {
|
|
99
|
+
return handleEventsApi(req, runtime, store);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
if (req.method === 'GET' && path === '/runs') {
|
|
103
|
+
const runIds = await run(runtime.listRuns(), store);
|
|
104
|
+
return Response.json({ runIds });
|
|
105
|
+
}
|
|
106
|
+
if (req.method === 'POST' && path === '/runs') {
|
|
107
|
+
const body = Schema.decodeUnknownSync(StartRunBodySchema)(await readJson(req));
|
|
108
|
+
const definitionName = body.definitionName ?? body.name;
|
|
109
|
+
const kind = body.kind;
|
|
110
|
+
if (!definitionName || !kind) {
|
|
111
|
+
return Response.json({ error: 'kind and definitionName required' }, { status: 400 });
|
|
112
|
+
}
|
|
113
|
+
const runId = body.runId ?? createRunId();
|
|
114
|
+
const idempotencyKey = extractIdempotencyKey(req, body.idempotencyKey);
|
|
115
|
+
if (wantsRunStream(req, url)) {
|
|
116
|
+
return streamStartRun(req, runtime, store, {
|
|
117
|
+
kind,
|
|
118
|
+
definitionName,
|
|
119
|
+
input: body.input ?? null,
|
|
120
|
+
runId,
|
|
121
|
+
idempotencyKey,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const result = await run(runtime.startRun({
|
|
125
|
+
kind,
|
|
126
|
+
definitionName,
|
|
127
|
+
input: body.input ?? null,
|
|
128
|
+
runId,
|
|
129
|
+
idempotencyKey,
|
|
130
|
+
}), store);
|
|
131
|
+
return Response.json(result);
|
|
132
|
+
}
|
|
133
|
+
const replayMatch = path.match(/^\/runs\/([^/]+)\/replay$/);
|
|
134
|
+
if (req.method === 'GET' && replayMatch) {
|
|
135
|
+
const runId = decodeURIComponent(replayMatch[1]);
|
|
136
|
+
const seq = Number(url.searchParams.get('seq') ?? '0');
|
|
137
|
+
const step = await run(runtime.replayTo(runId, seq), store);
|
|
138
|
+
return Response.json({ runId, step });
|
|
139
|
+
}
|
|
140
|
+
const projectionMatch = path.match(/^\/runs\/([^/]+)\/projections\/([^/]+)$/);
|
|
141
|
+
if (req.method === 'GET' && projectionMatch) {
|
|
142
|
+
const runId = decodeURIComponent(projectionMatch[1]);
|
|
143
|
+
const name = decodeURIComponent(projectionMatch[2]);
|
|
144
|
+
const definition = runtime.registry.projections.get(name);
|
|
145
|
+
if (!definition) {
|
|
146
|
+
return Response.json({ error: `Unknown projection: ${name}` }, { status: 404 });
|
|
147
|
+
}
|
|
148
|
+
const value = await run(runtime.project(runId, definition), store);
|
|
149
|
+
return Response.json({ runId, name, value });
|
|
150
|
+
}
|
|
151
|
+
const eventsMatch = path.match(/^\/runs\/([^/]+)\/events$/);
|
|
152
|
+
if (req.method === 'POST' && eventsMatch) {
|
|
153
|
+
const runId = decodeURIComponent(eventsMatch[1]);
|
|
154
|
+
const body = Schema.decodeUnknownSync(SignalBodySchema)(await readJson(req));
|
|
155
|
+
const events = body.events ??
|
|
156
|
+
(body.type
|
|
157
|
+
? [
|
|
158
|
+
{
|
|
159
|
+
type: body.type,
|
|
160
|
+
payload: body.payload ?? null,
|
|
161
|
+
threadId: body.threadId,
|
|
162
|
+
},
|
|
163
|
+
]
|
|
164
|
+
: []);
|
|
165
|
+
const idempotencyKey = extractIdempotencyKey(req, body.idempotencyKey);
|
|
166
|
+
const state = await run(runtime.signal(runId, events, { idempotencyKey }), store);
|
|
167
|
+
return Response.json({ runId, state });
|
|
168
|
+
}
|
|
169
|
+
if (req.method === 'GET' && eventsMatch) {
|
|
170
|
+
const runId = decodeURIComponent(eventsMatch[1]);
|
|
171
|
+
const fromSeq = url.searchParams.get('fromSeq')
|
|
172
|
+
? Number(url.searchParams.get('fromSeq'))
|
|
173
|
+
: undefined;
|
|
174
|
+
const limit = url.searchParams.get('limit')
|
|
175
|
+
? Number(url.searchParams.get('limit'))
|
|
176
|
+
: undefined;
|
|
177
|
+
const events = await run(runtime.getEvents(runId, { fromSeq, limit }), store);
|
|
178
|
+
return Response.json({ runId, events });
|
|
179
|
+
}
|
|
180
|
+
const threadsMatch = path.match(/^\/runs\/([^/]+)\/(threads)$/);
|
|
181
|
+
if (req.method === 'GET' && threadsMatch) {
|
|
182
|
+
const runId = decodeURIComponent(threadsMatch[1]);
|
|
183
|
+
const state = await run(runtime.getRun(runId), store);
|
|
184
|
+
return Response.json({ runId, threads: state.threads });
|
|
185
|
+
}
|
|
186
|
+
const wakeMatch = path.match(/^\/runs\/([^/]+)\/wake$/);
|
|
187
|
+
if (req.method === 'POST' && wakeMatch) {
|
|
188
|
+
const runId = decodeURIComponent(wakeMatch[1]);
|
|
189
|
+
const state = await run(runtime.wake(runId), store);
|
|
190
|
+
return Response.json({ runId, state });
|
|
191
|
+
}
|
|
192
|
+
const runMatch = path.match(/^\/runs\/([^/]+)$/);
|
|
193
|
+
if (req.method === 'GET' && runMatch) {
|
|
194
|
+
const runId = decodeURIComponent(runMatch[1]);
|
|
195
|
+
const state = await run(runtime.getRun(runId), store);
|
|
196
|
+
return Response.json({ runId, state });
|
|
197
|
+
}
|
|
198
|
+
return new Response('Not Found', { status: 404 });
|
|
199
|
+
}
|
|
200
|
+
catch (rawErr) {
|
|
201
|
+
const err = rawErr instanceof Error ? rawErr : new Error(String(rawErr));
|
|
202
|
+
return toErrorResponse(err);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export function serveHttp(fetchHandler, options) {
|
|
207
|
+
const port = options?.port ?? 8787;
|
|
208
|
+
const hostname = options?.hostname ?? '0.0.0.0';
|
|
209
|
+
const server = Bun.serve({
|
|
210
|
+
port,
|
|
211
|
+
hostname,
|
|
212
|
+
idleTimeout: 0,
|
|
213
|
+
async fetch(req, srv) {
|
|
214
|
+
srv.timeout(req, 0);
|
|
215
|
+
const result = await fetchHandler(req);
|
|
216
|
+
if (result === null) {
|
|
217
|
+
return new Response('Not Found', { status: 404 });
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
port: server.port ?? port,
|
|
224
|
+
stop: () => server.stop(true),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type EventStore } from '@swirls/looms/core';
|
|
2
|
+
export interface EventStreamOptions {
|
|
3
|
+
signal: AbortSignal;
|
|
4
|
+
store: EventStore;
|
|
5
|
+
runId: string;
|
|
6
|
+
fromSeq?: number;
|
|
7
|
+
heartbeatMs?: number;
|
|
8
|
+
onStart?: (write: (text: string) => void, close: () => void) => Promise<void> | void;
|
|
9
|
+
request?: Request;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates an SSE Response backed by an EventStore subscription.
|
|
13
|
+
* Handles heartbeats, fiber cancellation on disconnect, event encoding, and optional lifecycle hooks.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createEventStreamResponse(options: EventStreamOptions): Response;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Effect, Fiber, Stream } from 'effect';
|
|
2
|
+
import { encodeLoomsEvent } from '@swirls/looms/core';
|
|
3
|
+
function disableBunSocketTimeout(request) {
|
|
4
|
+
if (!request) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
// SAFETY: Bun.serve attaches runtime.bun.server to Request in srvx and Bun handlers.
|
|
9
|
+
const reqWithBun = request;
|
|
10
|
+
const bunServer = reqWithBun.runtime?.bun?.server;
|
|
11
|
+
if (bunServer) {
|
|
12
|
+
bunServer.timeout?.(request, 0);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Ignore environments where timeout is not supported.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates an SSE Response backed by an EventStore subscription.
|
|
21
|
+
* Handles heartbeats, fiber cancellation on disconnect, event encoding, and optional lifecycle hooks.
|
|
22
|
+
*/
|
|
23
|
+
export function createEventStreamResponse(options) {
|
|
24
|
+
const { signal, store, runId, fromSeq = 1, heartbeatMs = 3_000, onStart, request } = options;
|
|
25
|
+
disableBunSocketTimeout(request);
|
|
26
|
+
let closed = false;
|
|
27
|
+
let heartbeat;
|
|
28
|
+
let fiber;
|
|
29
|
+
let streamController;
|
|
30
|
+
const close = () => {
|
|
31
|
+
if (closed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
closed = true;
|
|
35
|
+
if (heartbeat !== undefined) {
|
|
36
|
+
clearInterval(heartbeat);
|
|
37
|
+
}
|
|
38
|
+
if (fiber) {
|
|
39
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
streamController?.close();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// already closed
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const stream = new ReadableStream({
|
|
49
|
+
start(controller) {
|
|
50
|
+
streamController = controller;
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
const write = (text) => {
|
|
53
|
+
if (closed) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
controller.enqueue(encoder.encode(text));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
close();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
fiber = Effect.runFork(store.subscribe(runId, { fromSeq }).pipe(Stream.tap((event) => Effect.sync(() => {
|
|
64
|
+
write(`id: ${event.seq}\ndata: ${JSON.stringify({ batch: [encodeLoomsEvent(event)] })}\n\n`);
|
|
65
|
+
})), Stream.runDrain));
|
|
66
|
+
// Send an immediate keepalive so bytes and headers flush over the wire immediately,
|
|
67
|
+
// preventing dev server proxies and Bun.serve from aborting quiet connections.
|
|
68
|
+
write(': keepalive\n\n');
|
|
69
|
+
heartbeat = setInterval(() => write(': keepalive\n\n'), heartbeatMs);
|
|
70
|
+
if (onStart) {
|
|
71
|
+
Promise.resolve(onStart(write, close)).catch((cause) => {
|
|
72
|
+
const error = cause instanceof Error ? cause.message : String(cause);
|
|
73
|
+
write(`event: error\ndata: ${JSON.stringify({ error })}\n\n`);
|
|
74
|
+
close();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (signal.aborted) {
|
|
78
|
+
close();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
signal.addEventListener('abort', close);
|
|
82
|
+
},
|
|
83
|
+
cancel() {
|
|
84
|
+
// When the consumer closes the stream or client disconnects, tear down resources immediately.
|
|
85
|
+
close();
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
return new Response(stream, {
|
|
89
|
+
headers: {
|
|
90
|
+
'content-type': 'text/event-stream',
|
|
91
|
+
'cache-control': 'no-cache, no-transform',
|
|
92
|
+
connection: 'keep-alive',
|
|
93
|
+
'x-accel-buffering': 'no',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
export interface WakeScheduler {
|
|
3
|
+
schedule(runId: string, at: number): void | Promise<void>;
|
|
4
|
+
cancel(runId: string): void | Promise<void>;
|
|
5
|
+
dispose?(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createTimeoutScheduler(wake: (runId: string) => Promise<void> | void): WakeScheduler;
|
|
8
|
+
export declare function toEffectVoid(result: void | Promise<void>): Effect.Effect<void>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
export function createTimeoutScheduler(wake) {
|
|
3
|
+
const scheduledTimers = new Map();
|
|
4
|
+
return {
|
|
5
|
+
schedule(runId, timerAt) {
|
|
6
|
+
const existing = scheduledTimers.get(runId);
|
|
7
|
+
if (existing) {
|
|
8
|
+
clearTimeout(existing);
|
|
9
|
+
scheduledTimers.delete(runId);
|
|
10
|
+
}
|
|
11
|
+
const delay = Math.max(0, timerAt - Date.now() + 5);
|
|
12
|
+
const handle = setTimeout(() => {
|
|
13
|
+
scheduledTimers.delete(runId);
|
|
14
|
+
void wake(runId);
|
|
15
|
+
}, delay);
|
|
16
|
+
scheduledTimers.set(runId, handle);
|
|
17
|
+
},
|
|
18
|
+
cancel(runId) {
|
|
19
|
+
const existing = scheduledTimers.get(runId);
|
|
20
|
+
if (existing) {
|
|
21
|
+
clearTimeout(existing);
|
|
22
|
+
scheduledTimers.delete(runId);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
dispose() {
|
|
26
|
+
for (const handle of scheduledTimers.values()) {
|
|
27
|
+
clearTimeout(handle);
|
|
28
|
+
}
|
|
29
|
+
scheduledTimers.clear();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function toEffectVoid(result) {
|
|
34
|
+
if (result instanceof Promise) {
|
|
35
|
+
return Effect.promise(() => result);
|
|
36
|
+
}
|
|
37
|
+
return Effect.void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for the S2-backed EventStore.
|
|
4
|
+
*
|
|
5
|
+
* `endpoint` is optional and typically used for local s2-lite:
|
|
6
|
+
* `{ account: "http://localhost:8080", basin: "http://localhost:8080" }`
|
|
7
|
+
*/
|
|
8
|
+
export declare const S2ConfigSchema: Schema.Struct<{
|
|
9
|
+
readonly basin: Schema.String;
|
|
10
|
+
readonly accessToken: Schema.String;
|
|
11
|
+
readonly endpoint: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Struct<{
|
|
12
|
+
readonly account: Schema.optional<Schema.String>;
|
|
13
|
+
readonly basin: Schema.optional<Schema.String>;
|
|
14
|
+
}>]>>;
|
|
15
|
+
readonly streamConfig: Schema.optional<Schema.Struct<{
|
|
16
|
+
readonly retentionPolicy: Schema.optional<Schema.Union<readonly [Schema.Struct<{
|
|
17
|
+
readonly ageSecs: Schema.Number;
|
|
18
|
+
}>, Schema.Struct<{
|
|
19
|
+
readonly infinite: Schema.Struct<{}>;
|
|
20
|
+
}>]>>;
|
|
21
|
+
readonly storageClass: Schema.optional<Schema.Literals<readonly ["standard", "express"]>>;
|
|
22
|
+
}>>;
|
|
23
|
+
readonly snapshotPrefix: Schema.optional<Schema.String>;
|
|
24
|
+
}>;
|
|
25
|
+
export type S2Config = Schema.Schema.Type<typeof S2ConfigSchema>;
|
|
26
|
+
export declare function streamNameForRun(runId: string): string;
|
|
27
|
+
export declare function s2ConfigFromEnv(env: Record<string, string | undefined>): S2Config;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for the S2-backed EventStore.
|
|
4
|
+
*
|
|
5
|
+
* `endpoint` is optional and typically used for local s2-lite:
|
|
6
|
+
* `{ account: "http://localhost:8080", basin: "http://localhost:8080" }`
|
|
7
|
+
*/
|
|
8
|
+
export const S2ConfigSchema = Schema.Struct({
|
|
9
|
+
basin: Schema.String,
|
|
10
|
+
accessToken: Schema.String,
|
|
11
|
+
endpoint: Schema.optional(Schema.Union([
|
|
12
|
+
Schema.String,
|
|
13
|
+
Schema.Struct({
|
|
14
|
+
account: Schema.optional(Schema.String),
|
|
15
|
+
basin: Schema.optional(Schema.String),
|
|
16
|
+
}),
|
|
17
|
+
])),
|
|
18
|
+
streamConfig: Schema.optional(Schema.Struct({
|
|
19
|
+
retentionPolicy: Schema.optional(Schema.Union([
|
|
20
|
+
Schema.Struct({ ageSecs: Schema.Number }),
|
|
21
|
+
Schema.Struct({ infinite: Schema.Struct({}) }),
|
|
22
|
+
])),
|
|
23
|
+
storageClass: Schema.optional(Schema.Literals(['standard', 'express'])),
|
|
24
|
+
})),
|
|
25
|
+
snapshotPrefix: Schema.optional(Schema.String),
|
|
26
|
+
});
|
|
27
|
+
export function streamNameForRun(runId) {
|
|
28
|
+
return `runs/${runId}`;
|
|
29
|
+
}
|
|
30
|
+
export function s2ConfigFromEnv(env) {
|
|
31
|
+
const basin = env.LOOMS_S2_BASIN ?? env.S2_BASIN ?? 'looms-demo';
|
|
32
|
+
const accessToken = env.LOOMS_S2_ACCESS_TOKEN ??
|
|
33
|
+
env.LOOMS_S2_AUTH_TOKEN ??
|
|
34
|
+
env.S2_ACCESS_TOKEN ??
|
|
35
|
+
env.S2_AUTH_TOKEN ??
|
|
36
|
+
's2_local';
|
|
37
|
+
const endpoint = env.LOOMS_S2_ENDPOINT ??
|
|
38
|
+
env.S2_ENDPOINT ??
|
|
39
|
+
((env.LOOMS_S2_PORT ?? env.S2_PORT ?? env.S2_LITE_PORT)
|
|
40
|
+
? `http://127.0.0.1:${env.LOOMS_S2_PORT ?? env.S2_PORT ?? env.S2_LITE_PORT}`
|
|
41
|
+
: undefined);
|
|
42
|
+
const config = {
|
|
43
|
+
basin,
|
|
44
|
+
accessToken,
|
|
45
|
+
};
|
|
46
|
+
if (endpoint !== undefined) {
|
|
47
|
+
return {
|
|
48
|
+
basin,
|
|
49
|
+
accessToken,
|
|
50
|
+
endpoint,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { S2ConfigSchema, s2ConfigFromEnv, streamNameForRun, type S2Config } from './config';
|
|
2
|
+
export { findS2Binary, isPortOpen, s2Lite, startS2Lite, type S2LiteOptions, type StartedS2Lite, } from './lite';
|
|
3
|
+
export { makeS2EventStore, paginateS2Stream, readAllPages, s2, S2EventStoreLive, type ReadAllPagesOptions, type S2ReadBatchLike, } from './store';
|
|
4
|
+
export { createKeyedSerializer, type KeyedSerializer } from './serialize';
|
|
5
|
+
export { s2Projector } from './projector';
|
|
6
|
+
export { assembleSnapshot, frameSnapshot, s2SnapshotStore, S2SnapshotStoreLive, type SnapshotFrame, } from './snapshot-store';
|
package/dist/s2/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { S2ConfigSchema, s2ConfigFromEnv, streamNameForRun } from './config.js';
|
|
2
|
+
export { findS2Binary, isPortOpen, s2Lite, startS2Lite, } from './lite.js';
|
|
3
|
+
export { makeS2EventStore, paginateS2Stream, readAllPages, s2, S2EventStoreLive, } from './store.js';
|
|
4
|
+
export { createKeyedSerializer } from './serialize.js';
|
|
5
|
+
export { s2Projector } from './projector.js';
|
|
6
|
+
export { assembleSnapshot, frameSnapshot, s2SnapshotStore, S2SnapshotStoreLive, } from './snapshot-store.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EventStore } from '@swirls/looms/core';
|
|
2
|
+
export interface S2LiteOptions {
|
|
3
|
+
/** Port for local s2-lite process. Defaults to 8080. */
|
|
4
|
+
readonly port?: number;
|
|
5
|
+
/** Basin name. Defaults to 'looms-demo'. */
|
|
6
|
+
readonly basin?: string;
|
|
7
|
+
/** Auth token for local access. Defaults to 's2_local'. */
|
|
8
|
+
readonly accessToken?: string;
|
|
9
|
+
/** Environment bag used to locate the s2 binary. */
|
|
10
|
+
readonly env?: Record<string, string | undefined>;
|
|
11
|
+
/** Explicit path to the s2 binary, skipping lookup. */
|
|
12
|
+
readonly binaryPath?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function findS2Binary(env: Record<string, string | undefined>): string | undefined;
|
|
15
|
+
export declare function isPortOpen(port: number, host?: string): Promise<boolean>;
|
|
16
|
+
export interface StartedS2Lite {
|
|
17
|
+
readonly port: number;
|
|
18
|
+
readonly endpoint: string;
|
|
19
|
+
readonly stop: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function startS2Lite(options?: S2LiteOptions): Promise<StartedS2Lite>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns a factory function for createLooms that automatically starts local s2-lite
|
|
24
|
+
* and connects an S2 EventStore to it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function s2Lite(options?: S2LiteOptions): () => Promise<EventStore>;
|
package/dist/s2/lite.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as childProcess from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as net from 'node:net';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { s2 } from './store.js';
|
|
7
|
+
export function findS2Binary(env) {
|
|
8
|
+
// 1. Check PATH
|
|
9
|
+
const pathDirs = (env.PATH ?? '').split(path.delimiter);
|
|
10
|
+
for (const dir of pathDirs) {
|
|
11
|
+
if (!dir) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const candidate = path.join(dir, 's2');
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore filesystem access errors during lookup.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// 2. Check FLOX_ENV_CACHE
|
|
25
|
+
if (env.FLOX_ENV_CACHE) {
|
|
26
|
+
const floxCandidate = path.join(env.FLOX_ENV_CACHE, 's2', 'bin', 's2');
|
|
27
|
+
if (fs.existsSync(floxCandidate)) {
|
|
28
|
+
return floxCandidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 3. Check S2_INSTALL_PREFIX
|
|
32
|
+
if (env.S2_INSTALL_PREFIX) {
|
|
33
|
+
const prefixCandidate = path.join(env.S2_INSTALL_PREFIX, 'bin', 's2');
|
|
34
|
+
if (fs.existsSync(prefixCandidate)) {
|
|
35
|
+
return prefixCandidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 4. Check ~/.s2/bin/s2
|
|
39
|
+
const homeCandidate = path.join(os.homedir(), '.s2', 'bin', 's2');
|
|
40
|
+
if (fs.existsSync(homeCandidate)) {
|
|
41
|
+
return homeCandidate;
|
|
42
|
+
}
|
|
43
|
+
// 5. Check local ./.s2/bin/s2
|
|
44
|
+
const localCandidate = path.join(process.cwd(), '.s2', 'bin', 's2');
|
|
45
|
+
if (fs.existsSync(localCandidate)) {
|
|
46
|
+
return localCandidate;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
export function isPortOpen(port, host = '127.0.0.1') {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const socket = new net.Socket();
|
|
53
|
+
socket.setTimeout(200);
|
|
54
|
+
socket.once('connect', () => {
|
|
55
|
+
socket.destroy();
|
|
56
|
+
resolve(true);
|
|
57
|
+
});
|
|
58
|
+
socket.once('timeout', () => {
|
|
59
|
+
socket.destroy();
|
|
60
|
+
resolve(false);
|
|
61
|
+
});
|
|
62
|
+
socket.once('error', () => {
|
|
63
|
+
socket.destroy();
|
|
64
|
+
resolve(false);
|
|
65
|
+
});
|
|
66
|
+
socket.connect(port, host);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
let activeProcess = null;
|
|
70
|
+
export async function startS2Lite(options = {}) {
|
|
71
|
+
const port = options.port ?? 8080;
|
|
72
|
+
const endpoint = `http://127.0.0.1:${port}`;
|
|
73
|
+
// 1. Check if an s2-lite instance is already responding on this port.
|
|
74
|
+
const alreadyRunning = await isPortOpen(port);
|
|
75
|
+
if (alreadyRunning) {
|
|
76
|
+
return {
|
|
77
|
+
port,
|
|
78
|
+
endpoint,
|
|
79
|
+
stop: () => {
|
|
80
|
+
// Did not spawn this process, no-op stop.
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// 2. Locate the s2 binary.
|
|
85
|
+
const binaryPath = options.binaryPath ?? findS2Binary(options.env ?? {});
|
|
86
|
+
if (!binaryPath) {
|
|
87
|
+
throw new Error(`s2 CLI binary not found on PATH or in standard locations.
|
|
88
|
+
To install s2-lite for local development:
|
|
89
|
+
flox activate # installs into $FLOX_ENV_CACHE/s2 (preferred)
|
|
90
|
+
bun run setup:s2 # scripts/install-s2-cli.sh
|
|
91
|
+
https://s2.dev/docs/cli/installation`);
|
|
92
|
+
}
|
|
93
|
+
// 3. Spawn s2 lite.
|
|
94
|
+
// SAFETY: child_process.spawn returns child process matching S2ChildProcess.
|
|
95
|
+
const child = childProcess.spawn(binaryPath, ['lite', '--port', String(port)], {
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
+
detached: false,
|
|
98
|
+
});
|
|
99
|
+
activeProcess = child;
|
|
100
|
+
const cleanup = () => {
|
|
101
|
+
if (activeProcess === child) {
|
|
102
|
+
activeProcess = null;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
child.kill('SIGTERM');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Process may already have exited.
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
process.once('exit', cleanup);
|
|
112
|
+
process.once('SIGINT', cleanup);
|
|
113
|
+
process.once('SIGTERM', cleanup);
|
|
114
|
+
let stderrOutput = '';
|
|
115
|
+
child.stderr?.on('data', (chunk) => {
|
|
116
|
+
stderrOutput += chunk.toString();
|
|
117
|
+
});
|
|
118
|
+
// 4. Poll until the port responds or the process exits unexpectedly.
|
|
119
|
+
const deadline = Date.now() + 5000;
|
|
120
|
+
while (Date.now() < deadline) {
|
|
121
|
+
if (child.exitCode !== null) {
|
|
122
|
+
throw new Error(`s2 lite process exited with code ${child.exitCode} before becoming ready: ${stderrOutput}`);
|
|
123
|
+
}
|
|
124
|
+
const ready = await isPortOpen(port);
|
|
125
|
+
if (ready) {
|
|
126
|
+
return {
|
|
127
|
+
port,
|
|
128
|
+
endpoint,
|
|
129
|
+
stop: cleanup,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
133
|
+
}
|
|
134
|
+
cleanup();
|
|
135
|
+
throw new Error(`Timed out waiting for s2 lite to start on port ${port}: ${stderrOutput}`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Returns a factory function for createLooms that automatically starts local s2-lite
|
|
139
|
+
* and connects an S2 EventStore to it.
|
|
140
|
+
*/
|
|
141
|
+
export function s2Lite(options = {}) {
|
|
142
|
+
return async () => {
|
|
143
|
+
const started = await startS2Lite(options);
|
|
144
|
+
return s2({
|
|
145
|
+
basin: options.basin ?? 'looms-demo',
|
|
146
|
+
accessToken: options.accessToken ?? 's2_local',
|
|
147
|
+
endpoint: started.endpoint,
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Projector } from '@swirls/looms/projectors';
|
|
2
|
+
import { S2ConfigSchema, s2ConfigFromEnv, streamNameForRun, type S2Config } from './config';
|
|
3
|
+
export { S2ConfigSchema, s2ConfigFromEnv, streamNameForRun, type S2Config };
|
|
4
|
+
/**
|
|
5
|
+
* Asynchronous S2 projector. The actor's local EventStore stays authoritative;
|
|
6
|
+
* this fans committed events out to `runs/{runId}` for the global stream lake.
|
|
7
|
+
*/
|
|
8
|
+
export declare function s2Projector(config: S2Config): Projector;
|