@swirls/looms 0.4.0 → 0.5.0

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.
Files changed (241) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/actor/cell.d.ts +21 -2
  3. package/dist/actor/cell.js +15 -3
  4. package/dist/actor/host.d.ts +10 -2
  5. package/dist/actor/host.js +27 -4
  6. package/dist/actor/index.d.ts +2 -1
  7. package/dist/actor/index.js +1 -1
  8. package/dist/actor/routing.d.ts +3 -0
  9. package/dist/actor/routing.js +13 -2
  10. package/dist/agent/definitions-store.d.ts +1 -1
  11. package/dist/agent/definitions-store.js +1 -1
  12. package/dist/agent/definitions.d.ts +48 -0
  13. package/dist/agent/definitions.js +44 -0
  14. package/dist/agent/effects.d.ts +184 -0
  15. package/dist/agent/effects.js +172 -40
  16. package/dist/agent/events.d.ts +89 -0
  17. package/dist/agent/events.js +25 -0
  18. package/dist/agent/module.d.ts +412 -0
  19. package/dist/agent/module.js +5 -1
  20. package/dist/agent/scope.d.ts +88 -0
  21. package/dist/agent/threads.js +39 -3
  22. package/dist/agent/types.d.ts +4 -0
  23. package/dist/approval/effects.d.ts +40 -0
  24. package/dist/approval/module.d.ts +80 -0
  25. package/dist/client/client.d.ts +468 -119
  26. package/dist/client/client.js +214 -21
  27. package/dist/client/index.d.ts +1 -1
  28. package/dist/client/sse.d.ts +1 -1
  29. package/dist/client/sse.js +2 -2
  30. package/dist/cloudflare/durable-object.d.ts +66 -2
  31. package/dist/cloudflare/durable-object.js +96 -1
  32. package/dist/cloudflare/event-store.d.ts +7 -3
  33. package/dist/cloudflare/event-store.js +16 -3
  34. package/dist/cloudflare/index.d.ts +7 -1
  35. package/dist/cloudflare/index.js +7 -1
  36. package/dist/cloudflare/memory-queue.d.ts +18 -0
  37. package/dist/cloudflare/memory-queue.js +53 -0
  38. package/dist/cloudflare/queue-consumer.d.ts +52 -0
  39. package/dist/cloudflare/queue-consumer.js +102 -0
  40. package/dist/cloudflare/queue-message.d.ts +31 -0
  41. package/dist/cloudflare/queue-message.js +33 -0
  42. package/dist/cloudflare/queue-pull.d.ts +62 -0
  43. package/dist/cloudflare/queue-pull.js +130 -0
  44. package/dist/cloudflare/queue-worker.d.ts +39 -0
  45. package/dist/cloudflare/queue-worker.js +62 -0
  46. package/dist/cloudflare/r2-blob-store.d.ts +33 -0
  47. package/dist/cloudflare/r2-blob-store.js +60 -0
  48. package/dist/cloudflare/router.d.ts +4 -1
  49. package/dist/cloudflare/router.js +8 -3
  50. package/dist/cloudflare/websocket.d.ts +27 -0
  51. package/dist/cloudflare/websocket.js +215 -0
  52. package/dist/core/blob-offload.d.ts +21 -0
  53. package/dist/core/blob-offload.js +143 -0
  54. package/dist/core/blob-store.d.ts +45 -0
  55. package/dist/core/blob-store.js +164 -0
  56. package/dist/core/blobs.d.ts +37 -0
  57. package/dist/core/blobs.js +160 -0
  58. package/dist/core/definition-resolver.d.ts +40 -0
  59. package/dist/core/definition-resolver.js +129 -0
  60. package/dist/core/definition-store.d.ts +6 -0
  61. package/dist/core/definition-store.js +37 -1
  62. package/dist/core/effects.d.ts +86 -3
  63. package/dist/core/effects.js +99 -3
  64. package/dist/core/envelope.d.ts +9 -0
  65. package/dist/core/envelope.js +4 -0
  66. package/dist/core/event-codec.js +3 -0
  67. package/dist/core/fold.js +83 -1
  68. package/dist/core/ids.d.ts +2 -0
  69. package/dist/core/ids.js +4 -0
  70. package/dist/core/index.d.ts +8 -0
  71. package/dist/core/index.js +8 -0
  72. package/dist/core/launcher.d.ts +97 -0
  73. package/dist/core/launcher.js +67 -0
  74. package/dist/core/module-scope.d.ts +7 -3
  75. package/dist/core/module-scope.js +3 -0
  76. package/dist/core/module.d.ts +10 -0
  77. package/dist/core/projection.js +4 -0
  78. package/dist/core/protocol.d.ts +36 -1
  79. package/dist/core/protocol.js +35 -0
  80. package/dist/core/published.d.ts +2 -0
  81. package/dist/core/published.js +4 -1
  82. package/dist/core/replay.d.ts +96 -0
  83. package/dist/core/run-context.d.ts +33 -0
  84. package/dist/core/run-context.js +23 -0
  85. package/dist/core/sqlite-store.js +57 -5
  86. package/dist/core/state.d.ts +60 -0
  87. package/dist/core/state.js +19 -0
  88. package/dist/core/store.d.ts +6 -0
  89. package/dist/core/store.js +7 -0
  90. package/dist/core/subscription.d.ts +97 -0
  91. package/dist/core/subscription.js +224 -0
  92. package/dist/core/thread.d.ts +7 -0
  93. package/dist/core/thread.js +1 -0
  94. package/dist/core/trace.d.ts +21 -0
  95. package/dist/core/trace.js +45 -0
  96. package/dist/core/worker-callback.d.ts +25 -1
  97. package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
  98. package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
  99. package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
  100. package/dist/debugger/app/index.html +2 -2
  101. package/dist/debugger/components/event-inspector.d.ts +3 -1
  102. package/dist/debugger/components/event-inspector.js +25 -4
  103. package/dist/debugger/components/event-stream.d.ts +4 -1
  104. package/dist/debugger/components/event-stream.js +2 -2
  105. package/dist/debugger/components/run-tree.d.ts +5 -2
  106. package/dist/debugger/components/run-tree.js +10 -4
  107. package/dist/debugger/context.d.ts +8 -1
  108. package/dist/debugger/context.js +15 -3
  109. package/dist/debugger/events/protocol-summarize.js +18 -3
  110. package/dist/debugger/index.d.ts +1 -1
  111. package/dist/debugger/index.js +1 -1
  112. package/dist/debugger/shell/debugger-shell.js +1 -1
  113. package/dist/debugger/shell/events-pane.js +4 -3
  114. package/dist/debugger/styles.css +1 -0
  115. package/dist/evaluator/definitions-store.d.ts +3 -4
  116. package/dist/evaluator/definitions-store.js +3 -3
  117. package/dist/evaluator/definitions.d.ts +4 -1
  118. package/dist/evaluator/definitions.js +5 -2
  119. package/dist/evaluator/effects.d.ts +74 -33
  120. package/dist/evaluator/effects.js +6 -2
  121. package/dist/evaluator/module.d.ts +150 -66
  122. package/dist/evaluator/module.js +7 -3
  123. package/dist/evaluator/threads.d.ts +2 -0
  124. package/dist/evaluator/threads.js +4 -0
  125. package/dist/projectors/event-log.d.ts +87 -0
  126. package/dist/projectors/event-log.js +396 -0
  127. package/dist/projectors/index-model.d.ts +12 -0
  128. package/dist/projectors/index-model.js +12 -3
  129. package/dist/projectors/memory.js +7 -0
  130. package/dist/projectors/postgres.d.ts +1 -0
  131. package/dist/projectors/postgres.js +32 -15
  132. package/dist/projectors/schema.d.ts +5 -0
  133. package/dist/projectors/schema.js +26 -1
  134. package/dist/projectors/sqlite.js +47 -19
  135. package/dist/projectors/with-projectors.d.ts +7 -1
  136. package/dist/projectors/with-projectors.js +13 -3
  137. package/dist/react/materialize.js +5 -1
  138. package/dist/react/react.d.ts +10 -2
  139. package/dist/react/react.js +43 -19
  140. package/dist/react/store.d.ts +11 -2
  141. package/dist/react/store.js +104 -12
  142. package/dist/react/tables.d.ts +4 -1
  143. package/dist/runtime/cancellation.d.ts +2 -1
  144. package/dist/runtime/cancellation.js +35 -2
  145. package/dist/runtime/child-run.d.ts +10 -0
  146. package/dist/runtime/child-run.js +58 -0
  147. package/dist/runtime/deadline-transition-planner.js +11 -2
  148. package/dist/runtime/dispatch-effect.d.ts +2 -2
  149. package/dist/runtime/dispatch-effect.js +43 -19
  150. package/dist/runtime/errors.d.ts +14 -1
  151. package/dist/runtime/errors.js +10 -0
  152. package/dist/runtime/filesystem-blob-store.d.ts +6 -0
  153. package/dist/runtime/filesystem-blob-store.js +59 -0
  154. package/dist/runtime/helpers.d.ts +15 -6
  155. package/dist/runtime/helpers.js +46 -6
  156. package/dist/runtime/index.d.ts +4 -2
  157. package/dist/runtime/index.js +3 -1
  158. package/dist/runtime/local-effect-executor.d.ts +11 -2
  159. package/dist/runtime/local-effect-executor.js +80 -4
  160. package/dist/runtime/looms.d.ts +38 -4
  161. package/dist/runtime/looms.js +17 -3
  162. package/dist/runtime/repository.d.ts +5 -4
  163. package/dist/runtime/repository.js +5 -2
  164. package/dist/runtime/retry-policy.d.ts +5 -0
  165. package/dist/runtime/retry-policy.js +10 -2
  166. package/dist/runtime/run-start.d.ts +1 -1
  167. package/dist/runtime/run-start.js +10 -0
  168. package/dist/runtime/runtime.d.ts +2 -2
  169. package/dist/runtime/runtime.js +191 -68
  170. package/dist/runtime/server/auth.d.ts +3 -0
  171. package/dist/runtime/server/fetch-handler.d.ts +2 -2
  172. package/dist/runtime/server/fetch-handler.js +35 -4
  173. package/dist/runtime/server/grant.d.ts +49 -0
  174. package/dist/runtime/server/grant.js +177 -0
  175. package/dist/runtime/server/routes/effects.js +47 -5
  176. package/dist/runtime/server/routes/runs.js +6 -1
  177. package/dist/runtime/server/run-route.d.ts +18 -0
  178. package/dist/runtime/server/run-route.js +26 -0
  179. package/dist/runtime/server.d.ts +3 -1
  180. package/dist/runtime/server.js +3 -1
  181. package/dist/runtime/sse.js +26 -4
  182. package/dist/runtime/subscription.d.ts +8 -0
  183. package/dist/runtime/subscription.js +23 -0
  184. package/dist/runtime/types.d.ts +87 -8
  185. package/dist/runtime/wake-engine.d.ts +4 -2
  186. package/dist/runtime/wake-engine.js +57 -6
  187. package/dist/runtime/worker-callbacks.d.ts +12 -2
  188. package/dist/runtime/worker-callbacks.js +102 -72
  189. package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
  190. package/dist/runtime/worker-effect-dispatch.js +84 -18
  191. package/dist/schedule/debugger/index.d.ts +4 -0
  192. package/dist/schedule/debugger/index.js +11 -0
  193. package/dist/schedule/debugger/schedule-view.d.ts +3 -0
  194. package/dist/schedule/debugger/schedule-view.js +68 -0
  195. package/dist/schedule/debugger/summarize.d.ts +2 -0
  196. package/dist/schedule/debugger/summarize.js +43 -0
  197. package/dist/schedule/definitions-store.d.ts +10 -0
  198. package/dist/schedule/definitions-store.js +8 -0
  199. package/dist/schedule/definitions.d.ts +57 -0
  200. package/dist/schedule/definitions.js +30 -0
  201. package/dist/schedule/effects.d.ts +756 -0
  202. package/dist/schedule/effects.js +244 -0
  203. package/dist/schedule/ensure.d.ts +55 -0
  204. package/dist/schedule/ensure.js +109 -0
  205. package/dist/schedule/events.d.ts +116 -0
  206. package/dist/schedule/events.js +49 -0
  207. package/dist/schedule/index.d.ts +11 -0
  208. package/dist/schedule/index.js +11 -0
  209. package/dist/schedule/module.d.ts +1474 -0
  210. package/dist/schedule/module.js +22 -0
  211. package/dist/schedule/projections.d.ts +55 -0
  212. package/dist/schedule/projections.js +88 -0
  213. package/dist/schedule/scope.d.ts +121 -0
  214. package/dist/schedule/scope.js +7 -0
  215. package/dist/schedule/signals.d.ts +25 -0
  216. package/dist/schedule/signals.js +33 -0
  217. package/dist/schedule/specs.d.ts +59 -0
  218. package/dist/schedule/specs.js +231 -0
  219. package/dist/schedule/threads.d.ts +25 -0
  220. package/dist/schedule/threads.js +259 -0
  221. package/dist/testing/corpus-module.d.ts +34 -0
  222. package/dist/testing/corpus-module.js +87 -0
  223. package/dist/testing/index.d.ts +2 -0
  224. package/dist/testing/index.js +5 -6
  225. package/dist/testing/replay-corpus.d.ts +22 -0
  226. package/dist/testing/replay-corpus.js +50 -0
  227. package/dist/workflow/definitions-store.d.ts +1 -1
  228. package/dist/workflow/definitions-store.js +1 -1
  229. package/dist/workflow/definitions.d.ts +14 -0
  230. package/dist/workflow/effects.d.ts +122 -0
  231. package/dist/workflow/effects.js +16 -6
  232. package/dist/workflow/events.d.ts +42 -0
  233. package/dist/workflow/events.js +4 -0
  234. package/dist/workflow/module.d.ts +345 -32
  235. package/dist/workflow/module.js +7 -3
  236. package/dist/workflow/scope.d.ts +42 -0
  237. package/dist/workflow/threads.d.ts +72 -0
  238. package/dist/workflow/threads.js +106 -38
  239. package/package.json +10 -1
  240. package/dist/debugger/app/assets/index-D--9esm9.js +0 -22
  241. 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 { SQLITE_SCHEMA_SQL } from './schema.js';
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 (run_id, actor_id, kind, status, definition_name, parent_actor_id, updated_at)
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, actor_id, kind, status, definition_name, parent_actor_id, updated_at
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
- .append(runId, events, appendOptions)
34
- .pipe(Effect.tap((result) => projectors.length === 0 ? Effect.void : project(runId, result.sequences))),
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;
@@ -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. */
@@ -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(endpoint, runId) {
10
- return `${endpoint}\0${runId}`;
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, endpoint, reconnectDelayMs, coalesce) {
24
- const key = registryKey(endpoint, runId);
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, endpoint, reconnectDelayMs, coalesce) {
43
- const key = registryKey(endpoint, runId);
44
- peekStore(runId, endpoint, reconnectDelayMs, coalesce);
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, endpoint) {
56
- const key = registryKey(endpoint, runId);
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 store = peekStore(runId, endpoint, reconnectDelayMs, coalesce);
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, endpoint, reconnectDelayMs, coalesce);
84
- return () => releaseStore(runId, endpoint);
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
  }, []);
@@ -1,4 +1,5 @@
1
- import { type AnyEventEnvelope, type EventInput, type ProjectionDefinition, type RegisteredEvent } from '@swirls/looms/core';
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 reconnects. */
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?: {
@@ -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, eventsFromSseData } from './sse.js';
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 applyEncoded = (data) => {
92
- applyBatch(eventsFromSseData(data, options.storeId));
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
- return `${endpoint}/runs/${encodeURIComponent(options.storeId)}/events?live=true&fromSeq=${fromSeq}`;
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 = (Ctor) => {
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
- startEventSource(Ctor);
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
- applyEncoded(msg.data);
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(Ctor);
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
  };
@@ -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[];