@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
@@ -1,19 +1,23 @@
1
1
  import { AppendInput, AppendRecord, S2, S2Endpoints, S2Error } from '@s2-dev/streamstore';
2
- import { Effect, Layer, Option, Schema } from 'effect';
3
- import { createKeyedSerializer, SnapshotStoreError, SnapshotStoreTag, } from '@swirls/looms/core';
2
+ import { Effect, Layer, Option, Schema, Stream } from 'effect';
3
+ import { createKeyedSerializer, assertSnapshotByteLimit, RunStateSchema, SnapshotStoreError, SnapshotPayloadTooLargeError, SnapshotStoreTag, } from '@swirls/looms/core';
4
4
  import { S2ConfigSchema } from './config.js';
5
+ import { paginateS2Stream } from './pagination.js';
5
6
  const DEFAULT_PREFIX = 'snapshots';
6
7
  const DEFAULT_MAX_CHUNK_BYTES = 900 * 1024;
7
8
  const SnapshotFrameSchema = Schema.Struct({
8
9
  v: Schema.Literal(1),
9
10
  runId: Schema.String,
10
- cursor: Schema.Number,
11
+ cursor: Schema.Finite,
11
12
  stateHash: Schema.String,
12
- takenAt: Schema.Number,
13
- chunk: Schema.Number,
14
- chunks: Schema.Number,
13
+ takenAt: Schema.Finite,
14
+ chunk: Schema.Finite,
15
+ chunks: Schema.Finite,
15
16
  data: Schema.String,
16
17
  });
18
+ const SnapshotFrameJson = Schema.fromJsonString(SnapshotFrameSchema);
19
+ const decodeFrame = Schema.decodeSync(SnapshotFrameJson);
20
+ const encodeFrame = Schema.encodeSync(SnapshotFrameJson);
17
21
  export function frameSnapshot(snapshot, maxChunkBytes = DEFAULT_MAX_CHUNK_BYTES) {
18
22
  const json = JSON.stringify(snapshot.state);
19
23
  const chunks = Math.max(1, Math.ceil(json.length / maxChunkBytes));
@@ -37,6 +41,8 @@ export function assembleSnapshot(frames) {
37
41
  throw new SnapshotStoreError('Cannot assemble snapshot from empty frames');
38
42
  }
39
43
  const first = frames[0];
44
+ // Oxlint's immutable alternative requires ES2023; sorting a fresh copy is safe on ES2022.
45
+ // oxlint-disable-next-line unicorn/no-array-sort
40
46
  const ordered = [...frames].sort((a, b) => a.chunk - b.chunk);
41
47
  for (const frame of ordered) {
42
48
  if (frame.cursor !== first.cursor ||
@@ -54,8 +60,7 @@ export function assembleSnapshot(frames) {
54
60
  }
55
61
  }
56
62
  const json = ordered.map((frame) => frame.data).join('');
57
- // SAFETY: frames are produced by `frameSnapshot` from a serialized RunState.
58
- const state = JSON.parse(json);
63
+ const state = Schema.decodeSync(Schema.fromJsonString(RunStateSchema))(json);
59
64
  return {
60
65
  runId: first.runId,
61
66
  cursor: first.cursor,
@@ -84,74 +89,56 @@ function createClient(config) {
84
89
  }
85
90
  function parseFrame(body) {
86
91
  try {
87
- return Schema.decodeUnknownSync(SnapshotFrameSchema)(JSON.parse(body));
92
+ return decodeFrame(body);
88
93
  }
89
94
  catch {
90
95
  return undefined;
91
96
  }
92
97
  }
98
+ function toSnapshotError(cause, message) {
99
+ return cause instanceof SnapshotStoreError
100
+ ? cause
101
+ : new SnapshotStoreError(`${message}: ${cause instanceof Error ? cause.message : String(cause)}`, cause);
102
+ }
93
103
  /**
94
104
  * Trailing command records (the trim written by `prune`) are skipped by
95
105
  * `ignoreCommandRecords`, so a `tailOffset: 1` read can come back empty.
96
106
  * Probe a few records from the tail instead.
97
107
  */
98
108
  const TAIL_PROBE = 4;
99
- async function readFromTail(stream, tailOffset) {
100
- try {
101
- const page = await stream.read({
109
+ function readFromTail(stream, tailOffset) {
110
+ return Effect.tryPromise({
111
+ try: () => stream.read({
102
112
  start: { from: { tailOffset } },
103
113
  ignoreCommandRecords: true,
104
- });
105
- return [...page.records];
106
- }
107
- catch (cause) {
108
- if (cause instanceof S2Error && (cause.status === 404 || cause.status === 416)) {
109
- return null;
110
- }
111
- throw cause;
112
- }
114
+ }),
115
+ catch: (cause) => toSnapshotError(cause, 'Failed to read snapshot stream tail'),
116
+ }).pipe(Effect.map((page) => [...page.records]), Effect.catchIf((error) => error.cause instanceof S2Error &&
117
+ (error.cause.status === 404 || error.cause.status === 416), () => Effect.succeed([])));
113
118
  }
114
119
  /** Latest complete data frame near the tail, with its record, or undefined. */
115
- async function readLatestFrame(stream) {
116
- const records = (await readFromTail(stream, TAIL_PROBE)) ?? [];
117
- for (let i = records.length - 1; i >= 0; i--) {
118
- const record = records[i];
119
- const frame = parseFrame(record.body);
120
- if (frame) {
121
- return { record, frame };
120
+ function readLatestFrame(stream) {
121
+ return readFromTail(stream, TAIL_PROBE).pipe(Effect.map((records) => {
122
+ for (let i = records.length - 1; i >= 0; i--) {
123
+ const record = records[i];
124
+ const frame = parseFrame(record.body);
125
+ if (frame) {
126
+ return { record, frame };
127
+ }
122
128
  }
123
- }
124
- return undefined;
129
+ return undefined;
130
+ }));
125
131
  }
126
- async function readAllSnapshotRecords(stream) {
127
- const records = [];
128
- let cursor = 0;
129
- while (true) {
130
- const page = await stream
131
- .read({
132
+ function readAllSnapshotRecords(stream) {
133
+ return paginateS2Stream((cursor, count) => Effect.tryPromise({
134
+ try: () => stream.read({
132
135
  start: { from: { seqNum: cursor }, clamp: true },
133
- stop: { limits: { count: 1000 } },
136
+ stop: { limits: { count } },
134
137
  ignoreCommandRecords: true,
135
- })
136
- .catch((cause) => {
137
- // 404: no stream yet. 416: nothing at or past `cursor` (e.g. only a trim record).
138
- if (cause instanceof S2Error && (cause.status === 404 || cause.status === 416)) {
139
- return null;
140
- }
141
- throw cause;
142
- });
143
- if (!page || page.records.length === 0) {
144
- break;
145
- }
146
- records.push(...page.records);
147
- const last = page.records[page.records.length - 1];
148
- const next = last.seqNum + 1;
149
- if (page.tail?.seqNum !== undefined && next >= page.tail.seqNum) {
150
- break;
151
- }
152
- cursor = next;
153
- }
154
- return records;
138
+ }),
139
+ catch: (cause) => toSnapshotError(cause, 'Failed to scan snapshot stream'),
140
+ }).pipe(Effect.catchIf((error) => error.cause instanceof S2Error &&
141
+ (error.cause.status === 404 || error.cause.status === 416), () => Effect.succeed(null)))).pipe(Stream.runCollect);
155
142
  }
156
143
  function groupCompleteSnapshots(records) {
157
144
  const byCursor = new Map();
@@ -173,10 +160,12 @@ function groupCompleteSnapshots(records) {
173
160
  continue;
174
161
  }
175
162
  }
163
+ // `assembled` is function-local, so in-place sorting cannot mutate caller-owned data.
164
+ // oxlint-disable-next-line unicorn/no-array-sort
176
165
  return assembled.sort((a, b) => a.cursor - b.cursor);
177
166
  }
178
167
  export function s2SnapshotStore(config) {
179
- const parsed = Schema.decodeUnknownSync(S2ConfigSchema)(config);
168
+ const parsed = Schema.decodeSync(S2ConfigSchema)(config);
180
169
  const client = createClient(parsed);
181
170
  const basin = client.basin(parsed.basin);
182
171
  const prefix = parsed.snapshotPrefix ?? DEFAULT_PREFIX;
@@ -184,133 +173,129 @@ export function s2SnapshotStore(config) {
184
173
  const ensured = new Set();
185
174
  let basinEnsured = false;
186
175
  const streamName = (runId) => `${prefix}/${runId}`;
187
- const ensureBasin = async () => {
176
+ const ensureBasin = Effect.suspend(() => {
188
177
  if (basinEnsured) {
189
- return;
190
- }
191
- try {
192
- await client.basins.create({ basin: parsed.basin });
178
+ return Effect.void;
193
179
  }
194
- catch (err) {
195
- if (!(err instanceof S2Error && err.status === 409)) {
196
- throw err;
197
- }
198
- }
199
- basinEnsured = true;
200
- };
201
- const ensureStream = async (runId) => {
202
- await ensureBasin();
180
+ return Effect.tryPromise({
181
+ try: () => client.basins.create({ basin: parsed.basin }),
182
+ catch: (cause) => toSnapshotError(cause, `Failed to ensure basin ${parsed.basin}`),
183
+ }).pipe(Effect.catchIf((error) => error.cause instanceof S2Error && error.cause.status === 409, () => Effect.void), Effect.tap(() => Effect.sync(() => {
184
+ basinEnsured = true;
185
+ })), Effect.asVoid);
186
+ });
187
+ const ensureStream = (runId) => Effect.gen(function* () {
188
+ yield* ensureBasin;
203
189
  const name = streamName(runId);
204
190
  if (!ensured.has(name)) {
205
- try {
206
- await basin.streams.create({
191
+ yield* Effect.tryPromise({
192
+ try: () => basin.streams.create({
207
193
  stream: name,
208
194
  config: { retentionPolicy: { infinite: {} } },
209
- });
210
- }
211
- catch (err) {
212
- if (!(err instanceof S2Error &&
213
- (err.status === 409 || err.status === 404 || err.status === 405))) {
214
- throw err;
215
- }
216
- }
195
+ }),
196
+ catch: (cause) => toSnapshotError(cause, `Failed to ensure stream ${name}`),
197
+ }).pipe(Effect.catchIf((error) => error.cause instanceof S2Error &&
198
+ (error.cause.status === 409 ||
199
+ error.cause.status === 404 ||
200
+ error.cause.status === 405), () => Effect.void));
217
201
  ensured.add(name);
218
202
  }
219
203
  return basin.stream(name);
220
- };
221
- const toError = (cause, message) => cause instanceof SnapshotStoreError
222
- ? cause
223
- : new SnapshotStoreError(`${message}: ${cause instanceof Error ? cause.message : String(cause)}`, cause);
204
+ });
224
205
  const service = {
225
- save: (snapshot) => Effect.tryPromise({
226
- try: async () => {
227
- const stream = await ensureStream(snapshot.runId);
228
- const frames = frameSnapshot(snapshot);
229
- const records = frames.map((frame) => AppendRecord.string({
230
- body: JSON.stringify(frame),
231
- headers: [
232
- ['looms-cursor', String(frame.cursor)],
233
- ['looms-chunk', `${frame.chunk}/${frame.chunks}`],
234
- ],
235
- }));
236
- await appends.run(snapshot.runId, async () => {
206
+ save: (snapshot) => Effect.gen(function* () {
207
+ yield* Effect.try({
208
+ try: () => assertSnapshotByteLimit(snapshot),
209
+ catch: (cause) => cause instanceof SnapshotPayloadTooLargeError
210
+ ? cause
211
+ : new SnapshotStoreError('snapshot size validation failed', cause),
212
+ });
213
+ const stream = yield* ensureStream(snapshot.runId);
214
+ const frames = frameSnapshot(snapshot);
215
+ const records = frames.map((frame) => AppendRecord.string({
216
+ body: encodeFrame(frame),
217
+ headers: [
218
+ ['looms-cursor', String(frame.cursor)],
219
+ ['looms-chunk', `${frame.chunk}/${frame.chunks}`],
220
+ ],
221
+ }));
222
+ yield* Effect.tryPromise({
223
+ try: () => appends.run(snapshot.runId, () => {
237
224
  if (records.length === 1) {
238
- await stream.append(AppendInput.create(records));
239
- return;
225
+ return stream.append(AppendInput.create(records)).then(() => undefined);
240
226
  }
227
+ let pending = Promise.resolve();
241
228
  for (const record of records) {
242
- await stream.append(AppendInput.create([record]));
229
+ pending = pending.then(() => stream.append(AppendInput.create([record])).then(() => undefined));
243
230
  }
244
- });
245
- },
246
- catch: (cause) => toError(cause, `Failed to save snapshot for ${snapshot.runId}`),
231
+ return pending;
232
+ }),
233
+ catch: (cause) => toSnapshotError(cause, `Failed to save snapshot for ${snapshot.runId}`),
234
+ });
247
235
  }),
248
- loadLatest: (runId) => Effect.tryPromise({
249
- try: async () => {
250
- const stream = await ensureStream(runId);
251
- const tailFrame = await readLatestFrame(stream);
252
- if (!tailFrame) {
253
- return Option.none();
254
- }
255
- if (tailFrame.frame.chunk === tailFrame.frame.chunks - 1) {
256
- const lastFrame = tailFrame.frame;
257
- const records = (await readFromTail(stream, lastFrame.chunks + TAIL_PROBE)) ?? [];
258
- const frames = records
259
- .map((record) => parseFrame(record.body))
260
- .filter((frame) => frame !== undefined)
261
- .filter((frame) => frame.cursor === lastFrame.cursor);
262
- if (frames.length === lastFrame.chunks) {
263
- return Option.some(assembleSnapshot(frames));
264
- }
236
+ loadLatest: (runId) => Effect.gen(function* () {
237
+ const stream = yield* ensureStream(runId);
238
+ const tailFrame = yield* readLatestFrame(stream);
239
+ if (!tailFrame) {
240
+ return Option.none();
241
+ }
242
+ if (tailFrame.frame.chunk === tailFrame.frame.chunks - 1) {
243
+ const lastFrame = tailFrame.frame;
244
+ const records = (yield* readFromTail(stream, lastFrame.chunks + TAIL_PROBE)) ?? [];
245
+ const frames = records
246
+ .map((record) => parseFrame(record.body))
247
+ .filter((frame) => frame !== undefined)
248
+ .filter((frame) => frame.cursor === lastFrame.cursor);
249
+ if (frames.length === lastFrame.chunks) {
250
+ return Option.some(assembleSnapshot(frames));
265
251
  }
266
- // Incomplete or interleaved tail: fall back to scanning the retained stream.
267
- const records = await readAllSnapshotRecords(stream);
268
- const complete = groupCompleteSnapshots(records);
269
- const newest = complete[complete.length - 1];
270
- return newest ? Option.some(newest) : Option.none();
271
- },
272
- catch: (cause) => toError(cause, `Failed to load snapshot for ${runId}`),
252
+ }
253
+ // Incomplete or interleaved tail: fall back to scanning the retained stream.
254
+ const records = yield* readAllSnapshotRecords(stream);
255
+ const complete = groupCompleteSnapshots(records);
256
+ const newest = complete[complete.length - 1];
257
+ return newest ? Option.some(newest) : Option.none();
273
258
  }),
274
- listCursors: (runId) => Effect.tryPromise({
275
- try: async () => {
276
- const stream = await ensureStream(runId);
277
- const records = await readAllSnapshotRecords(stream);
278
- return groupCompleteSnapshots(records).map((snapshot) => snapshot.cursor);
279
- },
280
- catch: (cause) => toError(cause, `Failed to list snapshot cursors for ${runId}`),
259
+ listCursors: (runId) => Effect.gen(function* () {
260
+ const stream = yield* ensureStream(runId);
261
+ const records = yield* readAllSnapshotRecords(stream);
262
+ return groupCompleteSnapshots(records).map((snapshot) => snapshot.cursor);
281
263
  }),
282
- prune: (runId, keepLatest) => Effect.tryPromise({
283
- try: async () => {
284
- const stream = await ensureStream(runId);
285
- if (keepLatest <= 1) {
286
- // Fast path: the latest complete frame tells us where the latest snapshot starts.
287
- const latest = await readLatestFrame(stream);
288
- if (!latest || latest.frame.chunk !== latest.frame.chunks - 1) {
289
- return;
290
- }
291
- const start = latest.record.seqNum - latest.frame.chunk;
292
- if (start <= 0) {
293
- return;
294
- }
295
- await appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(start)])));
296
- return;
297
- }
298
- const records = await readAllSnapshotRecords(stream);
299
- const complete = groupCompleteSnapshots(records);
300
- if (complete.length <= keepLatest) {
301
- return;
302
- }
303
- const keep = complete[complete.length - keepLatest];
304
- if (!keep) {
264
+ prune: (runId, keepLatest) => Effect.gen(function* () {
265
+ const stream = yield* ensureStream(runId);
266
+ if (keepLatest <= 1) {
267
+ // Fast path: the latest complete frame tells us where the latest snapshot starts.
268
+ const latest = yield* readLatestFrame(stream);
269
+ if (!latest || latest.frame.chunk !== latest.frame.chunks - 1) {
305
270
  return;
306
271
  }
307
- const firstKept = records.find((record) => parseFrame(record.body)?.cursor === keep.cursor);
308
- if (!firstKept) {
272
+ const start = latest.record.seqNum - latest.frame.chunk;
273
+ if (start <= 0) {
309
274
  return;
310
275
  }
311
- await appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(firstKept.seqNum)])));
312
- },
313
- catch: (cause) => toError(cause, `Failed to prune snapshots for ${runId}`),
276
+ yield* Effect.tryPromise({
277
+ try: () => appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(start)]))),
278
+ catch: (cause) => toSnapshotError(cause, `Failed to prune snapshots for ${runId}`),
279
+ });
280
+ return;
281
+ }
282
+ const records = yield* readAllSnapshotRecords(stream);
283
+ const complete = groupCompleteSnapshots(records);
284
+ if (complete.length <= keepLatest) {
285
+ return;
286
+ }
287
+ const keep = complete[complete.length - keepLatest];
288
+ if (!keep) {
289
+ return;
290
+ }
291
+ const firstKept = records.find((record) => parseFrame(record.body)?.cursor === keep.cursor);
292
+ if (!firstKept) {
293
+ return;
294
+ }
295
+ yield* Effect.tryPromise({
296
+ try: () => appends.run(runId, () => stream.append(AppendInput.create([AppendRecord.trim(firstKept.seqNum)]))),
297
+ catch: (cause) => toSnapshotError(cause, `Failed to prune snapshots for ${runId}`),
298
+ });
314
299
  }),
315
300
  };
316
301
  return service;
@@ -1,26 +1,8 @@
1
- import { Effect, Layer, Stream } from 'effect';
1
+ import { Effect, Layer } from 'effect';
2
2
  import { EventStoreError, EventStoreTag, type EventStore } from '@swirls/looms/core';
3
3
  import { type S2Config } from './config';
4
- export interface S2ReadBatchLike<T> {
5
- readonly records: ReadonlyArray<T>;
6
- readonly tail?: {
7
- readonly seqNum: number;
8
- } | null;
9
- }
10
- export interface ReadAllPagesOptions {
11
- readonly fromSeq?: number;
12
- readonly limit?: number;
13
- readonly maxPageSize?: number;
14
- }
15
- /**
16
- * Paginate an S2 stream lazily using Effect Streams.
17
- *
18
- * Pulls pages on demand until the stream tail is reached, an empty page is returned,
19
- * or the caller's optional limit is satisfied.
20
- */
21
- export declare function paginateS2Stream<T extends {
22
- readonly seqNum: number;
23
- }>(fetchPage: (cursor: number, count: number) => Effect.Effect<S2ReadBatchLike<T> | null, EventStoreError>, options?: ReadAllPagesOptions): Stream.Stream<T, EventStoreError>;
4
+ import { paginateS2Stream, type ReadAllPagesOptions, type S2ReadBatchLike } from './pagination';
5
+ export { paginateS2Stream, type ReadAllPagesOptions, type S2ReadBatchLike };
24
6
  export declare function readAllPages<T extends {
25
7
  readonly seqNum: number;
26
8
  }>(fetchPage: (cursor: number, count: number) => Promise<S2ReadBatchLike<T> | null>, options?: ReadAllPagesOptions): Promise<T[]>;