@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,328 @@
|
|
|
1
|
+
import { Effect, Option, Queue, Schema, Stream } from 'effect';
|
|
2
|
+
import { EventEnvelopeSchema, fromWireEvent, withAssignedSeq } from './envelope.js';
|
|
3
|
+
import { SnapshotStoreError, withSnapshotStore, } from './snapshot-store.js';
|
|
4
|
+
import { EventStoreConflictError, EventStoreError, EventStoreFencedError, } from './store.js';
|
|
5
|
+
export const SQLITE_EVENT_STORE_SCHEMA = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS looms_streams (
|
|
7
|
+
run_id TEXT PRIMARY KEY,
|
|
8
|
+
head INTEGER NOT NULL DEFAULT 1,
|
|
9
|
+
tail INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
fence_token TEXT
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS looms_events (
|
|
14
|
+
run_id TEXT NOT NULL,
|
|
15
|
+
seq INTEGER NOT NULL,
|
|
16
|
+
body TEXT NOT NULL,
|
|
17
|
+
PRIMARY KEY (run_id, seq)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS looms_snapshots (
|
|
21
|
+
run_id TEXT NOT NULL,
|
|
22
|
+
cursor INTEGER NOT NULL,
|
|
23
|
+
state_hash TEXT NOT NULL,
|
|
24
|
+
taken_at INTEGER NOT NULL,
|
|
25
|
+
state TEXT NOT NULL,
|
|
26
|
+
PRIMARY KEY (run_id, cursor)
|
|
27
|
+
);
|
|
28
|
+
`.trim();
|
|
29
|
+
const StreamRowSchema = Schema.Struct({
|
|
30
|
+
run_id: Schema.String,
|
|
31
|
+
head: Schema.Number,
|
|
32
|
+
tail: Schema.Number,
|
|
33
|
+
fence_token: Schema.NullOr(Schema.String),
|
|
34
|
+
});
|
|
35
|
+
const EventRowSchema = Schema.Struct({
|
|
36
|
+
body: Schema.String,
|
|
37
|
+
});
|
|
38
|
+
const SnapshotRowSchema = Schema.Struct({
|
|
39
|
+
run_id: Schema.String,
|
|
40
|
+
cursor: Schema.Number,
|
|
41
|
+
state_hash: Schema.String,
|
|
42
|
+
taken_at: Schema.Number,
|
|
43
|
+
state: Schema.String,
|
|
44
|
+
});
|
|
45
|
+
const RunIdRowSchema = Schema.Struct({
|
|
46
|
+
run_id: Schema.String,
|
|
47
|
+
});
|
|
48
|
+
const CursorRowSchema = Schema.Struct({
|
|
49
|
+
cursor: Schema.Number,
|
|
50
|
+
});
|
|
51
|
+
const decodeEventBody = Schema.decodeUnknownSync(Schema.fromJsonString(EventEnvelopeSchema));
|
|
52
|
+
const decodeRunState = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
|
|
53
|
+
const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
|
|
54
|
+
const decodeStreamRow = Schema.decodeUnknownSync(StreamRowSchema);
|
|
55
|
+
const decodeEventRow = Schema.decodeUnknownSync(EventRowSchema);
|
|
56
|
+
const decodeSnapshotRow = Schema.decodeUnknownSync(SnapshotRowSchema);
|
|
57
|
+
const decodeRunIdRow = Schema.decodeUnknownSync(RunIdRowSchema);
|
|
58
|
+
const decodeCursorRow = Schema.decodeUnknownSync(CursorRowSchema);
|
|
59
|
+
/** Adapter for Cloudflare Durable Object `ctx.storage.sql`. */
|
|
60
|
+
export function sqlStorageExec(sql, options) {
|
|
61
|
+
return {
|
|
62
|
+
run(sqlText, params = []) {
|
|
63
|
+
void sql.exec(sqlText, ...params);
|
|
64
|
+
},
|
|
65
|
+
rows(sqlText, params = []) {
|
|
66
|
+
return [...sql.exec(sqlText, ...params)];
|
|
67
|
+
},
|
|
68
|
+
transaction: options?.transaction,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function runInTransaction(exec, fn) {
|
|
72
|
+
if (exec.transaction) {
|
|
73
|
+
return exec.transaction(fn);
|
|
74
|
+
}
|
|
75
|
+
exec.run('BEGIN IMMEDIATE');
|
|
76
|
+
try {
|
|
77
|
+
const result = fn();
|
|
78
|
+
exec.run('COMMIT');
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
try {
|
|
83
|
+
exec.run('ROLLBACK');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Ignore rollback failures so the original error surfaces.
|
|
87
|
+
}
|
|
88
|
+
throw cause;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function initSchema(exec) {
|
|
92
|
+
for (const statement of SQLITE_EVENT_STORE_SCHEMA.split(';')) {
|
|
93
|
+
const sql = statement.trim();
|
|
94
|
+
if (sql.length > 0) {
|
|
95
|
+
exec.run(sql);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function parseEvent(body) {
|
|
100
|
+
return fromWireEvent(decodeEventBody(body));
|
|
101
|
+
}
|
|
102
|
+
function toSnapshot(row) {
|
|
103
|
+
// SAFETY: snapshot state is a JSON-encoded RunState written by this store.
|
|
104
|
+
const state = decodeRunState(row.state);
|
|
105
|
+
return {
|
|
106
|
+
runId: row.run_id,
|
|
107
|
+
cursor: row.cursor,
|
|
108
|
+
stateHash: row.state_hash,
|
|
109
|
+
takenAt: row.taken_at,
|
|
110
|
+
state,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function createSqliteSnapshotStore(exec) {
|
|
114
|
+
return {
|
|
115
|
+
loadLatest: (runId) => Effect.try({
|
|
116
|
+
try: () => {
|
|
117
|
+
const [row] = exec.rows('SELECT run_id, cursor, state_hash, taken_at, state FROM looms_snapshots WHERE run_id = ? ORDER BY cursor DESC LIMIT 1', [runId]);
|
|
118
|
+
return row === undefined ? Option.none() : Option.some(toSnapshot(decodeSnapshotRow(row)));
|
|
119
|
+
},
|
|
120
|
+
catch: (cause) => new SnapshotStoreError(`Failed to load snapshot for ${runId}`, cause),
|
|
121
|
+
}),
|
|
122
|
+
save: (snapshot) => Effect.try({
|
|
123
|
+
try: () => {
|
|
124
|
+
exec.run(`INSERT INTO looms_snapshots (run_id, cursor, state_hash, taken_at, state)
|
|
125
|
+
VALUES (?, ?, ?, ?, ?)
|
|
126
|
+
ON CONFLICT (run_id, cursor) DO UPDATE SET
|
|
127
|
+
state_hash = excluded.state_hash,
|
|
128
|
+
taken_at = excluded.taken_at,
|
|
129
|
+
state = excluded.state`, [
|
|
130
|
+
snapshot.runId,
|
|
131
|
+
snapshot.cursor,
|
|
132
|
+
snapshot.stateHash,
|
|
133
|
+
snapshot.takenAt,
|
|
134
|
+
encodeJson(snapshot.state),
|
|
135
|
+
]);
|
|
136
|
+
},
|
|
137
|
+
catch: (cause) => new SnapshotStoreError(`Failed to save snapshot for ${snapshot.runId}`, cause),
|
|
138
|
+
}),
|
|
139
|
+
listCursors: (runId) => Effect.try({
|
|
140
|
+
try: () => exec
|
|
141
|
+
.rows('SELECT cursor FROM looms_snapshots WHERE run_id = ? ORDER BY cursor ASC', [
|
|
142
|
+
runId,
|
|
143
|
+
])
|
|
144
|
+
.map((row) => decodeCursorRow(row).cursor),
|
|
145
|
+
catch: (cause) => new SnapshotStoreError(`Failed to list snapshots for ${runId}`, cause),
|
|
146
|
+
}),
|
|
147
|
+
prune: (runId, keepLatest) => Effect.try({
|
|
148
|
+
try: () => {
|
|
149
|
+
const cursors = exec
|
|
150
|
+
.rows('SELECT cursor FROM looms_snapshots WHERE run_id = ? ORDER BY cursor DESC', [
|
|
151
|
+
runId,
|
|
152
|
+
])
|
|
153
|
+
.map((row) => decodeCursorRow(row).cursor);
|
|
154
|
+
const drop = cursors.slice(Math.max(0, keepLatest));
|
|
155
|
+
for (const cursor of drop) {
|
|
156
|
+
exec.run('DELETE FROM looms_snapshots WHERE run_id = ? AND cursor = ?', [runId, cursor]);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
catch: (cause) => new SnapshotStoreError(`Failed to prune snapshots for ${runId}`, cause),
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function createSqliteEventStore(exec) {
|
|
164
|
+
const waiters = new Map();
|
|
165
|
+
const getStream = (runId) => {
|
|
166
|
+
const [existingRow] = exec.rows('SELECT run_id, head, tail, fence_token FROM looms_streams WHERE run_id = ?', [runId]);
|
|
167
|
+
const existing = existingRow === undefined ? undefined : decodeStreamRow(existingRow);
|
|
168
|
+
if (existing) {
|
|
169
|
+
return existing;
|
|
170
|
+
}
|
|
171
|
+
return { run_id: runId, head: 1, tail: 0, fence_token: null };
|
|
172
|
+
};
|
|
173
|
+
const ensureStream = (runId) => {
|
|
174
|
+
const existing = getStream(runId);
|
|
175
|
+
const [listedRow] = exec.rows('SELECT run_id FROM looms_streams WHERE run_id = ?', [runId]);
|
|
176
|
+
const listed = listedRow === undefined ? undefined : decodeRunIdRow(listedRow);
|
|
177
|
+
if (existing.tail > 0 || listed) {
|
|
178
|
+
return existing;
|
|
179
|
+
}
|
|
180
|
+
exec.run('INSERT OR IGNORE INTO looms_streams (run_id, head, tail) VALUES (?, 1, 0)', [runId]);
|
|
181
|
+
return getStream(runId);
|
|
182
|
+
};
|
|
183
|
+
const notify = (runId, events) => {
|
|
184
|
+
const listeners = waiters.get(runId);
|
|
185
|
+
if (!listeners) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const event of events) {
|
|
189
|
+
for (const waiter of listeners) {
|
|
190
|
+
waiter(event);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const service = {
|
|
195
|
+
append: (runId, events, options) => Effect.try({
|
|
196
|
+
try: () => {
|
|
197
|
+
if (events.length === 0 && options?.fence === undefined) {
|
|
198
|
+
return { sequences: [], tail: getStream(runId).tail };
|
|
199
|
+
}
|
|
200
|
+
return runInTransaction(exec, () => {
|
|
201
|
+
const log = ensureStream(runId);
|
|
202
|
+
if (options?.expectedTail !== undefined && log.tail !== options.expectedTail) {
|
|
203
|
+
throw new EventStoreConflictError(runId, options.expectedTail, log.tail);
|
|
204
|
+
}
|
|
205
|
+
if (options?.fencingToken !== undefined &&
|
|
206
|
+
(log.fence_token ?? undefined) !== options.fencingToken) {
|
|
207
|
+
throw new EventStoreFencedError(runId, options.fencingToken);
|
|
208
|
+
}
|
|
209
|
+
let nextTail = log.tail;
|
|
210
|
+
let fenceToken = log.fence_token;
|
|
211
|
+
if (options?.fence !== undefined) {
|
|
212
|
+
nextTail += 1;
|
|
213
|
+
fenceToken = options.fence === '' ? null : options.fence;
|
|
214
|
+
}
|
|
215
|
+
const sequences = [];
|
|
216
|
+
const appended = [];
|
|
217
|
+
for (const partial of events) {
|
|
218
|
+
const seq = nextTail + appended.length + 1;
|
|
219
|
+
const event = withAssignedSeq(partial, runId, seq);
|
|
220
|
+
appended.push(event);
|
|
221
|
+
sequences.push(seq);
|
|
222
|
+
exec.run('INSERT INTO looms_events (run_id, seq, body) VALUES (?, ?, ?)', [
|
|
223
|
+
runId,
|
|
224
|
+
seq,
|
|
225
|
+
encodeJson(event),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
nextTail += appended.length;
|
|
229
|
+
exec.run(`INSERT INTO looms_streams (run_id, head, tail, fence_token)
|
|
230
|
+
VALUES (?, ?, ?, ?)
|
|
231
|
+
ON CONFLICT (run_id) DO UPDATE SET
|
|
232
|
+
tail = excluded.tail,
|
|
233
|
+
fence_token = excluded.fence_token`, [runId, log.head, nextTail, fenceToken]);
|
|
234
|
+
notify(runId, appended);
|
|
235
|
+
return { sequences, tail: nextTail };
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
catch: (cause) => cause instanceof EventStoreConflictError ||
|
|
239
|
+
cause instanceof EventStoreFencedError ||
|
|
240
|
+
cause instanceof EventStoreError
|
|
241
|
+
? cause
|
|
242
|
+
: new EventStoreError(`append failed for ${runId}`, cause),
|
|
243
|
+
}),
|
|
244
|
+
read: (runId, options) => Effect.try({
|
|
245
|
+
try: () => {
|
|
246
|
+
const fromSeq = options?.fromSeq ?? 1;
|
|
247
|
+
const limit = options?.limit;
|
|
248
|
+
const sql = limit === undefined
|
|
249
|
+
? 'SELECT body FROM looms_events WHERE run_id = ? AND seq >= ? ORDER BY seq ASC'
|
|
250
|
+
: 'SELECT body FROM looms_events WHERE run_id = ? AND seq >= ? ORDER BY seq ASC LIMIT ?';
|
|
251
|
+
const params = limit === undefined ? [runId, fromSeq] : [runId, fromSeq, limit];
|
|
252
|
+
return exec.rows(sql, params).map((row) => parseEvent(decodeEventRow(row).body));
|
|
253
|
+
},
|
|
254
|
+
catch: (cause) => new EventStoreError(`read failed for ${runId}`, cause),
|
|
255
|
+
}),
|
|
256
|
+
readStream: (runId, options) => Stream.fromIterableEffect(service.read(runId, options)),
|
|
257
|
+
tail: (runId) => Effect.try({
|
|
258
|
+
try: () => getStream(runId).tail,
|
|
259
|
+
catch: (cause) => new EventStoreError(`tail failed for ${runId}`, cause),
|
|
260
|
+
}),
|
|
261
|
+
bounds: (runId) => Effect.try({
|
|
262
|
+
try: () => {
|
|
263
|
+
const log = getStream(runId);
|
|
264
|
+
return { head: log.head, tail: log.tail };
|
|
265
|
+
},
|
|
266
|
+
catch: (cause) => new EventStoreError(`bounds failed for ${runId}`, cause),
|
|
267
|
+
}),
|
|
268
|
+
trim: (runId, beforeSeq) => Effect.try({
|
|
269
|
+
try: () => {
|
|
270
|
+
exec.run('DELETE FROM looms_events WHERE run_id = ? AND seq < ?', [runId, beforeSeq]);
|
|
271
|
+
exec.run('UPDATE looms_streams SET head = MAX(head, ?) WHERE run_id = ?', [
|
|
272
|
+
beforeSeq,
|
|
273
|
+
runId,
|
|
274
|
+
]);
|
|
275
|
+
},
|
|
276
|
+
catch: (cause) => new EventStoreError(`trim failed for ${runId}`, cause),
|
|
277
|
+
}),
|
|
278
|
+
subscribe: (runId, options) => Stream.callback((queue) => Effect.gen(function* () {
|
|
279
|
+
const fromSeq = options?.fromSeq ?? 1;
|
|
280
|
+
const existing = exec
|
|
281
|
+
.rows('SELECT body FROM looms_events WHERE run_id = ? AND seq >= ? ORDER BY seq ASC', [
|
|
282
|
+
runId,
|
|
283
|
+
fromSeq,
|
|
284
|
+
])
|
|
285
|
+
.map((row) => parseEvent(decodeEventRow(row).body));
|
|
286
|
+
for (const event of existing) {
|
|
287
|
+
Queue.offerUnsafe(queue, event);
|
|
288
|
+
}
|
|
289
|
+
const waiter = (event) => {
|
|
290
|
+
if (event.seq >= fromSeq) {
|
|
291
|
+
Queue.offerUnsafe(queue, event);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const listeners = waiters.get(runId) ?? [];
|
|
295
|
+
listeners.push(waiter);
|
|
296
|
+
waiters.set(runId, listeners);
|
|
297
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
298
|
+
const current = waiters.get(runId);
|
|
299
|
+
if (!current) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const idx = current.indexOf(waiter);
|
|
303
|
+
if (idx >= 0) {
|
|
304
|
+
current.splice(idx, 1);
|
|
305
|
+
}
|
|
306
|
+
if (current.length === 0) {
|
|
307
|
+
waiters.delete(runId);
|
|
308
|
+
}
|
|
309
|
+
}));
|
|
310
|
+
})),
|
|
311
|
+
listRuns: () => Effect.try({
|
|
312
|
+
try: () => exec
|
|
313
|
+
.rows('SELECT run_id FROM looms_streams ORDER BY run_id ASC')
|
|
314
|
+
.map((row) => decodeRunIdRow(row).run_id),
|
|
315
|
+
catch: (cause) => new EventStoreError('listRuns failed', cause),
|
|
316
|
+
}),
|
|
317
|
+
};
|
|
318
|
+
return service;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Embedded SQLite EventStore for actor-local execution.
|
|
322
|
+
* Snapshots live in the same database and are attached automatically.
|
|
323
|
+
*/
|
|
324
|
+
export function sqliteEventStore(options) {
|
|
325
|
+
const exec = options.exec;
|
|
326
|
+
initSchema(exec);
|
|
327
|
+
return withSnapshotStore(createSqliteEventStore(exec), createSqliteSnapshotStore(exec));
|
|
328
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RuntimeEffect, WaitCondition } from './effects';
|
|
2
|
+
import type { JsonValue, ThreadStatus, RunStatus } from './types';
|
|
3
|
+
export interface ThreadRecord {
|
|
4
|
+
threadId: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
definitionName: string;
|
|
7
|
+
parentThreadId: string | null;
|
|
8
|
+
status: ThreadStatus;
|
|
9
|
+
input: JsonValue;
|
|
10
|
+
output: JsonValue | null;
|
|
11
|
+
error: string | null;
|
|
12
|
+
state: JsonValue;
|
|
13
|
+
}
|
|
14
|
+
export interface WaitRecord {
|
|
15
|
+
waitId: string;
|
|
16
|
+
threadId: string;
|
|
17
|
+
on: WaitCondition;
|
|
18
|
+
tag?: JsonValue;
|
|
19
|
+
}
|
|
20
|
+
export interface OutstandingEffect {
|
|
21
|
+
effectId: string;
|
|
22
|
+
threadId: string;
|
|
23
|
+
causingSeq: number;
|
|
24
|
+
causingEventId: string;
|
|
25
|
+
effect: RuntimeEffect;
|
|
26
|
+
}
|
|
27
|
+
export interface RunState {
|
|
28
|
+
runId: string;
|
|
29
|
+
status: RunStatus;
|
|
30
|
+
rootThreadId: string | null;
|
|
31
|
+
threads: {
|
|
32
|
+
[threadId: string]: ThreadRecord;
|
|
33
|
+
};
|
|
34
|
+
waits: {
|
|
35
|
+
[waitId: string]: WaitRecord;
|
|
36
|
+
};
|
|
37
|
+
outstandingEffects: OutstandingEffect[];
|
|
38
|
+
completedEffectIds?: string[];
|
|
39
|
+
processedIdempotencyKeys?: string[];
|
|
40
|
+
}
|
|
41
|
+
export declare function emptyRunState(runId: string): RunState;
|
|
42
|
+
export declare function isRunTerminal(state: RunState): boolean;
|
|
43
|
+
export declare function isRunParked(state: RunState): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function emptyRunState(runId) {
|
|
2
|
+
return {
|
|
3
|
+
runId,
|
|
4
|
+
status: 'running',
|
|
5
|
+
rootThreadId: null,
|
|
6
|
+
threads: {},
|
|
7
|
+
waits: {},
|
|
8
|
+
outstandingEffects: [],
|
|
9
|
+
completedEffectIds: [],
|
|
10
|
+
processedIdempotencyKeys: [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function isRunTerminal(state) {
|
|
14
|
+
return state.status === 'completed' || state.status === 'failed' || state.status === 'cancelled';
|
|
15
|
+
}
|
|
16
|
+
export function isRunParked(state) {
|
|
17
|
+
if (isRunTerminal(state)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (state.outstandingEffects.length > 0) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const records = Object.values(state.threads);
|
|
24
|
+
if (records.length === 0) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return records.every((record) => record.status === 'waiting' ||
|
|
28
|
+
record.status === 'completed' ||
|
|
29
|
+
record.status === 'failed' ||
|
|
30
|
+
record.status === 'cancelled');
|
|
31
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Stream } from 'effect';
|
|
2
|
+
import { type AppendableEvent, type EventEnvelope } from './envelope';
|
|
3
|
+
declare const EventStoreError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
4
|
+
readonly _tag: "EventStoreError";
|
|
5
|
+
} & Readonly<A>;
|
|
6
|
+
export declare class EventStoreError extends EventStoreError_base<{
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly cause?: unknown;
|
|
9
|
+
readonly conflict?: boolean;
|
|
10
|
+
}> {
|
|
11
|
+
readonly conflict?: boolean;
|
|
12
|
+
readonly cause?: unknown;
|
|
13
|
+
constructor(text: string, cause?: unknown, options?: {
|
|
14
|
+
conflict?: boolean;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
declare const EventStoreConflictError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
18
|
+
readonly _tag: "EventStoreConflictError";
|
|
19
|
+
} & Readonly<A>;
|
|
20
|
+
export declare class EventStoreConflictError extends EventStoreConflictError_base<{
|
|
21
|
+
readonly runId: string;
|
|
22
|
+
readonly expectedTail: number;
|
|
23
|
+
readonly actualTail: number;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
readonly cause?: unknown;
|
|
26
|
+
}> {
|
|
27
|
+
readonly conflict = true;
|
|
28
|
+
constructor(runId: string, expectedTail: number, actualTail: number, cause?: unknown);
|
|
29
|
+
}
|
|
30
|
+
declare const EventStoreTruncationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
31
|
+
readonly _tag: "EventStoreTruncationError";
|
|
32
|
+
} & Readonly<A>;
|
|
33
|
+
export declare class EventStoreTruncationError extends EventStoreTruncationError_base<{
|
|
34
|
+
readonly runId: string;
|
|
35
|
+
readonly readCount: number;
|
|
36
|
+
readonly tail: number;
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}> {
|
|
39
|
+
constructor(runId: string, readCount: number, tail: number);
|
|
40
|
+
}
|
|
41
|
+
declare const EventStoreTrimmedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
42
|
+
readonly _tag: "EventStoreTrimmedError";
|
|
43
|
+
} & Readonly<A>;
|
|
44
|
+
export declare class EventStoreTrimmedError extends EventStoreTrimmedError_base<{
|
|
45
|
+
readonly runId: string;
|
|
46
|
+
readonly head: number;
|
|
47
|
+
readonly requestedSeq: number;
|
|
48
|
+
readonly message: string;
|
|
49
|
+
}> {
|
|
50
|
+
constructor(runId: string, head: number, requestedSeq: number);
|
|
51
|
+
}
|
|
52
|
+
declare const EventStoreFencedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
53
|
+
readonly _tag: "EventStoreFencedError";
|
|
54
|
+
} & Readonly<A>;
|
|
55
|
+
export declare class EventStoreFencedError extends EventStoreFencedError_base<{
|
|
56
|
+
readonly runId: string;
|
|
57
|
+
readonly token: string;
|
|
58
|
+
readonly message: string;
|
|
59
|
+
}> {
|
|
60
|
+
constructor(runId: string, token: string);
|
|
61
|
+
}
|
|
62
|
+
export type EventStoreAppendError = EventStoreError | EventStoreConflictError | EventStoreFencedError;
|
|
63
|
+
export type AnyEventStoreError = EventStoreError | EventStoreConflictError | EventStoreTruncationError | EventStoreTrimmedError | EventStoreFencedError;
|
|
64
|
+
export declare function isEventStoreError(err: unknown): err is AnyEventStoreError;
|
|
65
|
+
export interface StreamBounds {
|
|
66
|
+
readonly head: number;
|
|
67
|
+
readonly tail: number;
|
|
68
|
+
}
|
|
69
|
+
export interface AppendOptions {
|
|
70
|
+
readonly expectedTail?: number;
|
|
71
|
+
/** Set the stream fencing token atomically in this batch (`''` clears). */
|
|
72
|
+
readonly fence?: string;
|
|
73
|
+
/** Must match the stream's current fencing token, or the append fails. */
|
|
74
|
+
readonly fencingToken?: string;
|
|
75
|
+
}
|
|
76
|
+
export interface PaginatePagesOptions<SCursor> {
|
|
77
|
+
readonly initialCursor: SCursor;
|
|
78
|
+
readonly limit?: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Pure Effect-native stream pagination primitive.
|
|
82
|
+
*
|
|
83
|
+
* Lazily pulls pages using `fetchPage` until:
|
|
84
|
+
* 1. `fetchPage` returns null or an empty record list,
|
|
85
|
+
* 2. `nextCursor` is null/undefined (signaling stream exhaustion / tail reached),
|
|
86
|
+
* 3. Or the caller's optional `limit` is satisfied (avoiding subsequent page fetches).
|
|
87
|
+
*/
|
|
88
|
+
export declare function paginatePages<T, SCursor, E = never, R = never>(options: PaginatePagesOptions<SCursor>, fetchPage: (cursor: SCursor) => Effect.Effect<{
|
|
89
|
+
readonly records: ReadonlyArray<T>;
|
|
90
|
+
readonly nextCursor?: SCursor | null;
|
|
91
|
+
} | null, E, R>): Stream.Stream<T, E, R>;
|
|
92
|
+
export interface AppendResult {
|
|
93
|
+
sequences: number[];
|
|
94
|
+
tail: number;
|
|
95
|
+
}
|
|
96
|
+
export interface EventStore {
|
|
97
|
+
readonly append: (runId: string, events: ReadonlyArray<AppendableEvent>, options?: AppendOptions) => Effect.Effect<AppendResult, EventStoreAppendError>;
|
|
98
|
+
/**
|
|
99
|
+
* Reads events for a run in sequence order.
|
|
100
|
+
*
|
|
101
|
+
* Contract:
|
|
102
|
+
* - If `limit` is omitted, returns every event from `fromSeq` (default 1) through
|
|
103
|
+
* the current stream tail. Implementations must page internally if the backing
|
|
104
|
+
* storage limits unary reads (e.g. S2 1000-record / 1 MiB limits).
|
|
105
|
+
* - If `limit` is provided, returns at most `limit` events starting from `fromSeq`.
|
|
106
|
+
* Callers using `limit` must page with their own cursor.
|
|
107
|
+
* - If the stream does not exist or has no events at or beyond `fromSeq`, returns an empty array.
|
|
108
|
+
*/
|
|
109
|
+
readonly read: (runId: string, options?: {
|
|
110
|
+
fromSeq?: number;
|
|
111
|
+
limit?: number;
|
|
112
|
+
}) => Effect.Effect<EventEnvelope[], EventStoreError>;
|
|
113
|
+
/**
|
|
114
|
+
* Reads events for a run as an interruptible stream in sequence order.
|
|
115
|
+
*/
|
|
116
|
+
readonly readStream?: (runId: string, options?: {
|
|
117
|
+
fromSeq?: number;
|
|
118
|
+
limit?: number;
|
|
119
|
+
}) => Stream.Stream<EventEnvelope, EventStoreError>;
|
|
120
|
+
readonly tail: (runId: string) => Effect.Effect<number, EventStoreError>;
|
|
121
|
+
/**
|
|
122
|
+
* Sequence bounds of the retained log.
|
|
123
|
+
* `head` is the seq of the first retained record (1 when nothing trimmed, `tail + 1` when empty).
|
|
124
|
+
*/
|
|
125
|
+
readonly bounds?: (runId: string) => Effect.Effect<StreamBounds, EventStoreError>;
|
|
126
|
+
/** Discard records with `seq < beforeSeq`. */
|
|
127
|
+
readonly trim?: (runId: string, beforeSeq: number) => Effect.Effect<void, EventStoreError>;
|
|
128
|
+
readonly subscribe: (runId: string, options?: {
|
|
129
|
+
fromSeq?: number;
|
|
130
|
+
}) => Stream.Stream<EventEnvelope, EventStoreError>;
|
|
131
|
+
readonly listRuns: () => Effect.Effect<string[], EventStoreError>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Utility to read an EventStore as a Stream.Stream, using `store.readStream` if implemented
|
|
135
|
+
* or falling back to `Stream.fromIterableEffect(store.read(...))`.
|
|
136
|
+
*/
|
|
137
|
+
export declare function readEventStream(store: EventStore, runId: string, options?: {
|
|
138
|
+
fromSeq?: number;
|
|
139
|
+
limit?: number;
|
|
140
|
+
}): Stream.Stream<EventEnvelope, EventStoreError>;
|
|
141
|
+
declare const EventStoreTag_base: Context.ServiceClass<EventStoreTag, "looms/EventStore", EventStore>;
|
|
142
|
+
export declare class EventStoreTag extends EventStoreTag_base {
|
|
143
|
+
}
|
|
144
|
+
export declare const makeMemoryEventStore: Effect.Effect<EventStore, never, never>;
|
|
145
|
+
export declare const MemoryEventStoreLive: Layer.Layer<EventStoreTag, never, never>;
|
|
146
|
+
export {};
|