@swirls/looms 0.4.0 → 0.5.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/CHANGELOG.md +61 -0
- package/dist/actor/cell.d.ts +21 -2
- package/dist/actor/cell.js +15 -3
- package/dist/actor/host.d.ts +10 -2
- package/dist/actor/host.js +27 -4
- package/dist/actor/index.d.ts +2 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +3 -0
- package/dist/actor/routing.js +13 -2
- package/dist/agent/definitions-store.d.ts +1 -1
- package/dist/agent/definitions-store.js +1 -1
- package/dist/agent/definitions.d.ts +48 -0
- package/dist/agent/definitions.js +44 -0
- package/dist/agent/effects.d.ts +184 -0
- package/dist/agent/effects.js +172 -40
- package/dist/agent/events.d.ts +89 -0
- package/dist/agent/events.js +25 -0
- package/dist/agent/module.d.ts +412 -0
- package/dist/agent/module.js +5 -1
- package/dist/agent/scope.d.ts +88 -0
- package/dist/agent/threads.js +39 -3
- package/dist/agent/types.d.ts +4 -0
- package/dist/approval/effects.d.ts +40 -0
- package/dist/approval/module.d.ts +80 -0
- package/dist/client/client.d.ts +468 -119
- package/dist/client/client.js +214 -21
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sse.d.ts +1 -1
- package/dist/client/sse.js +2 -2
- package/dist/cloudflare/durable-object.d.ts +66 -2
- package/dist/cloudflare/durable-object.js +96 -1
- package/dist/cloudflare/event-store.d.ts +7 -3
- package/dist/cloudflare/event-store.js +16 -3
- package/dist/cloudflare/index.d.ts +7 -1
- package/dist/cloudflare/index.js +7 -1
- package/dist/cloudflare/memory-queue.d.ts +18 -0
- package/dist/cloudflare/memory-queue.js +53 -0
- package/dist/cloudflare/queue-consumer.d.ts +52 -0
- package/dist/cloudflare/queue-consumer.js +102 -0
- package/dist/cloudflare/queue-message.d.ts +31 -0
- package/dist/cloudflare/queue-message.js +33 -0
- package/dist/cloudflare/queue-pull.d.ts +62 -0
- package/dist/cloudflare/queue-pull.js +130 -0
- package/dist/cloudflare/queue-worker.d.ts +39 -0
- package/dist/cloudflare/queue-worker.js +62 -0
- package/dist/cloudflare/queues.d.ts +10 -0
- package/dist/cloudflare/queues.js +10 -0
- package/dist/cloudflare/r2-blob-store.d.ts +33 -0
- package/dist/cloudflare/r2-blob-store.js +60 -0
- package/dist/cloudflare/router.d.ts +4 -1
- package/dist/cloudflare/router.js +8 -3
- package/dist/cloudflare/websocket.d.ts +27 -0
- package/dist/cloudflare/websocket.js +215 -0
- package/dist/core/blob-offload.d.ts +21 -0
- package/dist/core/blob-offload.js +143 -0
- package/dist/core/blob-store.d.ts +45 -0
- package/dist/core/blob-store.js +164 -0
- package/dist/core/blobs.d.ts +37 -0
- package/dist/core/blobs.js +160 -0
- package/dist/core/definition-resolver.d.ts +40 -0
- package/dist/core/definition-resolver.js +129 -0
- package/dist/core/definition-store.d.ts +6 -0
- package/dist/core/definition-store.js +37 -1
- package/dist/core/effects.d.ts +86 -3
- package/dist/core/effects.js +99 -3
- package/dist/core/envelope.d.ts +9 -0
- package/dist/core/envelope.js +4 -0
- package/dist/core/event-codec.js +3 -0
- package/dist/core/fold.js +83 -1
- package/dist/core/ids.d.ts +2 -0
- package/dist/core/ids.js +4 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +8 -0
- package/dist/core/launcher.d.ts +97 -0
- package/dist/core/launcher.js +67 -0
- package/dist/core/module-scope.d.ts +7 -3
- package/dist/core/module-scope.js +3 -0
- package/dist/core/module.d.ts +10 -0
- package/dist/core/projection.js +4 -0
- package/dist/core/protocol.d.ts +36 -1
- package/dist/core/protocol.js +35 -0
- package/dist/core/published.d.ts +2 -0
- package/dist/core/published.js +4 -1
- package/dist/core/replay.d.ts +96 -0
- package/dist/core/run-context.d.ts +33 -0
- package/dist/core/run-context.js +23 -0
- package/dist/core/sqlite-store.js +57 -5
- package/dist/core/state.d.ts +60 -0
- package/dist/core/state.js +19 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +7 -0
- package/dist/core/subscription.d.ts +97 -0
- package/dist/core/subscription.js +224 -0
- package/dist/core/thread.d.ts +7 -0
- package/dist/core/thread.js +1 -0
- package/dist/core/trace.d.ts +21 -0
- package/dist/core/trace.js +45 -0
- package/dist/core/worker-callback.d.ts +25 -1
- package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
- package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
- package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
- package/dist/debugger/app/index.html +2 -2
- package/dist/debugger/components/event-inspector.d.ts +3 -1
- package/dist/debugger/components/event-inspector.js +25 -4
- package/dist/debugger/components/event-stream.d.ts +4 -1
- package/dist/debugger/components/event-stream.js +2 -2
- package/dist/debugger/components/run-tree.d.ts +5 -2
- package/dist/debugger/components/run-tree.js +10 -4
- package/dist/debugger/context.d.ts +8 -1
- package/dist/debugger/context.js +15 -3
- package/dist/debugger/events/protocol-summarize.js +18 -3
- package/dist/debugger/index.d.ts +1 -1
- package/dist/debugger/index.js +1 -1
- package/dist/debugger/shell/debugger-shell.js +1 -1
- package/dist/debugger/shell/events-pane.js +4 -3
- package/dist/debugger/styles.css +1 -0
- package/dist/evaluator/definitions-store.d.ts +3 -4
- package/dist/evaluator/definitions-store.js +3 -3
- package/dist/evaluator/definitions.d.ts +4 -1
- package/dist/evaluator/definitions.js +5 -2
- package/dist/evaluator/effects.d.ts +41 -0
- package/dist/evaluator/effects.js +6 -2
- package/dist/evaluator/module.d.ts +84 -0
- package/dist/evaluator/module.js +7 -3
- package/dist/evaluator/threads.d.ts +2 -0
- package/dist/evaluator/threads.js +4 -0
- package/dist/projectors/event-log.d.ts +87 -0
- package/dist/projectors/event-log.js +396 -0
- package/dist/projectors/index-model.d.ts +12 -0
- package/dist/projectors/index-model.js +12 -3
- package/dist/projectors/memory.js +7 -0
- package/dist/projectors/postgres.d.ts +1 -0
- package/dist/projectors/postgres.js +32 -15
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +26 -1
- package/dist/projectors/sqlite.js +47 -19
- package/dist/projectors/with-projectors.d.ts +7 -1
- package/dist/projectors/with-projectors.js +13 -3
- package/dist/react/materialize.js +5 -1
- package/dist/react/react.d.ts +10 -2
- package/dist/react/react.js +43 -19
- package/dist/react/store.d.ts +11 -2
- package/dist/react/store.js +104 -12
- package/dist/react/tables.d.ts +4 -1
- package/dist/runtime/cancellation.d.ts +2 -1
- package/dist/runtime/cancellation.js +35 -2
- package/dist/runtime/child-run.d.ts +10 -0
- package/dist/runtime/child-run.js +58 -0
- package/dist/runtime/deadline-transition-planner.js +11 -2
- package/dist/runtime/dispatch-effect.d.ts +2 -2
- package/dist/runtime/dispatch-effect.js +43 -19
- package/dist/runtime/errors.d.ts +14 -1
- package/dist/runtime/errors.js +10 -0
- package/dist/runtime/filesystem-blob-store.d.ts +6 -0
- package/dist/runtime/filesystem-blob-store.js +59 -0
- package/dist/runtime/helpers.d.ts +15 -6
- package/dist/runtime/helpers.js +46 -6
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/local-effect-executor.d.ts +11 -2
- package/dist/runtime/local-effect-executor.js +80 -4
- package/dist/runtime/looms.d.ts +38 -4
- package/dist/runtime/looms.js +17 -3
- package/dist/runtime/repository.d.ts +5 -4
- package/dist/runtime/repository.js +5 -2
- package/dist/runtime/retry-policy.d.ts +5 -0
- package/dist/runtime/retry-policy.js +10 -2
- package/dist/runtime/run-start.d.ts +1 -1
- package/dist/runtime/run-start.js +10 -0
- package/dist/runtime/runtime.d.ts +2 -2
- package/dist/runtime/runtime.js +191 -68
- package/dist/runtime/server/auth.d.ts +3 -0
- package/dist/runtime/server/fetch-handler.d.ts +2 -2
- package/dist/runtime/server/fetch-handler.js +35 -4
- package/dist/runtime/server/grant.d.ts +49 -0
- package/dist/runtime/server/grant.js +177 -0
- package/dist/runtime/server/routes/effects.js +47 -5
- package/dist/runtime/server/routes/runs.js +6 -1
- package/dist/runtime/server/run-route.d.ts +18 -0
- package/dist/runtime/server/run-route.js +26 -0
- package/dist/runtime/server.d.ts +3 -1
- package/dist/runtime/server.js +3 -1
- package/dist/runtime/sse.js +26 -4
- package/dist/runtime/subscription.d.ts +8 -0
- package/dist/runtime/subscription.js +23 -0
- package/dist/runtime/types.d.ts +87 -8
- package/dist/runtime/wake-engine.d.ts +4 -2
- package/dist/runtime/wake-engine.js +57 -6
- package/dist/runtime/worker-callbacks.d.ts +12 -2
- package/dist/runtime/worker-callbacks.js +102 -72
- package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
- package/dist/runtime/worker-effect-dispatch.js +84 -18
- package/dist/schedule/debugger/index.d.ts +4 -0
- package/dist/schedule/debugger/index.js +11 -0
- package/dist/schedule/debugger/schedule-view.d.ts +3 -0
- package/dist/schedule/debugger/schedule-view.js +68 -0
- package/dist/schedule/debugger/summarize.d.ts +2 -0
- package/dist/schedule/debugger/summarize.js +43 -0
- package/dist/schedule/definitions-store.d.ts +10 -0
- package/dist/schedule/definitions-store.js +8 -0
- package/dist/schedule/definitions.d.ts +57 -0
- package/dist/schedule/definitions.js +30 -0
- package/dist/schedule/effects.d.ts +756 -0
- package/dist/schedule/effects.js +244 -0
- package/dist/schedule/ensure.d.ts +55 -0
- package/dist/schedule/ensure.js +109 -0
- package/dist/schedule/events.d.ts +116 -0
- package/dist/schedule/events.js +49 -0
- package/dist/schedule/index.d.ts +11 -0
- package/dist/schedule/index.js +11 -0
- package/dist/schedule/module.d.ts +1474 -0
- package/dist/schedule/module.js +22 -0
- package/dist/schedule/projections.d.ts +55 -0
- package/dist/schedule/projections.js +88 -0
- package/dist/schedule/scope.d.ts +121 -0
- package/dist/schedule/scope.js +7 -0
- package/dist/schedule/signals.d.ts +25 -0
- package/dist/schedule/signals.js +33 -0
- package/dist/schedule/specs.d.ts +59 -0
- package/dist/schedule/specs.js +231 -0
- package/dist/schedule/threads.d.ts +25 -0
- package/dist/schedule/threads.js +259 -0
- package/dist/testing/corpus-module.d.ts +34 -0
- package/dist/testing/corpus-module.js +87 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +5 -6
- package/dist/testing/replay-corpus.d.ts +22 -0
- package/dist/testing/replay-corpus.js +50 -0
- package/dist/workflow/definitions-store.d.ts +1 -1
- package/dist/workflow/definitions-store.js +1 -1
- package/dist/workflow/definitions.d.ts +14 -0
- package/dist/workflow/effects.d.ts +122 -0
- package/dist/workflow/effects.js +16 -6
- package/dist/workflow/events.d.ts +42 -0
- package/dist/workflow/events.js +4 -0
- package/dist/workflow/module.d.ts +313 -0
- package/dist/workflow/module.js +7 -3
- package/dist/workflow/scope.d.ts +42 -0
- package/dist/workflow/threads.d.ts +72 -0
- package/dist/workflow/threads.js +106 -38
- package/package.json +14 -1
- package/dist/debugger/app/assets/index-D--9esm9.js +0 -22
- package/dist/debugger/app/assets/routes-Dakrdusf.js +0 -47
|
@@ -1,21 +1,54 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
2
|
import { createIndexProjector, } from './index-model.js';
|
|
3
3
|
import { runProjectorSync } from './projector.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SQLITE_ADDED_ACTOR_COLUMNS, SQLITE_INDEXES_SQL, SQLITE_TABLES_SQL } from './schema.js';
|
|
5
5
|
export { SQLITE_SCHEMA_SQL } from './schema.js';
|
|
6
|
+
const ACTOR_COLUMNS = `run_id, actor_id, kind, status, definition_name, parent_actor_id,
|
|
7
|
+
launched_by_run_id, launched_by_effect_id, correlation_id, updated_at`;
|
|
6
8
|
function hasDb(options) {
|
|
7
9
|
return 'db' in options;
|
|
8
10
|
}
|
|
11
|
+
function actorFromRow(row) {
|
|
12
|
+
return {
|
|
13
|
+
runId: row.run_id,
|
|
14
|
+
actorId: row.actor_id,
|
|
15
|
+
kind: row.kind,
|
|
16
|
+
status: row.status,
|
|
17
|
+
definitionName: row.definition_name,
|
|
18
|
+
parentActorId: row.parent_actor_id,
|
|
19
|
+
launchedByRunId: row.launched_by_run_id,
|
|
20
|
+
launchedByEffectId: row.launched_by_effect_id,
|
|
21
|
+
correlationId: row.correlation_id,
|
|
22
|
+
updatedAt: row.updated_at,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Creates the tables, adds columns a database from an earlier release lacks, then indexes them. */
|
|
26
|
+
function migrate(db) {
|
|
27
|
+
db.exec(SQLITE_TABLES_SQL);
|
|
28
|
+
const existing = new Set(db
|
|
29
|
+
.query('PRAGMA table_info(looms_run_actors)')
|
|
30
|
+
.all()
|
|
31
|
+
.map((column) => column.name));
|
|
32
|
+
for (const column of SQLITE_ADDED_ACTOR_COLUMNS) {
|
|
33
|
+
if (!existing.has(column)) {
|
|
34
|
+
db.exec(`ALTER TABLE looms_run_actors ADD COLUMN ${column} TEXT`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
db.exec(SQLITE_INDEXES_SQL);
|
|
38
|
+
}
|
|
9
39
|
function applyOp(db, op) {
|
|
10
40
|
switch (op.type) {
|
|
11
41
|
case 'upsertActor':
|
|
12
|
-
db.run(`INSERT INTO looms_run_actors (
|
|
13
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
42
|
+
db.run(`INSERT INTO looms_run_actors (${ACTOR_COLUMNS})
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
14
44
|
ON CONFLICT (run_id, actor_id) DO UPDATE SET
|
|
15
45
|
kind = excluded.kind,
|
|
16
46
|
status = excluded.status,
|
|
17
47
|
definition_name = excluded.definition_name,
|
|
18
48
|
parent_actor_id = excluded.parent_actor_id,
|
|
49
|
+
launched_by_run_id = excluded.launched_by_run_id,
|
|
50
|
+
launched_by_effect_id = excluded.launched_by_effect_id,
|
|
51
|
+
correlation_id = excluded.correlation_id,
|
|
19
52
|
updated_at = excluded.updated_at`, [
|
|
20
53
|
op.runId,
|
|
21
54
|
op.actorId,
|
|
@@ -23,6 +56,9 @@ function applyOp(db, op) {
|
|
|
23
56
|
op.status,
|
|
24
57
|
op.definitionName,
|
|
25
58
|
op.parentActorId,
|
|
59
|
+
op.launchedByRunId,
|
|
60
|
+
op.launchedByEffectId,
|
|
61
|
+
op.correlationId,
|
|
26
62
|
op.updatedAt,
|
|
27
63
|
]);
|
|
28
64
|
return;
|
|
@@ -53,9 +89,7 @@ export function sqlite(options = {}) {
|
|
|
53
89
|
const createdDb = !hasDb(options);
|
|
54
90
|
const db = hasDb(options) ? options.db : new Database(options.path ?? ':memory:');
|
|
55
91
|
const backend = {
|
|
56
|
-
init: () => runProjectorSync('sqlite', () =>
|
|
57
|
-
db.exec(SQLITE_SCHEMA_SQL);
|
|
58
|
-
}),
|
|
92
|
+
init: () => runProjectorSync('sqlite', () => migrate(db)),
|
|
59
93
|
applyOps: (ops) => runProjectorSync('sqlite', () => {
|
|
60
94
|
const apply = db.transaction((batch) => {
|
|
61
95
|
for (const op of batch) {
|
|
@@ -66,21 +100,15 @@ export function sqlite(options = {}) {
|
|
|
66
100
|
}),
|
|
67
101
|
getActor: (runId, actorId) => runProjectorSync('sqlite', () => {
|
|
68
102
|
const row = db
|
|
69
|
-
.query(`SELECT run_id
|
|
70
|
-
FROM looms_run_actors WHERE run_id = ? AND actor_id = ?`)
|
|
103
|
+
.query(`SELECT ${ACTOR_COLUMNS} FROM looms_run_actors WHERE run_id = ? AND actor_id = ?`)
|
|
71
104
|
.get(runId, actorId);
|
|
72
|
-
return row
|
|
73
|
-
? {
|
|
74
|
-
runId: row.run_id,
|
|
75
|
-
actorId: row.actor_id,
|
|
76
|
-
kind: row.kind,
|
|
77
|
-
status: row.status,
|
|
78
|
-
definitionName: row.definition_name,
|
|
79
|
-
parentActorId: row.parent_actor_id,
|
|
80
|
-
updatedAt: row.updated_at,
|
|
81
|
-
}
|
|
82
|
-
: null;
|
|
105
|
+
return row ? actorFromRow(row) : null;
|
|
83
106
|
}),
|
|
107
|
+
listLaunched: (runId) => runProjectorSync('sqlite', () => db
|
|
108
|
+
.query(`SELECT ${ACTOR_COLUMNS} FROM looms_run_actors
|
|
109
|
+
WHERE launched_by_run_id = ? AND actor_id = run_id ORDER BY updated_at`)
|
|
110
|
+
.all(runId)
|
|
111
|
+
.map(actorFromRow)),
|
|
84
112
|
listReviews: (runId) => runProjectorSync('sqlite', () => {
|
|
85
113
|
const rows = runId
|
|
86
114
|
? db
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Layer } from 'effect';
|
|
1
|
+
import { Effect, Layer } from 'effect';
|
|
2
2
|
import { EventStoreTag, type EventStore, type EventEnvelope } from '@swirls/looms/core';
|
|
3
3
|
import type { ProjectorDelivery } from './delivery';
|
|
4
4
|
import { type Projector, type ProjectorErrorHandler } from './projector';
|
|
@@ -6,6 +6,12 @@ export interface WithProjectorsOptions {
|
|
|
6
6
|
onError?: ProjectorErrorHandler;
|
|
7
7
|
/** Durable cursor runner. When present, appends trigger catch-up from its persisted cursor. */
|
|
8
8
|
delivery?: ProjectorDelivery;
|
|
9
|
+
/**
|
|
10
|
+
* Runs projection after the append commits, without making the append wait.
|
|
11
|
+
* The callback has to keep the effect alive, for example with a Durable Object's `waitUntil`.
|
|
12
|
+
* Without it, projection still finishes before `append` returns.
|
|
13
|
+
*/
|
|
14
|
+
scheduleDelivery?: (runId: string, delivery: Effect.Effect<void>) => void;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
17
|
* Wrap an EventStore so every successful append is also projected.
|
|
@@ -16,6 +16,7 @@ function logDeliveryFailure(runId, cause) {
|
|
|
16
16
|
export function withProjectors(store, projectors, options) {
|
|
17
17
|
const onError = options?.onError ?? defaultOnError;
|
|
18
18
|
const delivery = options?.delivery;
|
|
19
|
+
const scheduleDelivery = options?.scheduleDelivery;
|
|
19
20
|
const project = (runId, sequences) => {
|
|
20
21
|
if (delivery) {
|
|
21
22
|
return delivery.deliver(runId).pipe(Effect.catch((cause) => logDeliveryFailure(runId, cause)));
|
|
@@ -29,9 +30,18 @@ export function withProjectors(store, projectors, options) {
|
|
|
29
30
|
}))), { concurrency: 'unbounded', discard: true })), Effect.catch((cause) => logDeliveryFailure(runId, cause)));
|
|
30
31
|
};
|
|
31
32
|
const wrapped = {
|
|
32
|
-
append: (runId, events, appendOptions) => store
|
|
33
|
-
.
|
|
34
|
-
|
|
33
|
+
append: (runId, events, appendOptions) => store.append(runId, events, appendOptions).pipe(Effect.tap((result) => {
|
|
34
|
+
if (projectors.length === 0) {
|
|
35
|
+
return Effect.void;
|
|
36
|
+
}
|
|
37
|
+
const work = project(runId, result.sequences);
|
|
38
|
+
if (!scheduleDelivery) {
|
|
39
|
+
return work;
|
|
40
|
+
}
|
|
41
|
+
return Effect.sync(() => {
|
|
42
|
+
scheduleDelivery(runId, work);
|
|
43
|
+
});
|
|
44
|
+
})),
|
|
35
45
|
read: (runId, readOptions) => store.read(runId, readOptions),
|
|
36
46
|
tail: (runId) => store.tail(runId),
|
|
37
47
|
bounds: store.bounds ? (runId) => store.bounds(runId) : undefined,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { completedRunStatus, isJsonObject, isJsonString, } from '@swirls/looms/core';
|
|
1
|
+
import { completedRunStatus, isJsonObject, isJsonString, readAttributes, readRunLaunchSource, } from '@swirls/looms/core';
|
|
2
2
|
import { emptyTables } from './tables.js';
|
|
3
3
|
function payloadObject(event) {
|
|
4
4
|
return isJsonObject(event.payload) ? event.payload : {};
|
|
@@ -15,6 +15,8 @@ function setRunStatus(tables, runId, status) {
|
|
|
15
15
|
rootThreadId: existing?.rootThreadId ?? null,
|
|
16
16
|
kind: existing?.kind ?? null,
|
|
17
17
|
definitionName: existing?.definitionName ?? null,
|
|
18
|
+
launchedBy: existing?.launchedBy ?? null,
|
|
19
|
+
attributes: existing?.attributes ?? {},
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
22
|
export function materializeEvents(events, from = emptyTables()) {
|
|
@@ -34,6 +36,8 @@ export function materializeEvents(events, from = emptyTables()) {
|
|
|
34
36
|
rootThreadId,
|
|
35
37
|
kind: readString(payload, 'kind') ?? existing?.kind ?? null,
|
|
36
38
|
definitionName: readString(payload, 'definitionName') ?? existing?.definitionName ?? null,
|
|
39
|
+
launchedBy: readRunLaunchSource(payload.launchedBy) ?? existing?.launchedBy ?? null,
|
|
40
|
+
attributes: readAttributes(payload.attributes),
|
|
37
41
|
};
|
|
38
42
|
tables.runs.set(event.runId, next);
|
|
39
43
|
break;
|
package/dist/react/react.d.ts
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import { type AnyEventEnvelope, type ProjectionDefinition, type RegisteredEvent, type RunStatus, type ThreadTree } from '@swirls/looms/core';
|
|
2
|
+
import { type AnyEventEnvelope, type ProjectionDefinition, type RegisteredEvent, type RunAttributes, type RunLaunchSource, type RunStatus, type SubscriptionTransport, type ThreadTree } from '@swirls/looms/core';
|
|
3
3
|
import { defineLiveFold, foldLive, type LiveFoldDefinition } from './derived';
|
|
4
4
|
import { createLoomsStore, type CoalesceOption, type LoomsClientStore, type LoomsConnectionStatus } from './store';
|
|
5
5
|
export interface LoomsProviderProps {
|
|
6
6
|
children: ReactNode;
|
|
7
7
|
endpoint?: string;
|
|
8
8
|
coalesce?: CoalesceOption;
|
|
9
|
+
transport?: SubscriptionTransport;
|
|
9
10
|
}
|
|
10
11
|
interface ProviderContextValue {
|
|
11
12
|
endpoint: string;
|
|
12
13
|
coalesce?: CoalesceOption;
|
|
14
|
+
transport?: SubscriptionTransport;
|
|
13
15
|
}
|
|
14
16
|
/** Root provider that supplies the Looms host endpoint to run stores. */
|
|
15
|
-
export declare function LoomsProvider({ children, endpoint, coalesce }: LoomsProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<ProviderContextValue>>;
|
|
17
|
+
export declare function LoomsProvider({ children, endpoint, coalesce, transport, }: LoomsProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<ProviderContextValue>>;
|
|
16
18
|
export interface UseRunStoreOptions {
|
|
17
19
|
endpoint?: string;
|
|
18
20
|
reconnectDelayMs?: number;
|
|
19
21
|
coalesce?: CoalesceOption;
|
|
22
|
+
transport?: SubscriptionTransport;
|
|
23
|
+
threadId?: string;
|
|
24
|
+
includeEphemeral?: boolean;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
27
|
* Acquires a non-subscribing handle to the run store.
|
|
@@ -38,6 +43,9 @@ export interface RunSummary {
|
|
|
38
43
|
readonly definitionName: string | undefined;
|
|
39
44
|
readonly rootThreadId: string | undefined;
|
|
40
45
|
readonly startedAt: number | undefined;
|
|
46
|
+
/** The effect in another run that launched this one. */
|
|
47
|
+
readonly launchedBy: RunLaunchSource | undefined;
|
|
48
|
+
readonly attributes: RunAttributes;
|
|
41
49
|
readonly connection: LoomsConnectionStatus;
|
|
42
50
|
}
|
|
43
51
|
/** Lightweight subscription to top-level run metadata and connection status. */
|
package/dist/react/react.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { Effect, Fiber } from 'effect';
|
|
2
2
|
import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore, } from 'react';
|
|
3
|
-
import { isTerminalStatus, threadTree, toThreadTree, } from '@swirls/looms/core';
|
|
3
|
+
import { isTerminalStatus, sameStringRecord, threadTree, toThreadTree, } from '@swirls/looms/core';
|
|
4
4
|
import { defineLiveFold, foldLive } from './derived.js';
|
|
5
5
|
import { createLoomsStore, } from './store.js';
|
|
6
6
|
const LoomsContext = createContext({ endpoint: '' });
|
|
7
7
|
const GRACE_MS = 5_000;
|
|
8
8
|
const registry = new Map();
|
|
9
|
-
function registryKey(
|
|
10
|
-
return
|
|
9
|
+
function registryKey(runId, identity) {
|
|
10
|
+
return [
|
|
11
|
+
identity.endpoint,
|
|
12
|
+
runId,
|
|
13
|
+
identity.transport ?? '',
|
|
14
|
+
identity.threadId ?? '',
|
|
15
|
+
identity.includeEphemeral === false ? '0' : '1',
|
|
16
|
+
].join('\0');
|
|
11
17
|
}
|
|
12
18
|
function disposeEntry(key) {
|
|
13
19
|
const entry = registry.get(key);
|
|
@@ -20,17 +26,20 @@ function disposeEntry(key) {
|
|
|
20
26
|
function scheduleDisposal(key) {
|
|
21
27
|
return Effect.runFork(Effect.sleep(GRACE_MS).pipe(Effect.andThen(Effect.sync(() => disposeEntry(key)))));
|
|
22
28
|
}
|
|
23
|
-
function peekStore(runId,
|
|
24
|
-
const key = registryKey(
|
|
29
|
+
function peekStore(runId, identity) {
|
|
30
|
+
const key = registryKey(runId, identity);
|
|
25
31
|
const existing = registry.get(key);
|
|
26
32
|
if (existing) {
|
|
27
33
|
return existing.store;
|
|
28
34
|
}
|
|
29
35
|
const store = createLoomsStore({
|
|
30
36
|
storeId: runId,
|
|
31
|
-
endpoint,
|
|
32
|
-
reconnectDelayMs,
|
|
33
|
-
coalesce,
|
|
37
|
+
endpoint: identity.endpoint,
|
|
38
|
+
reconnectDelayMs: identity.reconnectDelayMs,
|
|
39
|
+
coalesce: identity.coalesce,
|
|
40
|
+
transport: identity.transport,
|
|
41
|
+
threadId: identity.threadId,
|
|
42
|
+
includeEphemeral: identity.includeEphemeral,
|
|
34
43
|
});
|
|
35
44
|
registry.set(key, {
|
|
36
45
|
store,
|
|
@@ -39,9 +48,9 @@ function peekStore(runId, endpoint, reconnectDelayMs, coalesce) {
|
|
|
39
48
|
});
|
|
40
49
|
return store;
|
|
41
50
|
}
|
|
42
|
-
function retainStore(runId,
|
|
43
|
-
const key = registryKey(
|
|
44
|
-
peekStore(runId,
|
|
51
|
+
function retainStore(runId, identity) {
|
|
52
|
+
const key = registryKey(runId, identity);
|
|
53
|
+
peekStore(runId, identity);
|
|
45
54
|
const entry = registry.get(key);
|
|
46
55
|
if (!entry) {
|
|
47
56
|
return;
|
|
@@ -52,8 +61,8 @@ function retainStore(runId, endpoint, reconnectDelayMs, coalesce) {
|
|
|
52
61
|
entry.disposal = undefined;
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
|
-
function releaseStore(runId,
|
|
56
|
-
const key = registryKey(
|
|
64
|
+
function releaseStore(runId, identity) {
|
|
65
|
+
const key = registryKey(runId, identity);
|
|
57
66
|
const entry = registry.get(key);
|
|
58
67
|
if (!entry) {
|
|
59
68
|
return;
|
|
@@ -64,8 +73,8 @@ function releaseStore(runId, endpoint) {
|
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
/** Root provider that supplies the Looms host endpoint to run stores. */
|
|
67
|
-
export function LoomsProvider({ children, endpoint = '', coalesce }) {
|
|
68
|
-
const value = useMemo(() => ({ endpoint, coalesce }), [endpoint, coalesce]);
|
|
76
|
+
export function LoomsProvider({ children, endpoint = '', coalesce, transport, }) {
|
|
77
|
+
const value = useMemo(() => ({ endpoint, coalesce, transport }), [endpoint, coalesce, transport]);
|
|
69
78
|
return createElement(LoomsContext.Provider, { value, children });
|
|
70
79
|
}
|
|
71
80
|
/**
|
|
@@ -78,11 +87,22 @@ export function useRunStore(runId, options) {
|
|
|
78
87
|
const endpoint = options?.endpoint ?? context.endpoint;
|
|
79
88
|
const reconnectDelayMs = options?.reconnectDelayMs;
|
|
80
89
|
const coalesce = options?.coalesce ?? context.coalesce;
|
|
81
|
-
const
|
|
90
|
+
const transport = options?.transport ?? context.transport;
|
|
91
|
+
const threadId = options?.threadId;
|
|
92
|
+
const includeEphemeral = options?.includeEphemeral;
|
|
93
|
+
const identity = {
|
|
94
|
+
endpoint,
|
|
95
|
+
reconnectDelayMs,
|
|
96
|
+
coalesce,
|
|
97
|
+
transport,
|
|
98
|
+
threadId,
|
|
99
|
+
includeEphemeral,
|
|
100
|
+
};
|
|
101
|
+
const store = peekStore(runId, identity);
|
|
82
102
|
useEffect(() => {
|
|
83
|
-
retainStore(runId,
|
|
84
|
-
return () => releaseStore(runId,
|
|
85
|
-
}, [runId, endpoint, reconnectDelayMs, coalesce]);
|
|
103
|
+
retainStore(runId, identity);
|
|
104
|
+
return () => releaseStore(runId, identity);
|
|
105
|
+
}, [runId, endpoint, reconnectDelayMs, coalesce, transport, threadId, includeEphemeral]);
|
|
86
106
|
return store;
|
|
87
107
|
}
|
|
88
108
|
function useStoreSnapshot(store, getSnapshot) {
|
|
@@ -119,6 +139,8 @@ function shallowEqualSummary(a, b) {
|
|
|
119
139
|
a.definitionName === b.definitionName &&
|
|
120
140
|
a.rootThreadId === b.rootThreadId &&
|
|
121
141
|
a.startedAt === b.startedAt &&
|
|
142
|
+
a.launchedBy === b.launchedBy &&
|
|
143
|
+
sameStringRecord(a.attributes, b.attributes) &&
|
|
122
144
|
a.connection === b.connection);
|
|
123
145
|
}
|
|
124
146
|
/** Lightweight subscription to top-level run metadata and connection status. */
|
|
@@ -134,6 +156,8 @@ export function useRunSummary(store) {
|
|
|
134
156
|
definitionName: run?.definitionName ?? undefined,
|
|
135
157
|
rootThreadId: run?.rootThreadId ?? undefined,
|
|
136
158
|
startedAt: s.query.startedAt(),
|
|
159
|
+
launchedBy: run?.launchedBy ?? undefined,
|
|
160
|
+
attributes: run?.attributes ?? {},
|
|
137
161
|
connection: s.status(),
|
|
138
162
|
};
|
|
139
163
|
}, []);
|
package/dist/react/store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { SubscriptionSocketConstructor } from '@swirls/looms/client';
|
|
2
|
+
import { type AnyEventEnvelope, type EventInput, type ProjectionDefinition, type RegisteredEvent, type SubscriptionTransport } from '@swirls/looms/core';
|
|
2
3
|
import { type LiveFoldDefinition } from './derived';
|
|
3
4
|
import { type CoalesceOption } from './notify';
|
|
4
5
|
import { type MaterializedTables } from './tables';
|
|
@@ -14,10 +15,18 @@ export interface LoomsClientStoreOptions {
|
|
|
14
15
|
* Opens `${endpoint}/runs/:id/events?live=true` as an SSE stream.
|
|
15
16
|
*/
|
|
16
17
|
endpoint: string;
|
|
17
|
-
/** Backoff between SSE
|
|
18
|
+
/** Backoff between reconnects. The delay stays fixed, matching the SSE client. */
|
|
18
19
|
reconnectDelayMs?: number;
|
|
19
20
|
fetch?: typeof fetch;
|
|
20
21
|
coalesce?: CoalesceOption;
|
|
22
|
+
/**
|
|
23
|
+
* `undefined` keeps the current SSE path. `websocket` and `auto` use a hibernating
|
|
24
|
+
* socket when a constructor is available.
|
|
25
|
+
*/
|
|
26
|
+
transport?: SubscriptionTransport;
|
|
27
|
+
threadId?: string;
|
|
28
|
+
includeEphemeral?: boolean;
|
|
29
|
+
webSocket?: SubscriptionSocketConstructor;
|
|
21
30
|
}
|
|
22
31
|
export interface LoomsStoreQuery<TEvent extends AnyEventEnvelope = RegisteredEvent> {
|
|
23
32
|
events: (options?: {
|
package/dist/react/store.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Data, Effect, Fiber, Option, Predicate, Schema } from 'effect';
|
|
2
|
-
import { decodeLoomsEvent, stringifyJson, } from '@swirls/looms/core';
|
|
2
|
+
import { decodeLoomsEvent, readSubscriptionData, stringifyJson, } from '@swirls/looms/core';
|
|
3
3
|
import { EventIndex, ProjectionCache } from './derived.js';
|
|
4
4
|
import { materializeEvents } from './materialize.js';
|
|
5
5
|
import { createNotifyScheduler } from './notify.js';
|
|
6
|
-
import { consumeSseStreamEffect
|
|
6
|
+
import { consumeSseStreamEffect } from './sse.js';
|
|
7
7
|
import { emptyTables } from './tables.js';
|
|
8
8
|
class LoomsStoreError extends Data.TaggedError('LoomsStoreError') {
|
|
9
9
|
constructor(message, cause) {
|
|
@@ -11,6 +11,18 @@ class LoomsStoreError extends Data.TaggedError('LoomsStoreError') {
|
|
|
11
11
|
this.name = 'LoomsStoreError';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
function socketConstructor(options) {
|
|
15
|
+
if (options.transport !== 'websocket' && options.transport !== 'auto') {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (options.webSocket) {
|
|
19
|
+
return options.webSocket;
|
|
20
|
+
}
|
|
21
|
+
if (!('WebSocket' in globalThis) || !Predicate.isFunction(globalThis.WebSocket)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return globalThis.WebSocket;
|
|
25
|
+
}
|
|
14
26
|
function eventSourceCtor() {
|
|
15
27
|
if (typeof EventSource === 'undefined') {
|
|
16
28
|
return undefined;
|
|
@@ -42,6 +54,7 @@ export function createLoomsStore(options) {
|
|
|
42
54
|
let liveStarted = false;
|
|
43
55
|
let liveAbort;
|
|
44
56
|
let eventSource;
|
|
57
|
+
let socket;
|
|
45
58
|
let reconnectFiber;
|
|
46
59
|
let syncing = null;
|
|
47
60
|
const publishedEvents = () => {
|
|
@@ -88,11 +101,33 @@ export function createLoomsStore(options) {
|
|
|
88
101
|
version += 1;
|
|
89
102
|
notify.schedule();
|
|
90
103
|
};
|
|
91
|
-
const
|
|
92
|
-
|
|
104
|
+
const applyLiveData = (data) => {
|
|
105
|
+
const read = readSubscriptionData(data, options.storeId);
|
|
106
|
+
switch (read.kind) {
|
|
107
|
+
case 'events':
|
|
108
|
+
applyBatch(read.events.slice());
|
|
109
|
+
return 'continue';
|
|
110
|
+
case 'resync':
|
|
111
|
+
fromSeq = read.head;
|
|
112
|
+
return 'resync';
|
|
113
|
+
case 'hello':
|
|
114
|
+
case 'ignore':
|
|
115
|
+
return 'continue';
|
|
116
|
+
default: {
|
|
117
|
+
const unexpected = read;
|
|
118
|
+
return unexpected;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
93
121
|
};
|
|
94
122
|
const liveUrl = () => {
|
|
95
|
-
|
|
123
|
+
const params = new URLSearchParams({ live: 'true', fromSeq: String(fromSeq) });
|
|
124
|
+
if (options.threadId) {
|
|
125
|
+
params.set('threadId', options.threadId);
|
|
126
|
+
}
|
|
127
|
+
if (options.includeEphemeral === false) {
|
|
128
|
+
params.set('includeEphemeral', 'false');
|
|
129
|
+
}
|
|
130
|
+
return `${endpoint}/runs/${encodeURIComponent(options.storeId)}/events?${params.toString()}`;
|
|
96
131
|
};
|
|
97
132
|
const pull = Effect.gen(function* () {
|
|
98
133
|
const url = `${endpoint}/runs/${encodeURIComponent(options.storeId)}/events?fromSeq=${fromSeq}`;
|
|
@@ -135,7 +170,7 @@ export function createLoomsStore(options) {
|
|
|
135
170
|
});
|
|
136
171
|
return syncing;
|
|
137
172
|
};
|
|
138
|
-
const scheduleReconnect = (
|
|
173
|
+
const scheduleReconnect = (start) => {
|
|
139
174
|
if (disposed || !liveStarted || reconnectFiber !== undefined) {
|
|
140
175
|
return;
|
|
141
176
|
}
|
|
@@ -150,7 +185,7 @@ export function createLoomsStore(options) {
|
|
|
150
185
|
catch: (cause) => new LoomsStoreError('Event resync failed', cause),
|
|
151
186
|
}).pipe(Effect.ignore);
|
|
152
187
|
if (!disposed && liveStarted) {
|
|
153
|
-
|
|
188
|
+
start();
|
|
154
189
|
}
|
|
155
190
|
}));
|
|
156
191
|
};
|
|
@@ -165,7 +200,16 @@ export function createLoomsStore(options) {
|
|
|
165
200
|
setStatus('live');
|
|
166
201
|
});
|
|
167
202
|
es.addEventListener('message', (msg) => {
|
|
168
|
-
|
|
203
|
+
if (applyLiveData(msg.data) === 'resync') {
|
|
204
|
+
es.close();
|
|
205
|
+
if (eventSource === es) {
|
|
206
|
+
eventSource = undefined;
|
|
207
|
+
}
|
|
208
|
+
setStatus('reconnecting');
|
|
209
|
+
scheduleReconnect(() => {
|
|
210
|
+
startEventSource(Ctor);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
169
213
|
});
|
|
170
214
|
es.addEventListener('error', () => {
|
|
171
215
|
live = es.readyState === Ctor.OPEN;
|
|
@@ -178,7 +222,9 @@ export function createLoomsStore(options) {
|
|
|
178
222
|
if (eventSource === es) {
|
|
179
223
|
eventSource = undefined;
|
|
180
224
|
}
|
|
181
|
-
scheduleReconnect(
|
|
225
|
+
scheduleReconnect(() => {
|
|
226
|
+
startEventSource(Ctor);
|
|
227
|
+
});
|
|
182
228
|
}
|
|
183
229
|
else if (!live) {
|
|
184
230
|
setStatus('reconnecting');
|
|
@@ -209,9 +255,7 @@ export function createLoomsStore(options) {
|
|
|
209
255
|
}
|
|
210
256
|
live = true;
|
|
211
257
|
setStatus('live');
|
|
212
|
-
return yield* consumeSseStreamEffect(body, (frame) =>
|
|
213
|
-
applyEncoded(frame.data);
|
|
214
|
-
});
|
|
258
|
+
return yield* consumeSseStreamEffect(body, (frame) => applyLiveData(frame.data) === 'resync' ? false : undefined);
|
|
215
259
|
}).pipe(Effect.catch(() => Effect.sync(() => {
|
|
216
260
|
live = false;
|
|
217
261
|
if (!disposed) {
|
|
@@ -226,12 +270,58 @@ export function createLoomsStore(options) {
|
|
|
226
270
|
});
|
|
227
271
|
void Effect.runPromise(run, { signal: controller.signal }).catch(() => undefined);
|
|
228
272
|
};
|
|
273
|
+
const startWebSocket = (Ctor) => {
|
|
274
|
+
if (disposed || !liveStarted) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
let url;
|
|
278
|
+
try {
|
|
279
|
+
const http = new URL(liveUrl());
|
|
280
|
+
http.protocol = http.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
281
|
+
url = http.toString();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
startFetchStream();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const ws = new Ctor(url);
|
|
288
|
+
socket = ws;
|
|
289
|
+
ws.addEventListener('open', () => {
|
|
290
|
+
live = true;
|
|
291
|
+
setStatus('live');
|
|
292
|
+
});
|
|
293
|
+
ws.addEventListener('message', (event) => {
|
|
294
|
+
if (!Predicate.isString(event.data)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (applyLiveData(event.data) === 'resync') {
|
|
298
|
+
ws.close();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
ws.addEventListener('close', () => {
|
|
302
|
+
live = false;
|
|
303
|
+
if (socket === ws) {
|
|
304
|
+
socket = undefined;
|
|
305
|
+
}
|
|
306
|
+
if (!disposed && liveStarted) {
|
|
307
|
+
setStatus('reconnecting');
|
|
308
|
+
scheduleReconnect(() => {
|
|
309
|
+
startWebSocket(Ctor);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
};
|
|
229
314
|
const startLive = () => {
|
|
230
315
|
if (!options.storeId || (liveStarted && !disposed)) {
|
|
231
316
|
return;
|
|
232
317
|
}
|
|
233
318
|
liveStarted = true;
|
|
234
319
|
disposed = false;
|
|
320
|
+
const socketCtor = socketConstructor(options);
|
|
321
|
+
if (socketCtor) {
|
|
322
|
+
startWebSocket(socketCtor);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
235
325
|
const ctor = options.fetch === undefined ? eventSourceCtor() : undefined;
|
|
236
326
|
if (ctor) {
|
|
237
327
|
startEventSource(ctor);
|
|
@@ -249,6 +339,8 @@ export function createLoomsStore(options) {
|
|
|
249
339
|
}
|
|
250
340
|
eventSource?.close();
|
|
251
341
|
eventSource = undefined;
|
|
342
|
+
socket?.close();
|
|
343
|
+
socket = undefined;
|
|
252
344
|
liveAbort?.abort();
|
|
253
345
|
liveAbort = undefined;
|
|
254
346
|
};
|
package/dist/react/tables.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { RunStatus, ThreadStatus } from '@swirls/looms/core';
|
|
1
|
+
import type { RunAttributes, RunLaunchSource, RunStatus, ThreadStatus } from '@swirls/looms/core';
|
|
2
2
|
export interface RunRow {
|
|
3
3
|
runId: string;
|
|
4
4
|
status: RunStatus;
|
|
5
5
|
rootThreadId: string | null;
|
|
6
6
|
kind: string | null;
|
|
7
7
|
definitionName: string | null;
|
|
8
|
+
/** The effect in another run that launched this one. */
|
|
9
|
+
launchedBy: RunLaunchSource | null;
|
|
10
|
+
attributes: RunAttributes;
|
|
8
11
|
}
|
|
9
12
|
export interface ThreadRow {
|
|
10
13
|
threadId: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComposedRegistry } from '@swirls/looms/core';
|
|
1
|
+
import { type ComposedRegistry, type RunLauncher } from '@swirls/looms/core';
|
|
2
2
|
import type { RunIngress } from './ingress';
|
|
3
3
|
import { type RuntimeObserver } from './observer';
|
|
4
4
|
import type { RuntimeRepository } from './repository';
|
|
@@ -12,5 +12,6 @@ export interface CancellationOptions {
|
|
|
12
12
|
readonly observer?: RuntimeObserver;
|
|
13
13
|
readonly wake: LoomsRuntime['wake'];
|
|
14
14
|
readonly withRunIngress: RunIngress;
|
|
15
|
+
readonly launcher: () => RunLauncher;
|
|
15
16
|
}
|
|
16
17
|
export declare function createCancellation(options: CancellationOptions): LoomsRuntime['cancel'];
|
|
@@ -75,6 +75,32 @@ export function createCancellation(options) {
|
|
|
75
75
|
const store = yield* EventStoreTag;
|
|
76
76
|
const now = yield* Clock.currentTimeMillis;
|
|
77
77
|
return yield* repository.commit(store, runId, (current) => {
|
|
78
|
+
const rootCancel = threadId === undefined || threadId === current.state.rootThreadId;
|
|
79
|
+
const deferClose = rootCancel &&
|
|
80
|
+
!current.state.childCloseSent &&
|
|
81
|
+
current.state.launchedRuns.some((child) => child.close === 'wait');
|
|
82
|
+
if (deferClose) {
|
|
83
|
+
const requested = createEvent(runId, {
|
|
84
|
+
type: 'runtime.child.close.requested',
|
|
85
|
+
payload: { policy: 'wait' },
|
|
86
|
+
threadId: current.state.rootThreadId,
|
|
87
|
+
origin: { type: 'system' },
|
|
88
|
+
idempotencyKey: `child-close:${current.state.rootThreadId ?? runId}`,
|
|
89
|
+
});
|
|
90
|
+
return Effect.succeed({
|
|
91
|
+
events: [requested],
|
|
92
|
+
value: {
|
|
93
|
+
plan: {
|
|
94
|
+
events: [],
|
|
95
|
+
threadIds: new Set(),
|
|
96
|
+
workerCancellations: [],
|
|
97
|
+
},
|
|
98
|
+
now,
|
|
99
|
+
childRunIds: current.state.launchedRuns.map((child) => child.runId),
|
|
100
|
+
},
|
|
101
|
+
idempotencyKey: `signal:child-close:${current.state.rootThreadId ?? runId}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
78
104
|
const plan = planCancellation({
|
|
79
105
|
runId,
|
|
80
106
|
seq: current.seq,
|
|
@@ -84,14 +110,21 @@ export function createCancellation(options) {
|
|
|
84
110
|
worker,
|
|
85
111
|
now,
|
|
86
112
|
});
|
|
113
|
+
const childRunIds = [];
|
|
87
114
|
return Effect.succeed({
|
|
88
115
|
events: plan.events,
|
|
89
|
-
value: { plan, now },
|
|
116
|
+
value: { plan, now, childRunIds },
|
|
90
117
|
idempotencyKey: `signal:cancel:${threadId ?? current.state.rootThreadId ?? ''}`,
|
|
91
118
|
});
|
|
92
119
|
});
|
|
93
120
|
}));
|
|
94
|
-
const { plan, now } = committed.value;
|
|
121
|
+
const { plan, now, childRunIds } = committed.value;
|
|
122
|
+
if (childRunIds.length > 0) {
|
|
123
|
+
yield* Effect.forEach(childRunIds, (childRunId) => options.launcher().cancel(childRunId), {
|
|
124
|
+
concurrency: 1,
|
|
125
|
+
discard: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
95
128
|
if (committed.append) {
|
|
96
129
|
executionContexts.abort(runId, plan.threadIds, 'Effect cancelled');
|
|
97
130
|
for (const { item, execution } of plan.workerCancellations) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { type EventInput, type RunLauncher, type RunState } from '@swirls/looms/core';
|
|
3
|
+
/** Tell the launching run that this child has reached a terminal status. */
|
|
4
|
+
export declare function notifyLaunchingParent(launcher: RunLauncher, runId: string, state: RunState): Effect.Effect<void, Error>;
|
|
5
|
+
/**
|
|
6
|
+
* When a run asked to wait for child cancellation and those children are gone, cancel the
|
|
7
|
+
* run's own threads. The caller appends these events from the parent wake, which must not
|
|
8
|
+
* call `runtime.cancel` on itself.
|
|
9
|
+
*/
|
|
10
|
+
export declare function childCloseCancellations(state: RunState): EventInput[];
|