@swirls/looms 0.0.2 → 0.1.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 (256) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/dist/actor/cell.d.ts +9 -4
  3. package/dist/actor/cell.js +7 -11
  4. package/dist/actor/host.d.ts +7 -3
  5. package/dist/actor/host.js +55 -32
  6. package/dist/actor/index.d.ts +1 -1
  7. package/dist/actor/index.js +1 -1
  8. package/dist/actor/routing.d.ts +12 -1
  9. package/dist/actor/routing.js +57 -51
  10. package/dist/agent/agent-session.d.ts +5 -0
  11. package/dist/agent/agent-session.js +270 -0
  12. package/dist/agent/definitions-store.d.ts +2 -3
  13. package/dist/agent/definitions-store.js +2 -4
  14. package/dist/agent/definitions.d.ts +46 -20
  15. package/dist/agent/definitions.js +36 -8
  16. package/dist/agent/effects.d.ts +359 -2
  17. package/dist/agent/effects.js +60 -34
  18. package/dist/agent/events.d.ts +175 -19
  19. package/dist/agent/events.js +45 -10
  20. package/dist/agent/index.d.ts +1 -0
  21. package/dist/agent/index.js +1 -0
  22. package/dist/agent/llm.d.ts +11 -2
  23. package/dist/agent/llm.js +13 -3
  24. package/dist/agent/module.d.ts +812 -78
  25. package/dist/agent/module.js +16 -4
  26. package/dist/agent/scope.d.ts +174 -18
  27. package/dist/agent/signals.d.ts +24 -1
  28. package/dist/agent/signals.js +77 -0
  29. package/dist/agent/threads.d.ts +3 -33
  30. package/dist/agent/threads.js +20 -25
  31. package/dist/agent/tool-schema.js +0 -1
  32. package/dist/agent/types.d.ts +49 -4
  33. package/dist/agent/types.js +5 -5
  34. package/dist/ai-vercel/index.d.ts +1 -0
  35. package/dist/ai-vercel/index.js +1 -0
  36. package/dist/ai-vercel/messages.js +22 -7
  37. package/dist/ai-vercel/vercel-evaluator.d.ts +23 -0
  38. package/dist/ai-vercel/vercel-evaluator.js +68 -0
  39. package/dist/ai-vercel/vercel-llm.js +64 -44
  40. package/dist/approval/effects.d.ts +89 -0
  41. package/dist/approval/module.d.ts +179 -29
  42. package/dist/approval/projections.d.ts +0 -14
  43. package/dist/approval/projections.js +20 -3
  44. package/dist/approval/signals.d.ts +2 -0
  45. package/dist/approval/signals.js +18 -2
  46. package/dist/cli/cli.js +2 -0
  47. package/dist/cli/commands.js +12 -16
  48. package/dist/client/client.d.ts +1195 -18
  49. package/dist/client/client.js +118 -120
  50. package/dist/client/index.d.ts +1 -1
  51. package/dist/client/index.js +1 -1
  52. package/dist/client/sse.d.ts +13 -0
  53. package/dist/client/sse.js +38 -44
  54. package/dist/cloudflare/alarm-scheduler.d.ts +12 -2
  55. package/dist/cloudflare/alarm-scheduler.js +34 -7
  56. package/dist/cloudflare/durable-object.d.ts +10 -2
  57. package/dist/cloudflare/durable-object.js +59 -28
  58. package/dist/cloudflare/event-store.d.ts +7 -2
  59. package/dist/cloudflare/event-store.js +8 -7
  60. package/dist/cloudflare/index.d.ts +2 -2
  61. package/dist/cloudflare/index.js +2 -2
  62. package/dist/cloudflare/router.js +13 -14
  63. package/dist/core/catalog.d.ts +3 -3
  64. package/dist/core/catalog.js +2 -5
  65. package/dist/core/coalesce.js +5 -3
  66. package/dist/core/compose.d.ts +2 -1
  67. package/dist/core/compose.js +18 -2
  68. package/dist/core/definition-store.d.ts +11 -0
  69. package/dist/core/definition-store.js +6 -0
  70. package/dist/core/effects.d.ts +127 -15
  71. package/dist/core/effects.js +89 -17
  72. package/dist/core/envelope.d.ts +27 -7
  73. package/dist/core/envelope.js +20 -6
  74. package/dist/core/event-codec.d.ts +4 -2
  75. package/dist/core/event-codec.js +10 -5
  76. package/dist/core/event-index.js +1 -3
  77. package/dist/core/fold.d.ts +3 -0
  78. package/dist/core/fold.js +171 -24
  79. package/dist/core/ids.d.ts +5 -0
  80. package/dist/core/ids.js +35 -2
  81. package/dist/core/index.d.ts +5 -1
  82. package/dist/core/index.js +5 -1
  83. package/dist/core/kind.d.ts +22 -0
  84. package/dist/core/kind.js +17 -0
  85. package/dist/core/limits.d.ts +34 -0
  86. package/dist/core/limits.js +46 -0
  87. package/dist/core/match.d.ts +1 -0
  88. package/dist/core/match.js +10 -1
  89. package/dist/core/module-scope.d.ts +22 -20
  90. package/dist/core/module-scope.js +38 -28
  91. package/dist/core/module.d.ts +16 -5
  92. package/dist/core/module.js +4 -0
  93. package/dist/core/projection.d.ts +13 -13
  94. package/dist/core/projection.js +9 -5
  95. package/dist/core/protocol.d.ts +68 -11
  96. package/dist/core/protocol.js +82 -11
  97. package/dist/core/replay.d.ts +312 -3
  98. package/dist/core/replay.js +11 -1
  99. package/dist/core/schema.d.ts +8 -19
  100. package/dist/core/schema.js +25 -80
  101. package/dist/core/serialize.js +2 -4
  102. package/dist/core/snapshot-store.d.ts +2 -1
  103. package/dist/core/snapshot-store.js +8 -2
  104. package/dist/core/snapshots.d.ts +5 -1
  105. package/dist/core/snapshots.js +10 -15
  106. package/dist/core/sqlite-store.js +56 -11
  107. package/dist/core/state.d.ts +142 -4
  108. package/dist/core/state.js +78 -0
  109. package/dist/core/store.d.ts +38 -3
  110. package/dist/core/store.js +77 -22
  111. package/dist/core/thread.d.ts +10 -9
  112. package/dist/core/types.d.ts +5 -8
  113. package/dist/core/types.js +28 -24
  114. package/dist/core/validate-event.js +14 -4
  115. package/dist/core/worker-callback.d.ts +17 -0
  116. package/dist/core/worker-callback.js +1 -0
  117. package/dist/debugger/components/event-stream.js +9 -10
  118. package/dist/debugger/events/protocol-summarize.js +26 -0
  119. package/dist/debugger/lib/event-meta.js +1 -2
  120. package/dist/evaluator/definitions-store.d.ts +11 -0
  121. package/dist/evaluator/definitions-store.js +8 -0
  122. package/dist/evaluator/definitions.d.ts +18 -0
  123. package/dist/evaluator/definitions.js +4 -0
  124. package/dist/evaluator/effects.d.ts +226 -0
  125. package/dist/evaluator/effects.js +132 -0
  126. package/dist/evaluator/evaluator.d.ts +24 -0
  127. package/dist/evaluator/evaluator.js +67 -0
  128. package/dist/evaluator/events.d.ts +59 -0
  129. package/dist/evaluator/events.js +21 -0
  130. package/dist/evaluator/index.d.ts +11 -0
  131. package/dist/evaluator/index.js +11 -0
  132. package/dist/evaluator/module.d.ts +606 -0
  133. package/dist/evaluator/module.js +25 -0
  134. package/dist/evaluator/projections.d.ts +50 -0
  135. package/dist/evaluator/projections.js +20 -0
  136. package/dist/evaluator/scope.d.ts +59 -0
  137. package/dist/evaluator/scope.js +7 -0
  138. package/dist/evaluator/signals.d.ts +3 -0
  139. package/dist/evaluator/signals.js +17 -0
  140. package/dist/evaluator/threads.d.ts +62 -0
  141. package/dist/evaluator/threads.js +117 -0
  142. package/dist/evaluator/types.d.ts +125 -0
  143. package/dist/evaluator/types.js +51 -0
  144. package/dist/projectors/delivery.d.ts +83 -0
  145. package/dist/projectors/delivery.js +182 -0
  146. package/dist/projectors/index-model.d.ts +1 -1
  147. package/dist/projectors/index-model.js +4 -6
  148. package/dist/projectors/index.d.ts +2 -0
  149. package/dist/projectors/index.js +2 -0
  150. package/dist/projectors/memory.d.ts +1 -1
  151. package/dist/projectors/memory.js +10 -8
  152. package/dist/projectors/postgres.js +44 -58
  153. package/dist/projectors/projector.d.ts +17 -0
  154. package/dist/projectors/projector.js +26 -1
  155. package/dist/projectors/sqlite-delivery.d.ts +4 -0
  156. package/dist/projectors/sqlite-delivery.js +147 -0
  157. package/dist/projectors/sqlite.js +23 -23
  158. package/dist/projectors/with-projectors.d.ts +7 -2
  159. package/dist/projectors/with-projectors.js +27 -28
  160. package/dist/react/notify.js +11 -8
  161. package/dist/react/react.d.ts +2 -3
  162. package/dist/react/react.js +14 -30
  163. package/dist/react/sse.d.ts +1 -1
  164. package/dist/react/sse.js +1 -1
  165. package/dist/react/store.d.ts +2 -2
  166. package/dist/react/store.js +109 -119
  167. package/dist/runtime/cancellation.d.ts +16 -0
  168. package/dist/runtime/cancellation.js +129 -0
  169. package/dist/runtime/deadline-transition-planner.d.ts +31 -0
  170. package/dist/runtime/deadline-transition-planner.js +139 -0
  171. package/dist/runtime/dispatch-effect.d.ts +3 -2
  172. package/dist/runtime/dispatch-effect.js +7 -13
  173. package/dist/runtime/errors.d.ts +51 -0
  174. package/dist/runtime/errors.js +39 -0
  175. package/dist/runtime/{runtime-helpers.d.ts → helpers.d.ts} +24 -4
  176. package/dist/runtime/{runtime-helpers.js → helpers.js} +56 -19
  177. package/dist/runtime/index.d.ts +3 -3
  178. package/dist/runtime/index.js +3 -3
  179. package/dist/runtime/ingress.d.ts +15 -0
  180. package/dist/runtime/ingress.js +42 -0
  181. package/dist/runtime/local-effect-executor.d.ts +21 -0
  182. package/dist/runtime/local-effect-executor.js +137 -0
  183. package/dist/runtime/looms.d.ts +22 -4
  184. package/dist/runtime/looms.js +54 -36
  185. package/dist/runtime/observer.d.ts +45 -0
  186. package/dist/runtime/observer.js +11 -0
  187. package/dist/runtime/repository.d.ts +55 -0
  188. package/dist/runtime/repository.js +278 -0
  189. package/dist/runtime/retry-policy.d.ts +16 -0
  190. package/dist/runtime/retry-policy.js +29 -0
  191. package/dist/runtime/run-execution-context.d.ts +30 -0
  192. package/dist/runtime/run-execution-context.js +56 -0
  193. package/dist/runtime/run-start.d.ts +4 -0
  194. package/dist/runtime/run-start.js +44 -0
  195. package/dist/runtime/runtime.d.ts +6 -87
  196. package/dist/runtime/runtime.js +233 -470
  197. package/dist/runtime/server/auth.d.ts +40 -0
  198. package/dist/runtime/server/auth.js +40 -0
  199. package/dist/runtime/server/fetch-handler.d.ts +14 -0
  200. package/dist/runtime/server/fetch-handler.js +51 -0
  201. package/dist/runtime/server/http.d.ts +37 -0
  202. package/dist/runtime/server/http.js +62 -0
  203. package/dist/runtime/server/route-table.d.ts +55 -0
  204. package/dist/runtime/server/route-table.js +74 -0
  205. package/dist/runtime/server/routes/effects.d.ts +3 -0
  206. package/dist/runtime/server/routes/effects.js +59 -0
  207. package/dist/runtime/server/routes/operations.d.ts +3 -0
  208. package/dist/runtime/server/routes/operations.js +14 -0
  209. package/dist/runtime/server/routes/runs.d.ts +3 -0
  210. package/dist/runtime/server/routes/runs.js +192 -0
  211. package/dist/runtime/server.d.ts +2 -8
  212. package/dist/runtime/server.js +4 -211
  213. package/dist/runtime/sse.d.ts +2 -1
  214. package/dist/runtime/sse.js +24 -17
  215. package/dist/runtime/types.d.ts +115 -0
  216. package/dist/runtime/types.js +1 -0
  217. package/dist/runtime/wake-engine.d.ts +20 -0
  218. package/dist/runtime/wake-engine.js +311 -0
  219. package/dist/runtime/wake-scheduler.d.ts +3 -4
  220. package/dist/runtime/wake-scheduler.js +25 -31
  221. package/dist/runtime/worker-callbacks.d.ts +16 -0
  222. package/dist/runtime/worker-callbacks.js +167 -0
  223. package/dist/runtime/worker-effect-dispatch.d.ts +36 -0
  224. package/dist/runtime/worker-effect-dispatch.js +126 -0
  225. package/dist/s2/config.d.ts +1 -1
  226. package/dist/s2/config.js +1 -1
  227. package/dist/s2/lite.js +70 -82
  228. package/dist/s2/pagination.d.ts +15 -0
  229. package/dist/s2/pagination.js +36 -0
  230. package/dist/s2/projector.js +6 -5
  231. package/dist/s2/snapshot-store.js +148 -163
  232. package/dist/s2/store.d.ts +3 -21
  233. package/dist/s2/store.js +71 -165
  234. package/dist/testing/index.d.ts +15 -0
  235. package/dist/testing/index.js +53 -16
  236. package/dist/workflow/definitions-store.d.ts +2 -3
  237. package/dist/workflow/definitions-store.js +2 -2
  238. package/dist/workflow/definitions.d.ts +78 -22
  239. package/dist/workflow/definitions.js +80 -14
  240. package/dist/workflow/effects.d.ts +453 -107
  241. package/dist/workflow/effects.js +214 -115
  242. package/dist/workflow/events.d.ts +149 -5
  243. package/dist/workflow/events.js +27 -5
  244. package/dist/workflow/graph.d.ts +56 -0
  245. package/dist/workflow/graph.js +207 -0
  246. package/dist/workflow/index.d.ts +1 -0
  247. package/dist/workflow/index.js +1 -0
  248. package/dist/workflow/module.d.ts +1047 -266
  249. package/dist/workflow/projections.d.ts +14 -12
  250. package/dist/workflow/projections.js +30 -8
  251. package/dist/workflow/scope.d.ts +146 -4
  252. package/dist/workflow/threads.d.ts +383 -155
  253. package/dist/workflow/threads.js +308 -110
  254. package/package.json +7 -2
  255. package/dist/runtime/events-api.d.ts +0 -4
  256. package/dist/runtime/events-api.js +0 -84
package/dist/s2/store.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { AppendInput, AppendRecord, S2, S2Endpoints, S2Error } from '@s2-dev/streamstore';
2
- import { Effect, Layer, Option, Predicate, Queue, Schema, Stream } from 'effect';
3
- import { createKeyedSerializer, EventStoreConflictError, EventStoreError, EventStoreFencedError, EventStoreTag, EventEnvelopeSchema, fromWireEvent, withSnapshotStore, } from '@swirls/looms/core';
2
+ import { DateTime, Effect, Layer, Predicate, Schema, Stream } from 'effect';
3
+ import { createKeyedSerializer, assertEventByteLimits, EventStoreConflictError, EventStoreError, EventStoreFencedError, EventPayloadTooLargeError, EventStoreTag, EventEnvelopeSchema, fromWireEvent, withSnapshotStore, } from '@swirls/looms/core';
4
4
  import { S2ConfigSchema, streamNameForRun } from './config.js';
5
+ import { paginateS2Stream } from './pagination.js';
5
6
  import { s2SnapshotStore } from './snapshot-store.js';
6
7
  function toStoreError(cause, message) {
7
8
  if (cause instanceof EventStoreError) {
@@ -20,50 +21,8 @@ function isUnsatisfiableCause(cause) {
20
21
  const text = cause instanceof Error ? cause.message : String(cause);
21
22
  return text.includes('out of range') || text.includes('Range not satisfiable');
22
23
  }
23
- /**
24
- * Paginate an S2 stream lazily using Effect Streams.
25
- *
26
- * Pulls pages on demand until the stream tail is reached, an empty page is returned,
27
- * or the caller's optional limit is satisfied.
28
- */
29
- export function paginateS2Stream(fetchPage, options) {
30
- const fromSeq = options?.fromSeq ?? 0;
31
- const userLimit = options?.limit;
32
- const maxPageSize = options?.maxPageSize ?? 1000;
33
- if (userLimit !== undefined && userLimit <= 0) {
34
- return Stream.empty;
35
- }
36
- const baseStream = Stream.paginate({ cursor: fromSeq, remaining: userLimit }, (state) => Effect.gen(function* () {
37
- if (state.remaining !== undefined && state.remaining <= 0) {
38
- return [[], Option.none()];
39
- }
40
- const count = state.remaining !== undefined ? Math.min(maxPageSize, state.remaining) : maxPageSize;
41
- const page = yield* fetchPage(state.cursor, count);
42
- if (!page || page.records.length === 0) {
43
- return [[], Option.none()];
44
- }
45
- const records = page.records.filter((r) => r.seqNum >= fromSeq);
46
- const nextRemaining = state.remaining !== undefined ? state.remaining - records.length : undefined;
47
- const lastRecord = page.records[page.records.length - 1];
48
- if (!lastRecord || (nextRemaining !== undefined && nextRemaining <= 0)) {
49
- return [records, Option.none()];
50
- }
51
- const nextCursor = Math.max(state.cursor + 1, lastRecord.seqNum + 1);
52
- if (nextCursor <= state.cursor) {
53
- return [records, Option.none()];
54
- }
55
- // A byte-capped page is shorter than `count`. Do not trust `page.tail`
56
- // there — S2 may report the batch end, not the stream tail.
57
- const shortPage = page.records.length < count;
58
- const reachedTail = !shortPage && page.tail?.seqNum !== undefined && nextCursor >= page.tail.seqNum;
59
- if (reachedTail) {
60
- return [records, Option.none()];
61
- }
62
- return [records, Option.some({ cursor: nextCursor, remaining: nextRemaining })];
63
- }));
64
- return userLimit !== undefined ? baseStream.pipe(Stream.take(userLimit)) : baseStream;
65
- }
66
- export async function readAllPages(fetchPage, options) {
24
+ export { paginateS2Stream };
25
+ export function readAllPages(fetchPage, options) {
67
26
  const fetchPageEffect = (cursor, count) => Effect.tryPromise({
68
27
  try: () => fetchPage(cursor, count),
69
28
  catch: (cause) => toStoreError(cause, 'S2 page fetch failed'),
@@ -101,61 +60,49 @@ function createClient(config) {
101
60
  * Expected tail mismatches during `append` map to `EventStoreConflictError`.
102
61
  */
103
62
  export function s2(config) {
104
- const parsed = Schema.decodeUnknownSync(S2ConfigSchema)(config);
63
+ const parsed = Schema.decodeSync(S2ConfigSchema)(config);
105
64
  const client = createClient(parsed);
106
65
  const basin = client.basin(parsed.basin);
107
66
  const ensured = new Set();
108
67
  const appends = createKeyedSerializer();
109
68
  let basinEnsured = false;
110
- const ensureBasin = Effect.tryPromise({
111
- try: async () => {
112
- if (basinEnsured) {
113
- return;
114
- }
115
- try {
116
- await client.basins.create({ basin: parsed.basin });
117
- }
118
- catch (err) {
119
- // 409 = already exists
120
- if (!(err instanceof S2Error && err.status === 409)) {
121
- throw err;
122
- }
123
- }
69
+ const ensureBasin = Effect.suspend(() => {
70
+ if (basinEnsured) {
71
+ return Effect.void;
72
+ }
73
+ return Effect.tryPromise({
74
+ try: () => client.basins.create({ basin: parsed.basin }),
75
+ catch: (cause) => toStoreError(cause, `Failed to ensure basin ${parsed.basin}`),
76
+ }).pipe(Effect.catchIf((error) => error.cause instanceof S2Error && error.cause.status === 409, () => Effect.void), Effect.tap(() => Effect.sync(() => {
124
77
  basinEnsured = true;
125
- },
126
- catch: (cause) => toStoreError(cause, `Failed to ensure basin ${parsed.basin}`),
78
+ })), Effect.asVoid);
127
79
  });
128
80
  const ensureStream = (runId) => Effect.gen(function* () {
129
81
  yield* ensureBasin;
130
- return yield* Effect.tryPromise({
131
- try: async () => {
132
- const name = streamNameForRun(runId);
133
- if (ensured.has(name)) {
134
- return basin.stream(name);
135
- }
136
- try {
137
- await basin.streams.create({
138
- stream: name,
139
- config: parsed.streamConfig,
140
- });
141
- }
142
- catch (err) {
143
- // 409 = already exists
144
- if (!(err instanceof S2Error && err.status === 409)) {
145
- // Some local servers may not support create; try using the stream anyway.
146
- if (!(err instanceof S2Error && (err.status === 404 || err.status === 405))) {
147
- throw err;
148
- }
149
- }
150
- }
151
- ensured.add(name);
152
- return basin.stream(name);
153
- },
154
- catch: (cause) => toStoreError(cause, `Failed to open stream for ${runId}`),
155
- });
82
+ const name = streamNameForRun(runId);
83
+ if (!ensured.has(name)) {
84
+ yield* Effect.tryPromise({
85
+ try: () => basin.streams.create({
86
+ stream: name,
87
+ config: parsed.streamConfig,
88
+ }),
89
+ catch: (cause) => toStoreError(cause, `Failed to open stream for ${runId}`),
90
+ }).pipe(Effect.catchIf((error) => error.cause instanceof S2Error &&
91
+ (error.cause.status === 409 ||
92
+ error.cause.status === 404 ||
93
+ error.cause.status === 405), () => Effect.void));
94
+ ensured.add(name);
95
+ }
96
+ return basin.stream(name);
156
97
  });
157
98
  const service = {
158
99
  append: (runId, events, options) => Effect.gen(function* () {
100
+ yield* Effect.try({
101
+ try: () => assertEventByteLimits(runId, events),
102
+ catch: (cause) => cause instanceof EventPayloadTooLargeError
103
+ ? cause
104
+ : new EventStoreError('event size validation failed', cause),
105
+ });
159
106
  if (events.length === 0 && options?.fence === undefined) {
160
107
  const tail = yield* service.tail(runId);
161
108
  return { sequences: [], tail };
@@ -186,7 +133,7 @@ export function s2(config) {
186
133
  cause.status === 412 &&
187
134
  (message.includes('fenc') || message.includes('token'));
188
135
  if (isFence) {
189
- return yield* Effect.fail(new EventStoreFencedError(runId, options?.fencingToken ?? options?.fence ?? ''));
136
+ return yield* new EventStoreFencedError(runId, options?.fencingToken ?? options?.fence ?? '');
190
137
  }
191
138
  if (cause instanceof S2Error &&
192
139
  (cause.status === 409 ||
@@ -195,9 +142,9 @@ export function s2(config) {
195
142
  message.includes('match'))) {
196
143
  const tailCheck = yield* Effect.promise(() => stream.checkTail().catch(() => null));
197
144
  const actualTail = tailCheck?.tail?.seqNum ?? 0;
198
- return yield* Effect.fail(new EventStoreConflictError(runId, options?.expectedTail ?? 0, actualTail));
145
+ return yield* new EventStoreConflictError(runId, options?.expectedTail ?? 0, actualTail);
199
146
  }
200
- return yield* Effect.fail(toStoreError(cause, `Append failed for ${runId}`));
147
+ return yield* toStoreError(cause, `Append failed for ${runId}`);
201
148
  }
202
149
  const ack = appendResult.ack;
203
150
  const sequences = [];
@@ -213,23 +160,13 @@ export function s2(config) {
213
160
  return Stream.unwrap(Effect.gen(function* () {
214
161
  const stream = yield* ensureStream(runId);
215
162
  const fetchPage = (cursor, count) => Effect.tryPromise({
216
- try: async () => {
217
- try {
218
- return await stream.read({
219
- start: { from: { seqNum: cursor }, clamp: true },
220
- stop: { limits: { count } },
221
- ignoreCommandRecords: true,
222
- });
223
- }
224
- catch (cause) {
225
- if (isUnsatisfiableCause(cause)) {
226
- return null;
227
- }
228
- throw cause;
229
- }
230
- },
163
+ try: () => stream.read({
164
+ start: { from: { seqNum: cursor }, clamp: true },
165
+ stop: { limits: { count } },
166
+ ignoreCommandRecords: true,
167
+ }),
231
168
  catch: (cause) => toStoreError(cause, `Read failed for ${runId}`),
232
- });
169
+ }).pipe(Effect.catchIf((error) => isUnsatisfiableCause(error.cause), () => Effect.succeed(null)));
233
170
  const recordStream = paginateS2Stream(fetchPage, {
234
171
  fromSeq: s2From,
235
172
  limit: options?.limit,
@@ -246,31 +183,21 @@ export function s2(config) {
246
183
  const res = yield* Effect.tryPromise({
247
184
  try: () => stream.checkTail(),
248
185
  catch: (cause) => toStoreError(cause, `Tail failed for ${runId}`),
249
- }).pipe(Effect.catch((err) => err.cause instanceof S2Error && err.cause.status === 404
250
- ? Effect.succeed({ tail: { seqNum: 0, timestamp: new Date(0) } })
251
- : Effect.fail(err)));
186
+ }).pipe(Effect.catchIf((err) => err.cause instanceof S2Error && err.cause.status === 404, () => Effect.succeed({
187
+ tail: { seqNum: 0, timestamp: DateTime.toDateUtc(DateTime.makeUnsafe(0)) },
188
+ })));
252
189
  return res.tail.seqNum;
253
190
  }),
254
191
  bounds: (runId) => Effect.gen(function* () {
255
192
  const stream = yield* ensureStream(runId);
256
193
  const page = yield* Effect.tryPromise({
257
- try: async () => {
258
- try {
259
- return await stream.read({
260
- start: { from: { seqNum: 0 }, clamp: true },
261
- stop: { limits: { count: 1 } },
262
- ignoreCommandRecords: false,
263
- });
264
- }
265
- catch (cause) {
266
- if (isUnsatisfiableCause(cause)) {
267
- return null;
268
- }
269
- throw cause;
270
- }
271
- },
194
+ try: () => stream.read({
195
+ start: { from: { seqNum: 0 }, clamp: true },
196
+ stop: { limits: { count: 1 } },
197
+ ignoreCommandRecords: false,
198
+ }),
272
199
  catch: (cause) => toStoreError(cause, `Bounds failed for ${runId}`),
273
- });
200
+ }).pipe(Effect.catchIf((error) => isUnsatisfiableCause(error.cause), () => Effect.succeed(null)));
274
201
  const tail = page?.tail?.seqNum ?? 0;
275
202
  const first = page?.records[0];
276
203
  const head = first ? first.seqNum + 1 : tail + 1;
@@ -284,49 +211,28 @@ export function s2(config) {
284
211
  catch: (cause) => toStoreError(cause, `Trim failed for ${runId}`),
285
212
  });
286
213
  }),
287
- subscribe: (runId, options) => Stream.callback((queue) => Effect.callback((resume) => {
214
+ subscribe: (runId, options) => {
288
215
  const fromSeq = options?.fromSeq ?? 1;
289
216
  const s2From = Math.max(0, fromSeq - 1);
290
- let stopped = false;
291
- const run = async () => {
292
- try {
293
- const stream = await Effect.runPromise(ensureStream(runId));
294
- const session = await stream.readSession({
217
+ return Stream.unwrap(Effect.gen(function* () {
218
+ const stream = yield* ensureStream(runId);
219
+ const session = yield* Effect.tryPromise({
220
+ try: () => stream.readSession({
295
221
  start: { from: { seqNum: s2From }, clamp: true },
296
222
  ignoreCommandRecords: true,
297
- });
298
- for await (const record of session) {
299
- if (stopped) {
300
- break;
301
- }
302
- if (record.seqNum < s2From) {
303
- continue;
304
- }
305
- Queue.offerUnsafe(queue, parseEvent(record.body, record.seqNum));
306
- }
307
- Queue.endUnsafe(queue);
308
- resume(Effect.void);
309
- }
310
- catch (cause) {
311
- await Effect.runPromise(Queue.fail(queue, toStoreError(cause, `Subscribe failed for ${runId}`)));
312
- resume(Effect.void);
313
- }
314
- };
315
- void run();
316
- return Effect.sync(() => {
317
- stopped = true;
318
- });
319
- })),
320
- listRuns: () => Effect.tryPromise({
321
- try: async () => {
322
- const listed = await basin.streams.list({ prefix: 'runs/' });
323
- return listed.streams
324
- .map((s) => s.name)
325
- .filter((name) => name.startsWith('runs/'))
326
- .map((name) => name.slice('runs/'.length));
327
- },
223
+ }),
224
+ catch: (cause) => toStoreError(cause, `Subscribe failed for ${runId}`),
225
+ });
226
+ return Stream.fromAsyncIterable(session, (cause) => toStoreError(cause, `Subscribe failed for ${runId}`)).pipe(Stream.filter((record) => record.seqNum >= s2From), Stream.map((record) => parseEvent(record.body, record.seqNum)));
227
+ }));
228
+ },
229
+ listRuns: Effect.tryPromise({
230
+ try: () => basin.streams.list({ prefix: 'runs/' }),
328
231
  catch: (cause) => toStoreError(cause, 'listRuns failed'),
329
- }),
232
+ }).pipe(Effect.map((listed) => listed.streams
233
+ .map((stream) => stream.name)
234
+ .filter((name) => name.startsWith('runs/'))
235
+ .map((name) => name.slice('runs/'.length)))),
330
236
  };
331
237
  return withSnapshotStore(service, s2SnapshotStore(parsed));
332
238
  }
@@ -9,3 +9,18 @@ export interface TestRuntime {
9
9
  export declare function createTestRuntime(modules: readonly AnyRuntimeModule[]): Promise<TestRuntime>;
10
10
  export declare function assertReplayDeterministic(test: TestRuntime, runId: string): Promise<void>;
11
11
  export declare function moduleConformance(module: AnyRuntimeModule): string[];
12
+ export type EventStoreFaultOperation = 'append' | 'read' | 'tail' | 'bounds' | 'trim' | 'listRuns';
13
+ export interface EventStoreFaultContext {
14
+ readonly operation: EventStoreFaultOperation;
15
+ readonly runId: string | null;
16
+ readonly call: number;
17
+ }
18
+ export interface FaultInjectingEventStore {
19
+ readonly store: EventStore;
20
+ readonly calls: ReadonlyMap<EventStoreFaultOperation, number>;
21
+ }
22
+ /**
23
+ * Wraps an EventStore with deterministic delay/failure injection. Throw from
24
+ * `before` to fail a call, or await inside it to pause a race at an exact store boundary.
25
+ */
26
+ export declare function createFaultInjectingEventStore(source: EventStore, before: (context: EventStoreFaultContext) => void | Promise<void>): FaultInjectingEventStore;
@@ -1,5 +1,5 @@
1
1
  import { Effect, Predicate } from 'effect';
2
- import { composeModules, EventStoreTag, foldRun, makeMemoryEventStore, ModuleCompositionError, } from '@swirls/looms/core';
2
+ import { composeModules, EventStoreTag, foldRun, makeMemoryEventStore, ModuleCompositionError, EventStoreError, snapshotStoreOf, stringifyJson, withSnapshotStore, } from '@swirls/looms/core';
3
3
  import { createRuntime } from '@swirls/looms/runtime';
4
4
  function namedEffects(effects) {
5
5
  const listed = [];
@@ -15,22 +15,24 @@ function namedEffects(effects) {
15
15
  }
16
16
  return listed;
17
17
  }
18
- export async function createTestRuntime(modules) {
19
- const store = await Effect.runPromise(makeMemoryEventStore);
20
- const runtime = createRuntime({ modules, store });
21
- return {
22
- runtime,
23
- store,
24
- run: (effect) => Effect.runPromise(Effect.provideService(effect, EventStoreTag, store)),
25
- };
18
+ export function createTestRuntime(modules) {
19
+ return Effect.runPromise(makeMemoryEventStore.pipe(Effect.map((store) => {
20
+ const runtime = createRuntime({ modules, store });
21
+ return {
22
+ runtime,
23
+ store,
24
+ run: (effect) => Effect.runPromise(Effect.provideService(effect, EventStoreTag, store)),
25
+ };
26
+ })));
26
27
  }
27
- export async function assertReplayDeterministic(test, runId) {
28
- const events = await test.run(test.runtime.getEvents(runId));
29
- const first = foldRun(events, test.runtime.registry, { runId });
30
- const second = foldRun(events, test.runtime.registry, { runId });
31
- if (JSON.stringify(first) !== JSON.stringify(second)) {
32
- throw new Error(`Replay is not deterministic for run ${runId}`);
33
- }
28
+ export function assertReplayDeterministic(test, runId) {
29
+ return test.run(test.runtime.getEvents(runId)).then((events) => {
30
+ const first = foldRun(events, test.runtime.registry, { runId });
31
+ const second = foldRun(events, test.runtime.registry, { runId });
32
+ if (stringifyJson(first) !== stringifyJson(second)) {
33
+ throw new Error(`Replay is not deterministic for run ${runId}`);
34
+ }
35
+ });
34
36
  }
35
37
  export function moduleConformance(module) {
36
38
  const errors = [];
@@ -61,3 +63,38 @@ export function moduleConformance(module) {
61
63
  }
62
64
  return errors;
63
65
  }
66
+ /**
67
+ * Wraps an EventStore with deterministic delay/failure injection. Throw from
68
+ * `before` to fail a call, or await inside it to pause a race at an exact store boundary.
69
+ */
70
+ export function createFaultInjectingEventStore(source, before) {
71
+ const calls = new Map();
72
+ const inject = (operation, runId) => Effect.tryPromise({
73
+ try: () => {
74
+ const call = (calls.get(operation) ?? 0) + 1;
75
+ calls.set(operation, call);
76
+ return Promise.resolve(before({ operation, runId, call }));
77
+ },
78
+ catch: (cause) => cause instanceof EventStoreError
79
+ ? cause
80
+ : new EventStoreError(`Injected ${operation} fault`, cause),
81
+ });
82
+ const store = {
83
+ append: (runId, events, options) => inject('append', runId).pipe(Effect.andThen(source.append(runId, events, options))),
84
+ read: (runId, options) => inject('read', runId).pipe(Effect.andThen(source.read(runId, options))),
85
+ tail: (runId) => inject('tail', runId).pipe(Effect.andThen(source.tail(runId))),
86
+ subscribe: source.subscribe,
87
+ listRuns: inject('listRuns', null).pipe(Effect.andThen(source.listRuns)),
88
+ };
89
+ if (source.bounds) {
90
+ store.bounds = (runId) => inject('bounds', runId).pipe(Effect.andThen(source.bounds(runId)));
91
+ }
92
+ if (source.trim) {
93
+ store.trim = (runId, beforeSeq) => inject('trim', runId).pipe(Effect.andThen(source.trim(runId, beforeSeq)));
94
+ }
95
+ const snapshots = snapshotStoreOf(source);
96
+ if (snapshots) {
97
+ withSnapshotStore(store, snapshots);
98
+ }
99
+ return { store, calls };
100
+ }
@@ -1,8 +1,7 @@
1
1
  import { Context, Layer } from 'effect';
2
+ import { type DefinitionStore } from '@swirls/looms/core';
2
3
  import type { WorkflowDefinition } from './definitions';
3
- export interface WorkflowDefinitionStore {
4
- get(name: string): WorkflowDefinition | undefined;
5
- }
4
+ export type WorkflowDefinitionStore = DefinitionStore<WorkflowDefinition>;
6
5
  declare const WorkflowDefinitionsTag_base: Context.ServiceClass<WorkflowDefinitionsTag, "looms/WorkflowDefinitions", WorkflowDefinitionStore>;
7
6
  export declare class WorkflowDefinitionsTag extends WorkflowDefinitionsTag_base {
8
7
  }
@@ -1,8 +1,8 @@
1
1
  import { Context, Layer } from 'effect';
2
+ import { makeDefinitionStore } from '@swirls/looms/core';
2
3
  export class WorkflowDefinitionsTag extends Context.Service()('looms/WorkflowDefinitions') {
3
4
  }
4
5
  export function makeWorkflowDefinitionStore(definitions) {
5
- const map = new Map(definitions.map((def) => [def.name, def]));
6
- return { get: (name) => map.get(name) };
6
+ return makeDefinitionStore('workflow', definitions);
7
7
  }
8
8
  export const WorkflowDefinitionsLive = (definitions) => Layer.succeed(WorkflowDefinitionsTag, makeWorkflowDefinitionStore(definitions));
@@ -1,14 +1,34 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import { Schema } from 'effect';
3
- import type { InferDefinedSchema, JsonValue, RuntimeEffect } from '@swirls/looms/core';
3
+ import { type JsonValue, type RuntimeEffect } from '@swirls/looms/core';
4
+ import { type WorkflowEdge } from './graph';
4
5
  export declare const NodeStatusSchema: Schema.Union<readonly [Schema.Literal<"pending">, Schema.Literal<"running">, Schema.Literal<"completed">, Schema.Literal<"failed">, Schema.Literal<"skipped">]>;
5
6
  export type NodeStatus = Schema.Schema.Type<typeof NodeStatusSchema>;
6
7
  export declare const NodeStateSchema: Schema.Struct<{
7
8
  readonly status: Schema.Union<readonly [Schema.Literal<"pending">, Schema.Literal<"running">, Schema.Literal<"completed">, Schema.Literal<"failed">, Schema.Literal<"skipped">]>;
8
9
  readonly result: Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
9
10
  readonly error: Schema.NullOr<Schema.String>;
11
+ readonly attempts: Schema.optional<Schema.Finite>;
12
+ readonly branch: Schema.optional<Schema.String>;
13
+ /** Policy skips (skip/fallback) still satisfy dependents; inactive skips propagate. */
14
+ readonly skipCause: Schema.optional<Schema.Union<readonly [Schema.Literal<"inactive">, Schema.Literal<"policy">]>>;
15
+ readonly iteration: Schema.optional<Schema.Struct<{
16
+ readonly index: Schema.Finite;
17
+ readonly previous: Schema.Codec<Schema.Json, Schema.Json, never, never>;
18
+ }>>;
10
19
  }>;
11
20
  export type NodeState = Schema.Schema.Type<typeof NodeStateSchema>;
21
+ export interface WorkflowChild {
22
+ readonly kind: string;
23
+ readonly name: string;
24
+ readonly version?: string;
25
+ }
26
+ export interface WorkflowIteration {
27
+ /** Zero-based index of the loop iteration about to run. */
28
+ readonly index: number;
29
+ /** Output of the previous iteration's child. */
30
+ readonly previous: JsonValue;
31
+ }
12
32
  export interface WorkflowNodeContext<TInput = JsonValue> {
13
33
  readonly threadId: string;
14
34
  readonly nodeId: string;
@@ -16,12 +36,22 @@ export interface WorkflowNodeContext<TInput = JsonValue> {
16
36
  readonly results: {
17
37
  [nodeId: string]: JsonValue | null;
18
38
  };
19
- spawn(child: {
20
- kind: string;
21
- name: string;
22
- }, input: JsonValue): NodeResult;
39
+ /** Set when a `loop` node is re-run after one of its iterations completed. */
40
+ readonly iteration: WorkflowIteration | undefined;
41
+ spawn(child: WorkflowChild, input: JsonValue): NodeResult;
23
42
  sleep(ms: number): NodeResult;
24
43
  effects(effects: RuntimeEffect[]): NodeResult;
44
+ /** Run one child per item, one at a time, and finish with the ordered outputs. */
45
+ map(child: WorkflowChild, items: readonly JsonValue[]): NodeResult;
46
+ /** Run one child per item with bounded concurrency and finish with the ordered outputs. */
47
+ fanout(child: WorkflowChild, items: readonly JsonValue[], concurrency?: number): NodeResult;
48
+ /**
49
+ * Run one child iteration, then re-run this node with `ctx.iteration`. Returning a value
50
+ * instead ends the loop; reaching `maxIterations` ends it with the last iteration's output.
51
+ */
52
+ loop(child: WorkflowChild, input: JsonValue, options: {
53
+ maxIterations: number;
54
+ }): NodeResult;
25
55
  }
26
56
  export type NodeResult = {
27
57
  type: 'value';
@@ -30,26 +60,64 @@ export type NodeResult = {
30
60
  type: 'spawn';
31
61
  kind: string;
32
62
  name: string;
63
+ version: string;
33
64
  input: JsonValue;
65
+ childThreadId?: string;
34
66
  } | {
35
67
  type: 'sleep';
36
68
  ms: number;
37
69
  } | {
38
70
  type: 'effects';
39
71
  effects: RuntimeEffect[];
72
+ } | {
73
+ type: 'switch';
74
+ branch: string;
75
+ value?: JsonValue;
76
+ } | {
77
+ type: 'map';
78
+ kind: string;
79
+ name: string;
80
+ version: string;
81
+ items: readonly JsonValue[];
82
+ concurrency: number;
83
+ } | {
84
+ type: 'loop';
85
+ kind: string;
86
+ name: string;
87
+ version: string;
88
+ input: JsonValue;
89
+ maxIterations: number;
90
+ };
91
+ export type WorkflowFailurePolicy = {
92
+ readonly type: 'fail';
93
+ } | {
94
+ readonly type: 'skip';
95
+ } | {
96
+ readonly type: 'retry';
97
+ readonly maxAttempts: number;
98
+ } | {
99
+ readonly type: 'fallback';
100
+ readonly nodeId: string;
40
101
  };
41
102
  export interface WorkflowNodeDefinition<TInput = JsonValue> {
42
103
  id: string;
43
104
  deps?: string[];
105
+ type?: 'task' | 'switch' | 'workflow';
106
+ failure?: WorkflowFailurePolicy;
44
107
  run(ctx: WorkflowNodeContext<TInput>): Promise<JsonValue | NodeResult> | JsonValue | NodeResult;
45
108
  }
46
109
  export interface WorkflowDefinition<TName extends string = string, TInput = JsonValue, TOutput extends JsonValue = JsonValue> {
47
110
  readonly kind: 'workflow';
48
111
  readonly name: TName;
112
+ readonly version: string;
49
113
  readonly description?: string;
50
- readonly input?: StandardSchemaV1<any, TInput> | Schema.Schema<TInput>;
114
+ readonly input?: StandardSchemaV1<any, TInput> | Schema.ConstraintDecoder<TInput>;
51
115
  readonly nodes: WorkflowNodeDefinition<TInput>[];
116
+ readonly edges?: readonly WorkflowEdge[];
52
117
  readonly concurrency?: number;
118
+ readonly inputLimitBytes?: number;
119
+ readonly outputLimitBytes?: number;
120
+ readonly checkpointLimitBytes?: number;
53
121
  output?(ctx: {
54
122
  input: TInput;
55
123
  results: {
@@ -58,25 +126,13 @@ export interface WorkflowDefinition<TName extends string = string, TInput = Json
58
126
  }): TOutput;
59
127
  }
60
128
  export type AnyWorkflowDefinition = WorkflowDefinition;
61
- export declare function defineWorkflow<TName extends string, TSchema = undefined, TInput = InferDefinedSchema<TSchema>, TOutput extends JsonValue = JsonValue>(def: {
62
- name: TName;
63
- description?: string;
64
- input?: TSchema;
65
- nodes: Array<{
66
- id: string;
67
- deps?: string[];
68
- run: (ctx: WorkflowNodeContext<TInput>) => Promise<JsonValue | NodeResult> | JsonValue | NodeResult;
69
- }>;
70
- concurrency?: number;
71
- output?: (ctx: {
72
- input: TInput;
73
- results: {
74
- [nodeId: string]: JsonValue | null;
75
- };
76
- }) => TOutput;
129
+ export declare function defineWorkflow<TName extends string, TInput = JsonValue, TOutput extends JsonValue = JsonValue>(def: Omit<WorkflowDefinition<TName, TInput, TOutput>, 'kind' | 'version'> & {
130
+ readonly version?: string;
77
131
  }): WorkflowDefinition<TName, TInput, TOutput>;
78
132
  export declare function readyNodes(workflow: WorkflowDefinition, nodeStates: {
79
133
  [nodeId: string]: {
80
134
  status: string;
81
135
  };
82
136
  }): string[];
137
+ /** Builds the helpers a node's `run` uses to describe durable work. */
138
+ export declare function nodeResultBuilders(): Pick<WorkflowNodeContext, 'spawn' | 'sleep' | 'effects' | 'map' | 'fanout' | 'loop'>;