@swirls/looms 0.3.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 (253) hide show
  1. package/CHANGELOG.md +64 -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/debugger/agent-chat.js +3 -30
  11. package/dist/agent/debugger/streaming-text.d.ts +6 -0
  12. package/dist/agent/debugger/streaming-text.js +33 -0
  13. package/dist/agent/definitions-store.d.ts +1 -1
  14. package/dist/agent/definitions-store.js +1 -1
  15. package/dist/agent/definitions.d.ts +48 -0
  16. package/dist/agent/definitions.js +44 -0
  17. package/dist/agent/effects.d.ts +184 -0
  18. package/dist/agent/effects.js +172 -40
  19. package/dist/agent/events.d.ts +89 -0
  20. package/dist/agent/events.js +25 -0
  21. package/dist/agent/module.d.ts +819 -407
  22. package/dist/agent/module.js +5 -1
  23. package/dist/agent/scope.d.ts +88 -0
  24. package/dist/agent/threads.js +39 -3
  25. package/dist/agent/types.d.ts +4 -0
  26. package/dist/approval/effects.d.ts +40 -0
  27. package/dist/approval/module.d.ts +80 -0
  28. package/dist/client/client.d.ts +468 -119
  29. package/dist/client/client.js +214 -21
  30. package/dist/client/index.d.ts +1 -1
  31. package/dist/client/sse.d.ts +1 -1
  32. package/dist/client/sse.js +2 -2
  33. package/dist/cloudflare/durable-object.d.ts +66 -2
  34. package/dist/cloudflare/durable-object.js +96 -1
  35. package/dist/cloudflare/event-store.d.ts +7 -3
  36. package/dist/cloudflare/event-store.js +16 -3
  37. package/dist/cloudflare/index.d.ts +7 -1
  38. package/dist/cloudflare/index.js +7 -1
  39. package/dist/cloudflare/memory-queue.d.ts +18 -0
  40. package/dist/cloudflare/memory-queue.js +53 -0
  41. package/dist/cloudflare/queue-consumer.d.ts +52 -0
  42. package/dist/cloudflare/queue-consumer.js +102 -0
  43. package/dist/cloudflare/queue-message.d.ts +31 -0
  44. package/dist/cloudflare/queue-message.js +33 -0
  45. package/dist/cloudflare/queue-pull.d.ts +62 -0
  46. package/dist/cloudflare/queue-pull.js +130 -0
  47. package/dist/cloudflare/queue-worker.d.ts +39 -0
  48. package/dist/cloudflare/queue-worker.js +62 -0
  49. package/dist/cloudflare/r2-blob-store.d.ts +33 -0
  50. package/dist/cloudflare/r2-blob-store.js +60 -0
  51. package/dist/cloudflare/router.d.ts +4 -1
  52. package/dist/cloudflare/router.js +8 -3
  53. package/dist/cloudflare/websocket.d.ts +27 -0
  54. package/dist/cloudflare/websocket.js +215 -0
  55. package/dist/core/attempts.d.ts +8 -0
  56. package/dist/core/attempts.js +27 -0
  57. package/dist/core/blob-offload.d.ts +21 -0
  58. package/dist/core/blob-offload.js +143 -0
  59. package/dist/core/blob-store.d.ts +45 -0
  60. package/dist/core/blob-store.js +164 -0
  61. package/dist/core/blobs.d.ts +37 -0
  62. package/dist/core/blobs.js +160 -0
  63. package/dist/core/definition-resolver.d.ts +40 -0
  64. package/dist/core/definition-resolver.js +129 -0
  65. package/dist/core/definition-store.d.ts +6 -0
  66. package/dist/core/definition-store.js +37 -1
  67. package/dist/core/effects.d.ts +86 -3
  68. package/dist/core/effects.js +99 -3
  69. package/dist/core/envelope.d.ts +9 -0
  70. package/dist/core/envelope.js +4 -0
  71. package/dist/core/event-codec.js +3 -0
  72. package/dist/core/fold.js +83 -1
  73. package/dist/core/ids.d.ts +2 -0
  74. package/dist/core/ids.js +4 -0
  75. package/dist/core/index.d.ts +10 -0
  76. package/dist/core/index.js +9 -0
  77. package/dist/core/launcher.d.ts +97 -0
  78. package/dist/core/launcher.js +67 -0
  79. package/dist/core/module-scope.d.ts +7 -3
  80. package/dist/core/module-scope.js +3 -0
  81. package/dist/core/module.d.ts +10 -0
  82. package/dist/core/projection.d.ts +5 -4
  83. package/dist/core/projection.js +4 -0
  84. package/dist/core/protocol.d.ts +36 -1
  85. package/dist/core/protocol.js +35 -0
  86. package/dist/core/published.d.ts +2 -0
  87. package/dist/core/published.js +4 -1
  88. package/dist/{react → core}/register.d.ts +9 -8
  89. package/dist/core/replay.d.ts +96 -0
  90. package/dist/core/run-context.d.ts +33 -0
  91. package/dist/core/run-context.js +23 -0
  92. package/dist/core/sqlite-store.js +57 -5
  93. package/dist/core/state.d.ts +60 -0
  94. package/dist/core/state.js +19 -0
  95. package/dist/core/store.d.ts +6 -0
  96. package/dist/core/store.js +7 -0
  97. package/dist/core/subscription.d.ts +97 -0
  98. package/dist/core/subscription.js +224 -0
  99. package/dist/core/thread.d.ts +7 -0
  100. package/dist/core/thread.js +1 -0
  101. package/dist/core/trace.d.ts +21 -0
  102. package/dist/core/trace.js +45 -0
  103. package/dist/core/worker-callback.d.ts +25 -1
  104. package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
  105. package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
  106. package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
  107. package/dist/debugger/app/index.html +2 -2
  108. package/dist/debugger/components/event-inspector.d.ts +3 -1
  109. package/dist/debugger/components/event-inspector.js +25 -4
  110. package/dist/debugger/components/event-stream.d.ts +4 -1
  111. package/dist/debugger/components/event-stream.js +2 -2
  112. package/dist/debugger/components/run-tree.d.ts +5 -2
  113. package/dist/debugger/components/run-tree.js +10 -4
  114. package/dist/debugger/context.d.ts +8 -1
  115. package/dist/debugger/context.js +15 -3
  116. package/dist/debugger/events/protocol-summarize.js +18 -3
  117. package/dist/debugger/index.d.ts +1 -1
  118. package/dist/debugger/index.js +1 -1
  119. package/dist/debugger/shell/debugger-shell.js +1 -1
  120. package/dist/debugger/shell/events-pane.js +4 -3
  121. package/dist/debugger/styles.css +1 -0
  122. package/dist/evaluator/definitions-store.d.ts +3 -4
  123. package/dist/evaluator/definitions-store.js +3 -3
  124. package/dist/evaluator/definitions.d.ts +4 -1
  125. package/dist/evaluator/definitions.js +5 -2
  126. package/dist/evaluator/effects.d.ts +41 -0
  127. package/dist/evaluator/effects.js +6 -2
  128. package/dist/evaluator/module.d.ts +84 -0
  129. package/dist/evaluator/module.js +7 -3
  130. package/dist/evaluator/threads.d.ts +2 -0
  131. package/dist/evaluator/threads.js +4 -0
  132. package/dist/projectors/event-log.d.ts +87 -0
  133. package/dist/projectors/event-log.js +396 -0
  134. package/dist/projectors/index-model.d.ts +12 -0
  135. package/dist/projectors/index-model.js +12 -3
  136. package/dist/projectors/memory.js +7 -0
  137. package/dist/projectors/postgres.d.ts +1 -0
  138. package/dist/projectors/postgres.js +32 -15
  139. package/dist/projectors/schema.d.ts +5 -0
  140. package/dist/projectors/schema.js +26 -1
  141. package/dist/projectors/sqlite.js +47 -19
  142. package/dist/projectors/with-projectors.d.ts +7 -1
  143. package/dist/projectors/with-projectors.js +13 -3
  144. package/dist/react/derived.d.ts +26 -8
  145. package/dist/react/derived.js +23 -26
  146. package/dist/react/index.d.ts +3 -3
  147. package/dist/react/index.js +2 -2
  148. package/dist/react/materialize.js +5 -1
  149. package/dist/react/react.d.ts +16 -9
  150. package/dist/react/react.js +47 -23
  151. package/dist/react/store.d.ts +14 -6
  152. package/dist/react/store.js +104 -12
  153. package/dist/react/tables.d.ts +4 -1
  154. package/dist/runtime/cancellation.d.ts +2 -1
  155. package/dist/runtime/cancellation.js +35 -2
  156. package/dist/runtime/child-run.d.ts +10 -0
  157. package/dist/runtime/child-run.js +58 -0
  158. package/dist/runtime/deadline-transition-planner.js +11 -2
  159. package/dist/runtime/dispatch-effect.d.ts +2 -2
  160. package/dist/runtime/dispatch-effect.js +43 -19
  161. package/dist/runtime/errors.d.ts +14 -1
  162. package/dist/runtime/errors.js +10 -0
  163. package/dist/runtime/filesystem-blob-store.d.ts +6 -0
  164. package/dist/runtime/filesystem-blob-store.js +59 -0
  165. package/dist/runtime/helpers.d.ts +15 -6
  166. package/dist/runtime/helpers.js +46 -6
  167. package/dist/runtime/index.d.ts +4 -2
  168. package/dist/runtime/index.js +3 -1
  169. package/dist/runtime/local-effect-executor.d.ts +11 -2
  170. package/dist/runtime/local-effect-executor.js +80 -4
  171. package/dist/runtime/looms.d.ts +38 -4
  172. package/dist/runtime/looms.js +17 -3
  173. package/dist/runtime/repository.d.ts +5 -4
  174. package/dist/runtime/repository.js +5 -2
  175. package/dist/runtime/retry-policy.d.ts +5 -0
  176. package/dist/runtime/retry-policy.js +10 -2
  177. package/dist/runtime/run-start.d.ts +1 -1
  178. package/dist/runtime/run-start.js +10 -0
  179. package/dist/runtime/runtime.d.ts +2 -2
  180. package/dist/runtime/runtime.js +191 -68
  181. package/dist/runtime/server/auth.d.ts +3 -0
  182. package/dist/runtime/server/fetch-handler.d.ts +2 -2
  183. package/dist/runtime/server/fetch-handler.js +35 -4
  184. package/dist/runtime/server/grant.d.ts +49 -0
  185. package/dist/runtime/server/grant.js +177 -0
  186. package/dist/runtime/server/routes/effects.js +47 -5
  187. package/dist/runtime/server/routes/runs.js +6 -1
  188. package/dist/runtime/server/run-route.d.ts +18 -0
  189. package/dist/runtime/server/run-route.js +26 -0
  190. package/dist/runtime/server.d.ts +3 -1
  191. package/dist/runtime/server.js +3 -1
  192. package/dist/runtime/sse.js +26 -4
  193. package/dist/runtime/subscription.d.ts +8 -0
  194. package/dist/runtime/subscription.js +23 -0
  195. package/dist/runtime/types.d.ts +87 -8
  196. package/dist/runtime/wake-engine.d.ts +4 -2
  197. package/dist/runtime/wake-engine.js +57 -6
  198. package/dist/runtime/worker-callbacks.d.ts +12 -2
  199. package/dist/runtime/worker-callbacks.js +102 -72
  200. package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
  201. package/dist/runtime/worker-effect-dispatch.js +84 -18
  202. package/dist/schedule/debugger/index.d.ts +4 -0
  203. package/dist/schedule/debugger/index.js +11 -0
  204. package/dist/schedule/debugger/schedule-view.d.ts +3 -0
  205. package/dist/schedule/debugger/schedule-view.js +68 -0
  206. package/dist/schedule/debugger/summarize.d.ts +2 -0
  207. package/dist/schedule/debugger/summarize.js +43 -0
  208. package/dist/schedule/definitions-store.d.ts +10 -0
  209. package/dist/schedule/definitions-store.js +8 -0
  210. package/dist/schedule/definitions.d.ts +57 -0
  211. package/dist/schedule/definitions.js +30 -0
  212. package/dist/schedule/effects.d.ts +756 -0
  213. package/dist/schedule/effects.js +244 -0
  214. package/dist/schedule/ensure.d.ts +55 -0
  215. package/dist/schedule/ensure.js +109 -0
  216. package/dist/schedule/events.d.ts +116 -0
  217. package/dist/schedule/events.js +49 -0
  218. package/dist/schedule/index.d.ts +11 -0
  219. package/dist/schedule/index.js +11 -0
  220. package/dist/schedule/module.d.ts +1474 -0
  221. package/dist/schedule/module.js +22 -0
  222. package/dist/schedule/projections.d.ts +55 -0
  223. package/dist/schedule/projections.js +88 -0
  224. package/dist/schedule/scope.d.ts +121 -0
  225. package/dist/schedule/scope.js +7 -0
  226. package/dist/schedule/signals.d.ts +25 -0
  227. package/dist/schedule/signals.js +33 -0
  228. package/dist/schedule/specs.d.ts +59 -0
  229. package/dist/schedule/specs.js +231 -0
  230. package/dist/schedule/threads.d.ts +25 -0
  231. package/dist/schedule/threads.js +259 -0
  232. package/dist/testing/corpus-module.d.ts +34 -0
  233. package/dist/testing/corpus-module.js +87 -0
  234. package/dist/testing/index.d.ts +2 -0
  235. package/dist/testing/index.js +5 -6
  236. package/dist/testing/replay-corpus.d.ts +22 -0
  237. package/dist/testing/replay-corpus.js +50 -0
  238. package/dist/workflow/definitions-store.d.ts +1 -1
  239. package/dist/workflow/definitions-store.js +1 -1
  240. package/dist/workflow/definitions.d.ts +14 -0
  241. package/dist/workflow/effects.d.ts +122 -0
  242. package/dist/workflow/effects.js +16 -6
  243. package/dist/workflow/events.d.ts +42 -0
  244. package/dist/workflow/events.js +4 -0
  245. package/dist/workflow/module.d.ts +345 -32
  246. package/dist/workflow/module.js +7 -3
  247. package/dist/workflow/scope.d.ts +42 -0
  248. package/dist/workflow/threads.d.ts +72 -0
  249. package/dist/workflow/threads.js +106 -38
  250. package/package.json +11 -2
  251. package/dist/debugger/app/assets/index-BevQYa-U.js +0 -22
  252. package/dist/debugger/app/assets/routes-B17Om93i.js +0 -47
  253. /package/dist/{react → core}/register.js +0 -0
@@ -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,19 +1,37 @@
1
- import { type EventEnvelope, type ProjectionDefinition } from '@swirls/looms/core';
2
- import type { AnyEventEnvelope, RegisteredEvent } from './register';
3
- export type { AnyEventEnvelope } from './register';
1
+ import { type AnyEventEnvelope, type EventEnvelope, type InferSchemaOutput, type ProjectionDefinition, type RegisteredEvent, type SchemaInput } from '@swirls/looms/core';
4
2
  export { EventIndex, eventThreadKey } from '@swirls/looms/core';
5
- export interface EventFoldDefinition<S, TEvent extends AnyEventEnvelope = RegisteredEvent> {
3
+ export interface LiveFoldDefinition<S, TEvent extends AnyEventEnvelope = RegisteredEvent> {
4
+ readonly kind: 'liveFold';
6
5
  readonly name?: string;
6
+ readonly shape?: unknown;
7
7
  readonly initialState: S;
8
8
  reduce(state: S, event: TEvent): S;
9
- readonly includeEphemeral?: boolean;
10
9
  }
11
- export declare function createFold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(def: EventFoldDefinition<S, TEvent>): EventFoldDefinition<S, TEvent>;
12
- export declare function foldEvents<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: EventFoldDefinition<S, TEvent>, events: readonly TEvent[], from?: S): S;
10
+ /**
11
+ * Client-side fold that sees ephemeral events, which projections skip. Ephemeral events from an
12
+ * effect attempt that the runtime later retried never reach the reducer. The reducer still owns
13
+ * resets on domain events, such as the durable message that replaces streamed text. Use a
14
+ * projection for anything else.
15
+ */
16
+ export declare function defineLiveFold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(def: {
17
+ readonly name?: string;
18
+ readonly shape?: undefined;
19
+ readonly initialState: S;
20
+ reduce(state: S, event: TEvent): S;
21
+ }): LiveFoldDefinition<S, TEvent>;
22
+ export declare function defineLiveFold<TShape extends SchemaInput, TEvent extends AnyEventEnvelope = RegisteredEvent>(def: {
23
+ readonly name?: string;
24
+ readonly shape: TShape;
25
+ readonly initialState: InferSchemaOutput<TShape>;
26
+ reduce(state: NoInfer<InferSchemaOutput<TShape>>, event: TEvent): InferSchemaOutput<TShape>;
27
+ }): LiveFoldDefinition<InferSchemaOutput<TShape>, TEvent>;
28
+ /** Fold events through a live fold, as `useLiveFold` does, including ephemeral events. */
29
+ export declare function foldLive<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: LiveFoldDefinition<S, TEvent>, events: readonly TEvent[]): S;
13
30
  export declare class ProjectionCache {
14
31
  private projectionCache;
15
32
  private foldCache;
16
33
  project<S>(definition: ProjectionDefinition<S>, events: readonly EventEnvelope[]): S;
17
- fold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: EventFoldDefinition<S, TEvent>, events: readonly TEvent[]): S;
34
+ /** A retry can supersede events already folded, so a batch containing one refolds from scratch. */
35
+ fold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: LiveFoldDefinition<S, TEvent>, events: readonly TEvent[]): S;
18
36
  clear(): void;
19
37
  }
@@ -1,37 +1,33 @@
1
- import { project } from '@swirls/looms/core';
1
+ import { dropSupersededAttempts, project, retriedEffectId, } from '@swirls/looms/core';
2
2
  export { EventIndex, eventThreadKey } from '@swirls/looms/core';
3
- export function createFold(def) {
4
- return def;
3
+ export function defineLiveFold(def) {
4
+ return { ...def, kind: 'liveFold' };
5
5
  }
6
- export function foldEvents(definition, events, from) {
7
- let state = from !== undefined ? from : definition.initialState;
8
- const includeEphemeral = definition.includeEphemeral ?? false;
9
- for (const event of events) {
10
- if (event.ephemeral && !includeEphemeral) {
11
- continue;
12
- }
6
+ function foldFrom(definition, events, from) {
7
+ let state = from;
8
+ for (const event of dropSupersededAttempts(events)) {
13
9
  state = definition.reduce(state, event);
14
10
  }
15
11
  return state;
16
12
  }
17
- function advanceCache(cache, definition, events, compute) {
18
- const total = events.length;
13
+ /** Fold events through a live fold, as `useLiveFold` does, including ephemeral events. */
14
+ export function foldLive(definition, events) {
15
+ return foldFrom(definition, events, definition.initialState);
16
+ }
17
+ function hasNoRetries(batch) {
18
+ return !batch.some((event) => retriedEffectId(event) !== undefined);
19
+ }
20
+ /** Folds only the events appended since the last call, unless `canAppend` rejects them. */
21
+ function advanceCache(cache, definition, events, compute, canAppend = () => true) {
19
22
  const cached = cache.get(definition);
20
- if (!cached) {
21
- const state = compute(definition, events);
22
- cache.set(definition, { state, upTo: total });
23
- return state;
24
- }
25
- if (cached.upTo === total) {
23
+ if (cached?.upTo === events.length) {
26
24
  return cached.state;
27
25
  }
28
- if (cached.upTo < total) {
29
- const state = compute(definition, events.slice(cached.upTo), cached.state);
30
- cache.set(definition, { state, upTo: total });
31
- return state;
32
- }
33
- const state = compute(definition, events);
34
- cache.set(definition, { state, upTo: total });
26
+ const batch = cached && cached.upTo < events.length ? events.slice(cached.upTo) : undefined;
27
+ const state = cached && batch && canAppend(batch)
28
+ ? compute(definition, batch, cached.state)
29
+ : compute(definition, events, definition.initialState);
30
+ cache.set(definition, { state, upTo: events.length });
35
31
  return state;
36
32
  }
37
33
  export class ProjectionCache {
@@ -40,8 +36,9 @@ export class ProjectionCache {
40
36
  project(definition, events) {
41
37
  return advanceCache(this.projectionCache, definition, events, project);
42
38
  }
39
+ /** A retry can supersede events already folded, so a batch containing one refolds from scratch. */
43
40
  fold(definition, events) {
44
- return advanceCache(this.foldCache, definition, events, foldEvents);
41
+ return advanceCache(this.foldCache, definition, events, foldFrom, hasNoRetries);
45
42
  }
46
43
  clear() {
47
44
  this.projectionCache = new WeakMap();
@@ -1,6 +1,6 @@
1
1
  export { createLoomsStore, type LoomsClientStore, type LoomsClientStoreOptions, type LoomsConnectionStatus, type LoomsStoreQuery, type CoalesceOption, type StoreListener, } from './store';
2
- export { createFold, foldEvents, EventIndex, eventThreadKey, type EventFoldDefinition, } from './derived';
2
+ export { defineLiveFold, foldLive, EventIndex, eventThreadKey, type LiveFoldDefinition, } from './derived';
3
3
  export { materializeEvents } from './materialize';
4
4
  export { emptyTables, type MaterializedTables, type RunRow, type ThreadRow } from './tables';
5
- export type { LoomsRegister, RegisteredEvent, AnyEventEnvelope } from './register';
6
- export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useEventFold, type LoomsProviderProps, type UseRunStoreOptions, type RunSummary, } from './react';
5
+ export type { LoomsRegister, RegisteredEvent, AnyEventEnvelope } from '@swirls/looms/core';
6
+ export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useLiveFold, type LoomsProviderProps, type UseRunStoreOptions, type RunSummary, } from './react';
@@ -1,5 +1,5 @@
1
1
  export { createLoomsStore, } from './store.js';
2
- export { createFold, foldEvents, EventIndex, eventThreadKey, } from './derived.js';
2
+ export { defineLiveFold, foldLive, EventIndex, eventThreadKey, } from './derived.js';
3
3
  export { materializeEvents } from './materialize.js';
4
4
  export { emptyTables } from './tables.js';
5
- export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useEventFold, } from './react.js';
5
+ export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useLiveFold, } from './react.js';
@@ -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,23 +1,27 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type ProjectionDefinition, type RunStatus, type ThreadTree } from '@swirls/looms/core';
3
- import { createFold, foldEvents, type EventFoldDefinition } from './derived';
4
- import type { AnyEventEnvelope, RegisteredEvent } from './register';
2
+ import { type AnyEventEnvelope, type ProjectionDefinition, type RegisteredEvent, type RunAttributes, type RunLaunchSource, type RunStatus, type SubscriptionTransport, type ThreadTree } from '@swirls/looms/core';
3
+ import { defineLiveFold, foldLive, type LiveFoldDefinition } from './derived';
5
4
  import { createLoomsStore, type CoalesceOption, type LoomsClientStore, type LoomsConnectionStatus } from './store';
6
5
  export interface LoomsProviderProps {
7
6
  children: ReactNode;
8
7
  endpoint?: string;
9
8
  coalesce?: CoalesceOption;
9
+ transport?: SubscriptionTransport;
10
10
  }
11
11
  interface ProviderContextValue {
12
12
  endpoint: string;
13
13
  coalesce?: CoalesceOption;
14
+ transport?: SubscriptionTransport;
14
15
  }
15
16
  /** Root provider that supplies the Looms host endpoint to run stores. */
16
- 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>>;
17
18
  export interface UseRunStoreOptions {
18
19
  endpoint?: string;
19
20
  reconnectDelayMs?: number;
20
21
  coalesce?: CoalesceOption;
22
+ transport?: SubscriptionTransport;
23
+ threadId?: string;
24
+ includeEphemeral?: boolean;
21
25
  }
22
26
  /**
23
27
  * Acquires a non-subscribing handle to the run store.
@@ -39,6 +43,9 @@ export interface RunSummary {
39
43
  readonly definitionName: string | undefined;
40
44
  readonly rootThreadId: string | undefined;
41
45
  readonly startedAt: number | undefined;
46
+ /** The effect in another run that launched this one. */
47
+ readonly launchedBy: RunLaunchSource | undefined;
48
+ readonly attributes: RunAttributes;
42
49
  readonly connection: LoomsConnectionStatus;
43
50
  }
44
51
  /** Lightweight subscription to top-level run metadata and connection status. */
@@ -55,10 +62,10 @@ export declare function useEventCounts<TEvent extends AnyEventEnvelope = Registe
55
62
  export declare function useEventBySeq<TEvent extends AnyEventEnvelope = RegisteredEvent>(store: LoomsClientStore<TEvent>, seq: number | undefined): TEvent | undefined;
56
63
  /** Subscribe to the latest event of a given type. */
57
64
  export declare function useLatestEvent<TEvent extends AnyEventEnvelope = RegisteredEvent>(store: LoomsClientStore<TEvent>, type: string): TEvent | undefined;
58
- /** Subscribe to a custom incremental fold. */
59
- export declare function useEventFold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(store: LoomsClientStore<TEvent>, fold: EventFoldDefinition<S, TEvent>): S;
65
+ /** Subscribe to a live fold, which includes ephemeral events. */
66
+ export declare function useLiveFold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(store: LoomsClientStore<TEvent>, fold: LiveFoldDefinition<S, TEvent>): S;
60
67
  export { createLoomsStore };
61
- export { createFold, foldEvents };
68
+ export { defineLiveFold, foldLive };
62
69
  export type { LoomsClientStore, LoomsClientStoreOptions, LoomsConnectionStatus, LoomsStoreQuery, CoalesceOption, StoreListener, } from './store';
63
- export type { EventFoldDefinition } from './derived';
64
- export type { LoomsRegister, RegisteredEvent, AnyEventEnvelope } from './register';
70
+ export type { LiveFoldDefinition } from './derived';
71
+ export type { LoomsRegister, RegisteredEvent, AnyEventEnvelope } from '@swirls/looms/core';
@@ -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';
4
- import { createFold, foldEvents } from './derived.js';
3
+ import { isTerminalStatus, sameStringRecord, threadTree, toThreadTree, } from '@swirls/looms/core';
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
  }, []);
@@ -165,10 +189,10 @@ export function useLatestEvent(store, type) {
165
189
  const selector = useCallback((s) => s.query.latest(type), [type]);
166
190
  return useRunSelector(store, selector);
167
191
  }
168
- /** Subscribe to a custom incremental fold. */
169
- export function useEventFold(store, fold) {
192
+ /** Subscribe to a live fold, which includes ephemeral events. */
193
+ export function useLiveFold(store, fold) {
170
194
  const getSnapshot = useCallback(() => store.fold(fold), [store, fold]);
171
195
  return useStoreSnapshot(store, getSnapshot);
172
196
  }
173
197
  export { createLoomsStore };
174
- export { createFold, foldEvents };
198
+ export { defineLiveFold, foldLive };
@@ -1,7 +1,7 @@
1
- import { type EventInput, type ProjectionDefinition } from '@swirls/looms/core';
2
- import { type EventFoldDefinition } from './derived';
1
+ import type { SubscriptionSocketConstructor } from '@swirls/looms/client';
2
+ import { type AnyEventEnvelope, type EventInput, type ProjectionDefinition, type RegisteredEvent, type SubscriptionTransport } from '@swirls/looms/core';
3
+ import { type LiveFoldDefinition } from './derived';
3
4
  import { type CoalesceOption } from './notify';
4
- import type { AnyEventEnvelope, RegisteredEvent } from './register';
5
5
  import { type MaterializedTables } from './tables';
6
6
  export type { CoalesceOption } from './notify';
7
7
  /** Fired after the store version bumps. Re-read via `events()` / `getState()` / `query`. */
@@ -15,10 +15,18 @@ export interface LoomsClientStoreOptions {
15
15
  * Opens `${endpoint}/runs/:id/events?live=true` as an SSE stream.
16
16
  */
17
17
  endpoint: string;
18
- /** Backoff between SSE reconnects. */
18
+ /** Backoff between reconnects. The delay stays fixed, matching the SSE client. */
19
19
  reconnectDelayMs?: number;
20
20
  fetch?: typeof fetch;
21
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;
22
30
  }
23
31
  export interface LoomsStoreQuery<TEvent extends AnyEventEnvelope = RegisteredEvent> {
24
32
  events: (options?: {
@@ -30,7 +38,7 @@ export interface LoomsStoreQuery<TEvent extends AnyEventEnvelope = RegisteredEve
30
38
  bySeq: (seq: number) => TEvent | undefined;
31
39
  startedAt: () => number | undefined;
32
40
  latest: (type: string) => TEvent | undefined;
33
- fold: <S>(def: EventFoldDefinition<S, TEvent>) => S;
41
+ fold: <S>(def: LiveFoldDefinition<S, TEvent>) => S;
34
42
  }
35
43
  export interface LoomsClientStore<TEvent extends AnyEventEnvelope = RegisteredEvent> {
36
44
  storeId: string;
@@ -43,7 +51,7 @@ export interface LoomsClientStore<TEvent extends AnyEventEnvelope = RegisteredEv
43
51
  query: LoomsStoreQuery<TEvent>;
44
52
  subscribe: (listener: StoreListener) => () => void;
45
53
  project<S>(definition: ProjectionDefinition<S>): S;
46
- fold<S>(definition: EventFoldDefinition<S, TEvent>): S;
54
+ fold<S>(definition: LiveFoldDefinition<S, TEvent>): S;
47
55
  flush: () => void;
48
56
  commit: (event: EventInput | readonly EventInput[]) => Promise<void>;
49
57
  sync: () => Promise<void>;