@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,5 +1,5 @@
1
- import { Data, Effect, Option, Schema, Stream } from 'effect';
2
- import { createRunId, DefinitionCatalogSchema, EventEnvelopeSchema, ReplayStepSchema, RunStateSchema, RunSummariesSchema, stringifyJson, } from '@swirls/looms/core';
1
+ import { Data, Effect, Option, Predicate, Schema, Stream } from 'effect';
2
+ import { createRunId, DefinitionCatalogSchema, EventEnvelopeSchema, readSubscriptionData, ReplayStepSchema, RunStateSchema, RunSummariesSchema, SecretContextSchema, stringifyJson, traceHeaders, } from '@swirls/looms/core';
3
3
  import { consumeSseStreamEffect, eventsFromSseData, sseFrames } from './sse.js';
4
4
  class LoomsClientError extends Data.TaggedError('LoomsClientError') {
5
5
  constructor(message, cause) {
@@ -14,6 +14,46 @@ const StartResultSchema = Schema.Struct({
14
14
  state: RunStateSchema,
15
15
  });
16
16
  const RunResultSchema = Schema.Struct({ runId: Schema.String, state: RunStateSchema });
17
+ function attemptPath(runId, attempt) {
18
+ return `/runs/${encodeURIComponent(runId)}/effects/${encodeURIComponent(attempt.effectId)}/${attempt.attempt}`;
19
+ }
20
+ function workerCallbackBody(input) {
21
+ switch (input.kind) {
22
+ case 'complete':
23
+ return { events: input.events };
24
+ case 'fail': {
25
+ const body = { error: input.error };
26
+ if (input.nonRetryable !== undefined) {
27
+ body.nonRetryable = input.nonRetryable;
28
+ }
29
+ if (input.retryAfterMs !== undefined) {
30
+ body.retryAfterMs = input.retryAfterMs;
31
+ }
32
+ return body;
33
+ }
34
+ case 'heartbeat':
35
+ return input.details === undefined ? {} : { details: input.details };
36
+ case 'started':
37
+ case 'cancelled':
38
+ return {};
39
+ default: {
40
+ const exhaustive = input;
41
+ return exhaustive;
42
+ }
43
+ }
44
+ }
45
+ const WorkerCallbackResultSchema = Schema.Struct({
46
+ status: Schema.Union([
47
+ Schema.Literal('accepted'),
48
+ Schema.Literal('stale'),
49
+ Schema.Literal('cancelled'),
50
+ ]),
51
+ cancelRequested: Schema.Boolean,
52
+ });
53
+ const WorkerSecretsResultSchema = Schema.Union([
54
+ Schema.Struct({ status: Schema.Literal('accepted'), secrets: SecretContextSchema }),
55
+ Schema.Struct({ status: Schema.Union([Schema.Literal('stale'), Schema.Literal('cancelled')]) }),
56
+ ]);
17
57
  /** `appended` counts the events the run gained while the wake ran. */
18
58
  const WakeResultSchema = Schema.Struct({
19
59
  runId: Schema.String,
@@ -54,19 +94,33 @@ export function createLoomsClient(options = {}) {
54
94
  return yield* Schema.decodeUnknownEffect(schema)(body).pipe(Effect.mapError((cause) => new LoomsClientError('Invalid response body', cause)));
55
95
  }));
56
96
  }
57
- const subscribeEvents = (runId, onEvent, reconnectDelayMs = 1_000) => {
97
+ const subscribeEvents = (runId, onEvent, third = 1_000) => {
98
+ const settings = subscriptionSettings(third, options.grant);
58
99
  let fromSeq = 1;
59
100
  const controller = new AbortController();
60
101
  const run = Effect.gen(function* () {
61
102
  while (!controller.signal.aborted) {
62
103
  yield* Effect.gen(function* () {
104
+ if (useWebSocket(settings)) {
105
+ return yield* consumeWebSocket({
106
+ baseUrl,
107
+ runId,
108
+ settings,
109
+ signal: controller.signal,
110
+ currentSeq: () => fromSeq,
111
+ setSeq: (next) => {
112
+ fromSeq = next;
113
+ },
114
+ onEvent,
115
+ });
116
+ }
63
117
  const cursor = Math.max(0, fromSeq - 1);
64
118
  const headers = new Headers({ accept: 'text/event-stream' });
65
119
  if (cursor > 0) {
66
120
  headers.set('last-event-id', String(cursor));
67
121
  }
68
122
  const res = yield* Effect.tryPromise({
69
- try: (signal) => fetchImpl(`${baseUrl}/runs/${encodeURIComponent(runId)}/events?fromSeq=${fromSeq}`, {
123
+ try: (signal) => fetchImpl(`${baseUrl}${eventsQuery(runId, fromSeq, settings)}`, {
70
124
  headers,
71
125
  signal,
72
126
  }),
@@ -75,10 +129,20 @@ export function createLoomsClient(options = {}) {
75
129
  const body = res.body;
76
130
  if (res.ok && body) {
77
131
  return yield* consumeSseStreamEffect(body, (frame) => {
78
- for (const event of eventsFromSseData(frame.data, runId)) {
79
- onEvent(event);
80
- fromSeq = Math.max(fromSeq, event.seq + 1);
81
- }
132
+ let resync = false;
133
+ deliverSubscriptionData({
134
+ data: frame.data,
135
+ runId,
136
+ currentSeq: () => fromSeq,
137
+ setSeq: (next) => {
138
+ fromSeq = next;
139
+ },
140
+ onEvent,
141
+ onResync: () => {
142
+ resync = true;
143
+ },
144
+ });
145
+ return resync ? false : undefined;
82
146
  });
83
147
  }
84
148
  return yield* new LoomsClientError(res.ok ? 'event stream missing body' : `HTTP ${res.status}`);
@@ -86,7 +150,7 @@ export function createLoomsClient(options = {}) {
86
150
  if (controller.signal.aborted) {
87
151
  break;
88
152
  }
89
- yield* Effect.sleep(reconnectDelayMs);
153
+ yield* Effect.sleep(settings.reconnectDelayMs);
90
154
  }
91
155
  });
92
156
  void Effect.runPromise(run, { signal: controller.signal }).catch(() => undefined);
@@ -94,31 +158,43 @@ export function createLoomsClient(options = {}) {
94
158
  controller.abort();
95
159
  };
96
160
  };
97
- const startRun = (args) => request('/runs', StartResultSchema, { method: 'POST', body: stringifyJson(args) });
161
+ const startRun = (args) => {
162
+ const { trace, ...body } = args;
163
+ return request('/runs', StartResultSchema, {
164
+ method: 'POST',
165
+ headers: traceHeaders(trace),
166
+ body: stringifyJson(body),
167
+ });
168
+ };
169
+ const workerHeaders = options.workerCallbackToken
170
+ ? { authorization: `Bearer ${options.workerCallbackToken}` }
171
+ : undefined;
98
172
  const workerCallback = (runId, input) => {
99
- const body = input.kind === 'complete'
100
- ? { events: input.events }
101
- : input.kind === 'fail'
102
- ? { error: input.error }
103
- : {};
104
- return request(`/runs/${encodeURIComponent(runId)}/effects/${encodeURIComponent(input.effectId)}/${input.attempt}/${input.kind}`, RunResultSchema, {
173
+ const body = workerCallbackBody(input);
174
+ return request(`${attemptPath(runId, input)}/${input.kind}`, WorkerCallbackResultSchema, {
105
175
  method: 'POST',
106
- headers: options.workerCallbackToken
107
- ? { authorization: `Bearer ${options.workerCallbackToken}` }
108
- : undefined,
176
+ headers: workerHeaders,
109
177
  body: stringifyJson(body),
110
178
  });
111
179
  };
180
+ /** Read the run's secret context. Only the attempt that is currently running gets it. */
181
+ const workerSecrets = (runId, attempt) => request(`${attemptPath(runId, attempt)}/secrets`, WorkerSecretsResultSchema, {
182
+ method: 'POST',
183
+ headers: workerHeaders,
184
+ body: '{}',
185
+ });
112
186
  const streamRun = (args) => {
113
- const runId = args.runId ?? createRunId();
187
+ const { trace, ...body } = args;
188
+ const runId = body.runId ?? createRunId();
114
189
  const stream = Stream.unwrap(Effect.tryPromise({
115
190
  try: (signal) => fetchImpl(`${baseUrl}/runs`, {
116
191
  method: 'POST',
117
192
  headers: {
193
+ ...traceHeaders(trace),
118
194
  'content-type': 'application/json',
119
195
  accept: 'text/event-stream',
120
196
  },
121
- body: stringifyJson({ ...args, runId }),
197
+ body: stringifyJson({ ...body, runId }),
122
198
  signal,
123
199
  }),
124
200
  catch: (cause) => new LoomsClientError('Failed to start event stream', cause),
@@ -175,6 +251,7 @@ export function createLoomsClient(options = {}) {
175
251
  body: stringifyJson(forkOptions?.runId === undefined ? {} : { runId: forkOptions.runId }),
176
252
  }),
177
253
  workerCallback,
254
+ workerSecrets,
178
255
  listRunSummaries: () => request('/runs/summaries', RunSummariesSchema),
179
256
  listDefinitions: () => request('/definitions', DefinitionCatalogSchema),
180
257
  replayTo: (runId, seq) => request(`/runs/${encodeURIComponent(runId)}/replay?seq=${seq}`, ReplayResultSchema),
@@ -193,3 +270,119 @@ export function createLoomsClient(options = {}) {
193
270
  }),
194
271
  };
195
272
  }
273
+ function subscriptionSettings(third, grant) {
274
+ if (Predicate.isNumber(third)) {
275
+ return { transport: 'sse', reconnectDelayMs: third, grant };
276
+ }
277
+ return {
278
+ transport: third.transport ?? 'sse',
279
+ reconnectDelayMs: third.reconnectDelayMs ?? 1_000,
280
+ threadId: third.threadId,
281
+ includeEphemeral: third.includeEphemeral,
282
+ webSocket: third.webSocket,
283
+ grant,
284
+ };
285
+ }
286
+ function useWebSocket(settings) {
287
+ if (settings.transport !== 'websocket' && settings.transport !== 'auto') {
288
+ return false;
289
+ }
290
+ return settings.webSocket !== undefined || globalWebSocket() !== undefined;
291
+ }
292
+ function eventsQuery(runId, fromSeq, settings) {
293
+ const params = new URLSearchParams({ fromSeq: String(fromSeq) });
294
+ if (settings.threadId) {
295
+ params.set('threadId', settings.threadId);
296
+ }
297
+ if (settings.includeEphemeral === false) {
298
+ params.set('includeEphemeral', 'false');
299
+ }
300
+ if (settings.grant) {
301
+ params.set('grant', settings.grant);
302
+ }
303
+ return `/runs/${encodeURIComponent(runId)}/events?${params.toString()}`;
304
+ }
305
+ function webSocketUrl(baseUrl, path) {
306
+ const url = new URL(path, baseUrl);
307
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
308
+ return url.toString();
309
+ }
310
+ function globalWebSocket() {
311
+ if (!('WebSocket' in globalThis) || !Predicate.isFunction(globalThis.WebSocket)) {
312
+ return undefined;
313
+ }
314
+ return globalThis.WebSocket;
315
+ }
316
+ function consumeWebSocket(subscription) {
317
+ const Socket = subscription.settings.webSocket ?? globalWebSocket();
318
+ if (!Socket) {
319
+ return Effect.void;
320
+ }
321
+ const url = webSocketUrl(subscription.baseUrl, eventsQuery(subscription.runId, subscription.currentSeq(), subscription.settings));
322
+ return Effect.callback((resume) => {
323
+ const socket = new Socket(url);
324
+ const finish = () => {
325
+ resume(Effect.void);
326
+ };
327
+ if (subscription.signal.aborted) {
328
+ socket.close();
329
+ finish();
330
+ return Effect.void;
331
+ }
332
+ const onAbort = () => {
333
+ socket.close();
334
+ finish();
335
+ };
336
+ subscription.signal.addEventListener('abort', onAbort);
337
+ socket.addEventListener('message', (event) => {
338
+ if (!Predicate.isString(event.data)) {
339
+ return;
340
+ }
341
+ deliverSubscriptionData({
342
+ data: event.data,
343
+ runId: subscription.runId,
344
+ currentSeq: subscription.currentSeq,
345
+ setSeq: subscription.setSeq,
346
+ onEvent: subscription.onEvent,
347
+ onResync: () => {
348
+ socket.close();
349
+ },
350
+ });
351
+ });
352
+ socket.addEventListener('close', finish);
353
+ socket.addEventListener('error', finish);
354
+ return Effect.sync(() => {
355
+ subscription.signal.removeEventListener('abort', onAbort);
356
+ socket.close();
357
+ });
358
+ });
359
+ }
360
+ function deliverSubscriptionData(delivery) {
361
+ const read = readSubscriptionData(delivery.data, delivery.runId);
362
+ switch (read.kind) {
363
+ case 'events': {
364
+ const fromSeq = delivery.currentSeq();
365
+ let next = fromSeq;
366
+ for (const event of read.events) {
367
+ if (event.seq < fromSeq) {
368
+ continue;
369
+ }
370
+ delivery.onEvent(event);
371
+ next = Math.max(next, event.seq + 1);
372
+ }
373
+ delivery.setSeq(next);
374
+ return;
375
+ }
376
+ case 'resync':
377
+ delivery.setSeq(read.head);
378
+ delivery.onResync();
379
+ return;
380
+ case 'hello':
381
+ case 'ignore':
382
+ return;
383
+ default: {
384
+ const unexpected = read;
385
+ return unexpected;
386
+ }
387
+ }
388
+ }
@@ -1,2 +1,2 @@
1
- export { createLoomsClient, type LoomsClient, type LoomsClientOptions, type StartResult, type StreamHandle, } from './client';
1
+ export { createLoomsClient, type LoomsClient, type LoomsClientOptions, type StartResult, type StreamHandle, type SubscribeEventsOptions, type SubscriptionSocket, type SubscriptionSocketConstructor, } from './client';
2
2
  export { consumeSseStream, consumeSseStreamEffect, delay, eventsFromSseData, parseSseFrame, type SseFrame, } from './sse';
@@ -15,7 +15,7 @@ export declare class SseStreamError extends SseStreamError_base<{
15
15
  }> {
16
16
  constructor(cause: unknown);
17
17
  }
18
- export declare function consumeSseStreamEffect(body: ReadableStream<Uint8Array>, onFrame: (frame: SseFrame) => void): Effect.Effect<void, SseStreamError>;
18
+ export declare function consumeSseStreamEffect(body: ReadableStream<Uint8Array>, onFrame: (frame: SseFrame) => void | boolean): Effect.Effect<void, SseStreamError>;
19
19
  export declare function sseFrames(body: ReadableStream<Uint8Array>): Stream.Stream<SseFrame, SseStreamError>;
20
20
  export declare function consumeSseStream(body: ReadableStream<Uint8Array>, onFrame: (frame: SseFrame) => void, signal?: AbortSignal): Promise<void>;
21
21
  export declare function eventsFromSseData(data: string, runId: string): EventEnvelope[];
@@ -52,8 +52,8 @@ export function consumeSseStreamEffect(body, onFrame) {
52
52
  buffer = chunks.pop() ?? '';
53
53
  for (const chunk of chunks) {
54
54
  const frame = parseSseFrame(chunk);
55
- if (frame) {
56
- onFrame(frame);
55
+ if (frame && onFrame(frame) === false) {
56
+ return;
57
57
  }
58
58
  }
59
59
  }
@@ -1,10 +1,14 @@
1
1
  import { DurableObject } from 'cloudflare:workers';
2
- import type { AnyRuntimeModule, EventStoreTrimCoverage } from '@swirls/looms/core';
2
+ import { type AnyRuntimeModule, type DefinitionResolver, type EventInput, type BlobStore, type EventStoreTrimCoverage, type LaunchRunArgs, type RunLauncher, type RunState, type RunStatusSnapshot } from '@swirls/looms/core';
3
3
  import type { Projector } from '@swirls/looms/projectors';
4
- import type { Authorize } from '@swirls/looms/runtime';
4
+ import { type Authorize, type StartResult, type StartRunArgs } from '@swirls/looms/runtime';
5
5
  export interface LoomsDurableObjectConfig {
6
6
  readonly modules: readonly AnyRuntimeModule[];
7
+ /** Loads definitions that were not registered on a module. See `CreateRuntimeOptions`. */
8
+ readonly definitionResolver?: DefinitionResolver;
7
9
  readonly projectors?: readonly Projector[];
10
+ /** Content-addressed store for event and snapshot JSON over the inline byte limits. */
11
+ readonly blobs?: BlobStore;
8
12
  readonly snapshotEvery?: number;
9
13
  readonly maxWakeIterations?: number;
10
14
  readonly maxPendingRunOperations?: number;
@@ -14,6 +18,11 @@ export interface LoomsDurableObjectConfig {
14
18
  };
15
19
  /** Authorizes the actor's HTTP API; see `createFetchHandler`. */
16
20
  readonly authorize?: Authorize;
21
+ /**
22
+ * Starts runs from effect handlers. A schedule object passes `durableObjectLauncher` so fires
23
+ * land in the target's object. Without it, a fire fails instead of writing into this object.
24
+ */
25
+ readonly launcher?: RunLauncher;
17
26
  }
18
27
  /**
19
28
  * Base Durable Object class for hosting a Looms actor cell.
@@ -23,10 +32,65 @@ export interface LoomsDurableObjectConfig {
23
32
  export declare abstract class LoomsDurableObject<Env = unknown> extends DurableObject<Env> {
24
33
  private cellPromise?;
25
34
  private store?;
35
+ private authorize;
26
36
  abstract configure(env: Env): LoomsDurableObjectConfig | Promise<LoomsDurableObjectConfig>;
27
37
  private getCell;
28
38
  private createCell;
29
39
  fetch(req: Request): Promise<Response>;
40
+ /** Client messages are ignored. Signals and other writes stay on HTTP. */
41
+ webSocketMessage(): void;
42
+ webSocketClose(): void;
43
+ webSocketError(): void;
44
+ private acceptRunSocket;
45
+ /** Commit this object's run and set its alarm. Returns before the run wakes. */
46
+ launch(args: LaunchRunArgs): Promise<void>;
47
+ status(): Promise<RunStatusSnapshot | null>;
48
+ startRun(args: StartRunArgs): Promise<StartResult>;
49
+ signal(events: ReadonlyArray<EventInput>, options?: {
50
+ idempotencyKey?: string;
51
+ coalesce?: boolean;
52
+ }): Promise<RunState>;
53
+ accept(events: ReadonlyArray<EventInput>, options?: {
54
+ idempotencyKey?: string;
55
+ coalesce?: boolean;
56
+ }): Promise<RunState>;
57
+ cancel(): Promise<void>;
58
+ getRun(): Promise<RunState>;
30
59
  /** A rejected alarm is retried by the platform with backoff, so failures just propagate. */
31
60
  alarm(): Promise<void>;
32
61
  }
62
+ export interface DurableObjectRunStub {
63
+ fetch(request: Request): Promise<Response>;
64
+ launch(args: LaunchRunArgs): Promise<void>;
65
+ status(): Promise<RunStatusSnapshot | null>;
66
+ startRun(args: StartRunArgs): Promise<StartResult>;
67
+ signal(events: ReadonlyArray<EventInput>, options?: {
68
+ idempotencyKey?: string;
69
+ coalesce?: boolean;
70
+ }): Promise<RunState>;
71
+ accept(events: ReadonlyArray<EventInput>, options?: {
72
+ idempotencyKey?: string;
73
+ coalesce?: boolean;
74
+ }): Promise<RunState>;
75
+ cancel(): Promise<void>;
76
+ getRun(): Promise<RunState>;
77
+ }
78
+ /**
79
+ * Launches and reads runs in other Durable Objects. One object per `runId`.
80
+ * The argument is the binding, not the stub, so a class can pass its own namespace
81
+ * without the type checker expanding that class through this function.
82
+ */
83
+ export declare function durableObjectLauncher(namespace: {
84
+ getByName(name: string): object;
85
+ }): RunLauncher;
86
+ export interface DurableObjectScheduleHost {
87
+ startRun(args: StartRunArgs): Promise<StartResult>;
88
+ signal(runId: string, events: ReadonlyArray<EventInput>, options?: {
89
+ idempotencyKey?: string;
90
+ }): Promise<RunState>;
91
+ getRun(runId: string): Promise<RunState>;
92
+ }
93
+ /** `ScheduleHost` for a Worker that reconciles schedules onto Durable Objects. */
94
+ export declare function durableObjectScheduleHost(namespace: {
95
+ getByName(name: string): object;
96
+ }): DurableObjectScheduleHost;
@@ -1,8 +1,11 @@
1
1
  import { DurableObject } from 'cloudflare:workers';
2
2
  import { Effect } from 'effect';
3
3
  import { createActorCell } from '@swirls/looms/actor';
4
+ import { createRemoteRunLauncher, } from '@swirls/looms/core';
5
+ import { defaultAuthorize, } from '@swirls/looms/runtime';
4
6
  import { durableObjectAlarms } from './alarm-scheduler.js';
5
7
  import { durableObjectEventStore } from './event-store.js';
8
+ import { attachSocketFanout, isRunSocketUpgrade, openRunSocket } from './websocket.js';
6
9
  /**
7
10
  * Base Durable Object class for hosting a Looms actor cell.
8
11
  * Subclasses implement `configure(env)` to supply configured modules.
@@ -11,6 +14,7 @@ import { durableObjectEventStore } from './event-store.js';
11
14
  export class LoomsDurableObject extends DurableObject {
12
15
  cellPromise;
13
16
  store;
17
+ authorize = defaultAuthorize;
14
18
  getCell() {
15
19
  if (this.cellPromise) {
16
20
  return this.cellPromise;
@@ -36,18 +40,22 @@ export class LoomsDurableObject extends DurableObject {
36
40
  const store = durableObjectEventStore(this.ctx, {
37
41
  projectors: config.projectors,
38
42
  scheduleProjectorRetry: alarms.scheduleAtEarliest,
43
+ blobs: config.blobs,
39
44
  });
40
45
  this.store = store;
46
+ this.authorize = config.authorize ?? defaultAuthorize;
41
47
  const cell = createActorCell({
42
48
  runId,
43
- store,
49
+ store: attachSocketFanout(this.ctx, store, runId),
44
50
  scheduler: alarms.scheduler,
45
51
  modules: config.modules,
52
+ definitionResolver: config.definitionResolver,
46
53
  snapshotEvery: config.snapshotEvery,
47
54
  maxWakeIterations: config.maxWakeIterations,
48
55
  maxPendingRunOperations: config.maxPendingRunOperations,
49
56
  trimAfterSnapshot: config.trimAfterSnapshot,
50
57
  authorize: config.authorize,
58
+ launcher: config.launcher,
51
59
  });
52
60
  const delivery = store.projectorDelivery;
53
61
  const recoverProjectors = delivery
@@ -57,8 +65,58 @@ export class LoomsDurableObject extends DurableObject {
57
65
  });
58
66
  }
59
67
  fetch(req) {
68
+ if (isRunSocketUpgrade(req)) {
69
+ return this.getCell().then((cell) => this.acceptRunSocket(req, cell.runtime));
70
+ }
60
71
  return this.getCell().then((cell) => cell.fetch(req));
61
72
  }
73
+ /** Client messages are ignored. Signals and other writes stay on HTTP. */
74
+ webSocketMessage() {
75
+ // A hibernated socket stays quiet. The platform delivers this only if a client writes.
76
+ }
77
+ webSocketClose() {
78
+ // The platform drops the socket. Nothing is retained for it.
79
+ }
80
+ webSocketError() {
81
+ // The platform drops the socket. Nothing is retained for it.
82
+ }
83
+ acceptRunSocket(req, runtime) {
84
+ const store = this.store;
85
+ const runId = this.ctx.id.name;
86
+ if (!store || !runId) {
87
+ return Promise.resolve(Response.json({ error: 'Run is not ready' }, { status: 500 }));
88
+ }
89
+ return Effect.runPromise(openRunSocket({
90
+ ctx: this.ctx,
91
+ runtime,
92
+ store,
93
+ request: req,
94
+ runId,
95
+ authorize: this.authorize,
96
+ }));
97
+ }
98
+ /** Commit this object's run and set its alarm. Returns before the run wakes. */
99
+ launch(args) {
100
+ return this.getCell().then((cell) => cell.launch(args));
101
+ }
102
+ status() {
103
+ return this.getCell().then((cell) => cell.status());
104
+ }
105
+ startRun(args) {
106
+ return this.getCell().then((cell) => cell.startRun(args));
107
+ }
108
+ signal(events, options) {
109
+ return this.getCell().then((cell) => cell.signal(events, options));
110
+ }
111
+ accept(events, options) {
112
+ return this.getCell().then((cell) => cell.accept(events, options));
113
+ }
114
+ cancel() {
115
+ return this.getCell().then((cell) => cell.cancel());
116
+ }
117
+ getRun() {
118
+ return this.getCell().then((cell) => cell.getRun());
119
+ }
62
120
  /** A rejected alarm is retried by the platform with backoff, so failures just propagate. */
63
121
  alarm() {
64
122
  return this.getCell()
@@ -72,3 +130,40 @@ export class LoomsDurableObject extends DurableObject {
72
130
  .then(() => undefined);
73
131
  }
74
132
  }
133
+ /**
134
+ * Launches and reads runs in other Durable Objects. One object per `runId`.
135
+ * The argument is the binding, not the stub, so a class can pass its own namespace
136
+ * without the type checker expanding that class through this function.
137
+ */
138
+ export function durableObjectLauncher(namespace) {
139
+ const runs = loomsNamespace(namespace);
140
+ return createRemoteRunLauncher({
141
+ launch: (runId, args) => runs.getByName(runId).launch(args),
142
+ status: (runId) => runs.getByName(runId).status(),
143
+ accept: (runId, events, options) => runs
144
+ .getByName(runId)
145
+ .accept(events, options)
146
+ .then(() => undefined),
147
+ cancel: (runId) => runs.getByName(runId).cancel(),
148
+ });
149
+ }
150
+ /** `ScheduleHost` for a Worker that reconciles schedules onto Durable Objects. */
151
+ export function durableObjectScheduleHost(namespace) {
152
+ const runs = loomsNamespace(namespace);
153
+ return {
154
+ startRun: (args) => {
155
+ const runId = args.runId;
156
+ if (!runId) {
157
+ return Promise.reject(new Error('ScheduleHost.startRun requires runId'));
158
+ }
159
+ return runs.getByName(runId).startRun(args);
160
+ },
161
+ signal: (runId, events, options) => runs.getByName(runId).signal(events, options),
162
+ getRun: (runId) => runs.getByName(runId).getRun(),
163
+ };
164
+ }
165
+ function loomsNamespace(namespace) {
166
+ // SAFETY: callers pass a Looms Durable Object namespace, whose stub implements these methods.
167
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
168
+ return namespace;
169
+ }
@@ -1,15 +1,19 @@
1
- import type { Effect } from 'effect';
2
- import type { EventStore } from '@swirls/looms/core';
1
+ import { Effect } from 'effect';
2
+ import { type BlobStore, type EventStore } from '@swirls/looms/core';
3
3
  import { type Projector, type ProjectorDelivery } from '@swirls/looms/projectors';
4
4
  export interface DurableObjectStoreOptions {
5
5
  readonly projectors?: readonly Projector[];
6
6
  readonly scheduleProjectorRetry?: (at: number) => Effect.Effect<void>;
7
+ /** Offloads oversized event and snapshot JSON. See `withBlobOffload`. */
8
+ readonly blobs?: BlobStore;
7
9
  }
8
10
  export interface DurableObjectEventStore extends EventStore {
9
11
  readonly projectorDelivery?: ProjectorDelivery;
10
12
  }
11
13
  /**
12
14
  * Creates an EventStore backed by a Durable Object's embedded SQLite database (`ctx.storage.sql`).
13
- * Optionally attaches asynchronous projectors (such as S2).
15
+ * Optionally attaches projectors. Their delivery is scheduled with `waitUntil`, so an append
16
+ * returns when the log commits. A crashed delivery is retried from the durable cursor on the next
17
+ * alarm or request.
14
18
  */
15
19
  export declare function durableObjectEventStore(ctx: DurableObjectState, options?: DurableObjectStoreOptions): DurableObjectEventStore;
@@ -1,18 +1,31 @@
1
+ import { Effect } from 'effect';
2
+ import { snapshotStoreOf, withBlobOffload } from '@swirls/looms/core';
1
3
  import { sqliteEventStore, sqlStorageExec } from '@swirls/looms/core/sqlite-store';
2
4
  import { createProjectorDelivery, sqliteProjectorCursorStore, withProjectors, } from '@swirls/looms/projectors';
3
5
  /**
4
6
  * Creates an EventStore backed by a Durable Object's embedded SQLite database (`ctx.storage.sql`).
5
- * Optionally attaches asynchronous projectors (such as S2).
7
+ * Optionally attaches projectors. Their delivery is scheduled with `waitUntil`, so an append
8
+ * returns when the log commits. A crashed delivery is retried from the durable cursor on the next
9
+ * alarm or request.
6
10
  */
7
11
  export function durableObjectEventStore(ctx, options = {}) {
8
12
  const exec = sqlStorageExec(ctx.storage.sql, {
9
13
  // DO SQLite rejects BEGIN/COMMIT/ROLLBACK; use the storage transaction API.
10
14
  transaction: (fn) => ctx.storage.transactionSync(fn),
11
15
  });
12
- const store = sqliteEventStore({ exec });
16
+ const sqlite = sqliteEventStore({ exec });
17
+ const snapshots = snapshotStoreOf(sqlite);
18
+ const store = options.blobs
19
+ ? withBlobOffload(sqlite, options.blobs, snapshots ? { snapshots } : undefined)
20
+ : sqlite;
13
21
  if (options.projectors && options.projectors.length > 0) {
14
22
  const delivery = createProjectorDelivery(store, options.projectors, sqliteProjectorCursorStore(exec), { scheduleRetry: options.scheduleProjectorRetry });
15
- const wrapped = withProjectors(store, options.projectors, { delivery });
23
+ const wrapped = withProjectors(store, options.projectors, {
24
+ delivery,
25
+ scheduleDelivery: (_runId, work) => {
26
+ ctx.waitUntil(Effect.runPromise(work));
27
+ },
28
+ });
16
29
  return Object.assign(wrapped, { projectorDelivery: delivery });
17
30
  }
18
31
  return store;
@@ -1,4 +1,10 @@
1
1
  export { durableObjectAlarms, type AlarmStorage, type DurableObjectAlarms } from './alarm-scheduler';
2
2
  export { durableObjectEventStore, type DurableObjectEventStore, type DurableObjectStoreOptions, } from './event-store';
3
- export { LoomsDurableObject, type LoomsDurableObjectConfig } from './durable-object';
3
+ export { durableObjectLauncher, durableObjectScheduleHost, LoomsDurableObject, type DurableObjectRunStub, type LoomsDurableObjectConfig, } from './durable-object';
4
+ export { r2BlobStore, type R2BucketLike, type R2ListResult, type R2ObjectBodyLike, type R2StoredObject, } from './r2-blob-store';
4
5
  export { routeToDurableObject, type DurableObjectNamespaceLike, type DurableObjectStubLike, } from './router';
6
+ export { createMemoryQueue, type MemoryQueue, type MemoryQueueOptions } from './memory-queue';
7
+ export { processQueueMessage, queueConsumer, QueueTaskError, type QueueBatchLike, type QueueConsumerOptions, type QueueTask, type QueueTaskContext, type QueueTaskHandler, type WorkerCallbackClient, } from './queue-consumer';
8
+ export { readQueueEffectMessage, type QueueEffectMessage, type QueueMessageLike, } from './queue-message';
9
+ export { cloudflareQueuePullSource, createQueuePullConsumer, QueuePullError, type CloudflareQueuePullSourceOptions, type PulledQueueMessage, type QueueLeaseRetry, type QueuePullConsumer, type QueuePullConsumerOptions, type QueuePullResult, type QueuePullSource, } from './queue-pull';
10
+ export { cloudflareQueueWorker, DEFAULT_MAX_QUEUE_MESSAGE_BYTES, QueueDispatchError, type CloudflareQueueWorkerOptions, type QueueProducer, type QueueRoute, } from './queue-worker';
@@ -1,4 +1,10 @@
1
1
  export { durableObjectAlarms } from './alarm-scheduler.js';
2
2
  export { durableObjectEventStore, } from './event-store.js';
3
- export { LoomsDurableObject } from './durable-object.js';
3
+ export { durableObjectLauncher, durableObjectScheduleHost, LoomsDurableObject, } from './durable-object.js';
4
+ export { r2BlobStore, } from './r2-blob-store.js';
4
5
  export { routeToDurableObject, } from './router.js';
6
+ export { createMemoryQueue } from './memory-queue.js';
7
+ export { processQueueMessage, queueConsumer, QueueTaskError, } from './queue-consumer.js';
8
+ export { readQueueEffectMessage, } from './queue-message.js';
9
+ export { cloudflareQueuePullSource, createQueuePullConsumer, QueuePullError, } from './queue-pull.js';
10
+ export { cloudflareQueueWorker, DEFAULT_MAX_QUEUE_MESSAGE_BYTES, QueueDispatchError, } from './queue-worker.js';