@swirls/looms 0.0.1 → 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 (257) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +2 -0
  3. package/dist/actor/cell.d.ts +9 -4
  4. package/dist/actor/cell.js +7 -11
  5. package/dist/actor/host.d.ts +7 -3
  6. package/dist/actor/host.js +55 -32
  7. package/dist/actor/index.d.ts +1 -1
  8. package/dist/actor/index.js +1 -1
  9. package/dist/actor/routing.d.ts +12 -1
  10. package/dist/actor/routing.js +57 -51
  11. package/dist/agent/agent-session.d.ts +5 -0
  12. package/dist/agent/agent-session.js +270 -0
  13. package/dist/agent/definitions-store.d.ts +2 -3
  14. package/dist/agent/definitions-store.js +2 -4
  15. package/dist/agent/definitions.d.ts +46 -20
  16. package/dist/agent/definitions.js +36 -8
  17. package/dist/agent/effects.d.ts +359 -2
  18. package/dist/agent/effects.js +60 -34
  19. package/dist/agent/events.d.ts +175 -19
  20. package/dist/agent/events.js +45 -10
  21. package/dist/agent/index.d.ts +1 -0
  22. package/dist/agent/index.js +1 -0
  23. package/dist/agent/llm.d.ts +11 -2
  24. package/dist/agent/llm.js +13 -3
  25. package/dist/agent/module.d.ts +812 -78
  26. package/dist/agent/module.js +16 -4
  27. package/dist/agent/scope.d.ts +174 -18
  28. package/dist/agent/signals.d.ts +24 -1
  29. package/dist/agent/signals.js +77 -0
  30. package/dist/agent/threads.d.ts +3 -33
  31. package/dist/agent/threads.js +20 -25
  32. package/dist/agent/tool-schema.js +0 -1
  33. package/dist/agent/types.d.ts +49 -4
  34. package/dist/agent/types.js +5 -5
  35. package/dist/ai-vercel/index.d.ts +1 -0
  36. package/dist/ai-vercel/index.js +1 -0
  37. package/dist/ai-vercel/messages.js +22 -7
  38. package/dist/ai-vercel/vercel-evaluator.d.ts +23 -0
  39. package/dist/ai-vercel/vercel-evaluator.js +68 -0
  40. package/dist/ai-vercel/vercel-llm.js +64 -44
  41. package/dist/approval/effects.d.ts +89 -0
  42. package/dist/approval/module.d.ts +179 -29
  43. package/dist/approval/projections.d.ts +0 -14
  44. package/dist/approval/projections.js +20 -3
  45. package/dist/approval/signals.d.ts +2 -0
  46. package/dist/approval/signals.js +18 -2
  47. package/dist/cli/cli.js +2 -0
  48. package/dist/cli/commands.js +12 -16
  49. package/dist/client/client.d.ts +1195 -18
  50. package/dist/client/client.js +118 -120
  51. package/dist/client/index.d.ts +1 -1
  52. package/dist/client/index.js +1 -1
  53. package/dist/client/sse.d.ts +13 -0
  54. package/dist/client/sse.js +38 -44
  55. package/dist/cloudflare/alarm-scheduler.d.ts +12 -2
  56. package/dist/cloudflare/alarm-scheduler.js +34 -7
  57. package/dist/cloudflare/durable-object.d.ts +10 -2
  58. package/dist/cloudflare/durable-object.js +59 -28
  59. package/dist/cloudflare/event-store.d.ts +7 -2
  60. package/dist/cloudflare/event-store.js +8 -7
  61. package/dist/cloudflare/index.d.ts +2 -2
  62. package/dist/cloudflare/index.js +2 -2
  63. package/dist/cloudflare/router.js +13 -14
  64. package/dist/core/catalog.d.ts +3 -3
  65. package/dist/core/catalog.js +2 -5
  66. package/dist/core/coalesce.js +5 -3
  67. package/dist/core/compose.d.ts +2 -1
  68. package/dist/core/compose.js +18 -2
  69. package/dist/core/definition-store.d.ts +11 -0
  70. package/dist/core/definition-store.js +6 -0
  71. package/dist/core/effects.d.ts +127 -15
  72. package/dist/core/effects.js +89 -17
  73. package/dist/core/envelope.d.ts +27 -7
  74. package/dist/core/envelope.js +20 -6
  75. package/dist/core/event-codec.d.ts +4 -2
  76. package/dist/core/event-codec.js +10 -5
  77. package/dist/core/event-index.js +1 -3
  78. package/dist/core/fold.d.ts +3 -0
  79. package/dist/core/fold.js +171 -24
  80. package/dist/core/ids.d.ts +5 -0
  81. package/dist/core/ids.js +35 -2
  82. package/dist/core/index.d.ts +5 -1
  83. package/dist/core/index.js +5 -1
  84. package/dist/core/kind.d.ts +22 -0
  85. package/dist/core/kind.js +17 -0
  86. package/dist/core/limits.d.ts +34 -0
  87. package/dist/core/limits.js +46 -0
  88. package/dist/core/match.d.ts +1 -0
  89. package/dist/core/match.js +10 -1
  90. package/dist/core/module-scope.d.ts +22 -20
  91. package/dist/core/module-scope.js +38 -28
  92. package/dist/core/module.d.ts +16 -5
  93. package/dist/core/module.js +4 -0
  94. package/dist/core/projection.d.ts +13 -13
  95. package/dist/core/projection.js +9 -5
  96. package/dist/core/protocol.d.ts +68 -11
  97. package/dist/core/protocol.js +82 -11
  98. package/dist/core/replay.d.ts +312 -3
  99. package/dist/core/replay.js +11 -1
  100. package/dist/core/schema.d.ts +8 -19
  101. package/dist/core/schema.js +25 -80
  102. package/dist/core/serialize.js +2 -4
  103. package/dist/core/snapshot-store.d.ts +2 -1
  104. package/dist/core/snapshot-store.js +8 -2
  105. package/dist/core/snapshots.d.ts +5 -1
  106. package/dist/core/snapshots.js +10 -15
  107. package/dist/core/sqlite-store.js +56 -11
  108. package/dist/core/state.d.ts +142 -4
  109. package/dist/core/state.js +78 -0
  110. package/dist/core/store.d.ts +38 -3
  111. package/dist/core/store.js +77 -22
  112. package/dist/core/thread.d.ts +10 -9
  113. package/dist/core/types.d.ts +5 -8
  114. package/dist/core/types.js +28 -24
  115. package/dist/core/validate-event.js +14 -4
  116. package/dist/core/worker-callback.d.ts +17 -0
  117. package/dist/core/worker-callback.js +1 -0
  118. package/dist/debugger/components/event-stream.js +9 -10
  119. package/dist/debugger/events/protocol-summarize.js +26 -0
  120. package/dist/debugger/lib/event-meta.js +1 -2
  121. package/dist/evaluator/definitions-store.d.ts +11 -0
  122. package/dist/evaluator/definitions-store.js +8 -0
  123. package/dist/evaluator/definitions.d.ts +18 -0
  124. package/dist/evaluator/definitions.js +4 -0
  125. package/dist/evaluator/effects.d.ts +226 -0
  126. package/dist/evaluator/effects.js +132 -0
  127. package/dist/evaluator/evaluator.d.ts +24 -0
  128. package/dist/evaluator/evaluator.js +67 -0
  129. package/dist/evaluator/events.d.ts +59 -0
  130. package/dist/evaluator/events.js +21 -0
  131. package/dist/evaluator/index.d.ts +11 -0
  132. package/dist/evaluator/index.js +11 -0
  133. package/dist/evaluator/module.d.ts +606 -0
  134. package/dist/evaluator/module.js +25 -0
  135. package/dist/evaluator/projections.d.ts +50 -0
  136. package/dist/evaluator/projections.js +20 -0
  137. package/dist/evaluator/scope.d.ts +59 -0
  138. package/dist/evaluator/scope.js +7 -0
  139. package/dist/evaluator/signals.d.ts +3 -0
  140. package/dist/evaluator/signals.js +17 -0
  141. package/dist/evaluator/threads.d.ts +62 -0
  142. package/dist/evaluator/threads.js +117 -0
  143. package/dist/evaluator/types.d.ts +125 -0
  144. package/dist/evaluator/types.js +51 -0
  145. package/dist/projectors/delivery.d.ts +83 -0
  146. package/dist/projectors/delivery.js +182 -0
  147. package/dist/projectors/index-model.d.ts +1 -1
  148. package/dist/projectors/index-model.js +4 -6
  149. package/dist/projectors/index.d.ts +2 -0
  150. package/dist/projectors/index.js +2 -0
  151. package/dist/projectors/memory.d.ts +1 -1
  152. package/dist/projectors/memory.js +10 -8
  153. package/dist/projectors/postgres.js +44 -58
  154. package/dist/projectors/projector.d.ts +17 -0
  155. package/dist/projectors/projector.js +26 -1
  156. package/dist/projectors/sqlite-delivery.d.ts +4 -0
  157. package/dist/projectors/sqlite-delivery.js +147 -0
  158. package/dist/projectors/sqlite.js +23 -23
  159. package/dist/projectors/with-projectors.d.ts +7 -2
  160. package/dist/projectors/with-projectors.js +27 -28
  161. package/dist/react/notify.js +11 -8
  162. package/dist/react/react.d.ts +2 -3
  163. package/dist/react/react.js +14 -30
  164. package/dist/react/sse.d.ts +1 -1
  165. package/dist/react/sse.js +1 -1
  166. package/dist/react/store.d.ts +2 -2
  167. package/dist/react/store.js +109 -119
  168. package/dist/runtime/cancellation.d.ts +16 -0
  169. package/dist/runtime/cancellation.js +129 -0
  170. package/dist/runtime/deadline-transition-planner.d.ts +31 -0
  171. package/dist/runtime/deadline-transition-planner.js +139 -0
  172. package/dist/runtime/dispatch-effect.d.ts +3 -2
  173. package/dist/runtime/dispatch-effect.js +7 -13
  174. package/dist/runtime/errors.d.ts +51 -0
  175. package/dist/runtime/errors.js +39 -0
  176. package/dist/runtime/{runtime-helpers.d.ts → helpers.d.ts} +24 -4
  177. package/dist/runtime/{runtime-helpers.js → helpers.js} +56 -19
  178. package/dist/runtime/index.d.ts +3 -3
  179. package/dist/runtime/index.js +3 -3
  180. package/dist/runtime/ingress.d.ts +15 -0
  181. package/dist/runtime/ingress.js +42 -0
  182. package/dist/runtime/local-effect-executor.d.ts +21 -0
  183. package/dist/runtime/local-effect-executor.js +137 -0
  184. package/dist/runtime/looms.d.ts +22 -4
  185. package/dist/runtime/looms.js +54 -36
  186. package/dist/runtime/observer.d.ts +45 -0
  187. package/dist/runtime/observer.js +11 -0
  188. package/dist/runtime/repository.d.ts +55 -0
  189. package/dist/runtime/repository.js +278 -0
  190. package/dist/runtime/retry-policy.d.ts +16 -0
  191. package/dist/runtime/retry-policy.js +29 -0
  192. package/dist/runtime/run-execution-context.d.ts +30 -0
  193. package/dist/runtime/run-execution-context.js +56 -0
  194. package/dist/runtime/run-start.d.ts +4 -0
  195. package/dist/runtime/run-start.js +44 -0
  196. package/dist/runtime/runtime.d.ts +6 -87
  197. package/dist/runtime/runtime.js +233 -470
  198. package/dist/runtime/server/auth.d.ts +40 -0
  199. package/dist/runtime/server/auth.js +40 -0
  200. package/dist/runtime/server/fetch-handler.d.ts +14 -0
  201. package/dist/runtime/server/fetch-handler.js +51 -0
  202. package/dist/runtime/server/http.d.ts +37 -0
  203. package/dist/runtime/server/http.js +62 -0
  204. package/dist/runtime/server/route-table.d.ts +55 -0
  205. package/dist/runtime/server/route-table.js +74 -0
  206. package/dist/runtime/server/routes/effects.d.ts +3 -0
  207. package/dist/runtime/server/routes/effects.js +59 -0
  208. package/dist/runtime/server/routes/operations.d.ts +3 -0
  209. package/dist/runtime/server/routes/operations.js +14 -0
  210. package/dist/runtime/server/routes/runs.d.ts +3 -0
  211. package/dist/runtime/server/routes/runs.js +192 -0
  212. package/dist/runtime/server.d.ts +2 -8
  213. package/dist/runtime/server.js +4 -211
  214. package/dist/runtime/sse.d.ts +2 -1
  215. package/dist/runtime/sse.js +24 -17
  216. package/dist/runtime/types.d.ts +115 -0
  217. package/dist/runtime/types.js +1 -0
  218. package/dist/runtime/wake-engine.d.ts +20 -0
  219. package/dist/runtime/wake-engine.js +311 -0
  220. package/dist/runtime/wake-scheduler.d.ts +3 -4
  221. package/dist/runtime/wake-scheduler.js +25 -31
  222. package/dist/runtime/worker-callbacks.d.ts +16 -0
  223. package/dist/runtime/worker-callbacks.js +167 -0
  224. package/dist/runtime/worker-effect-dispatch.d.ts +36 -0
  225. package/dist/runtime/worker-effect-dispatch.js +126 -0
  226. package/dist/s2/config.d.ts +1 -1
  227. package/dist/s2/config.js +1 -1
  228. package/dist/s2/lite.js +70 -82
  229. package/dist/s2/pagination.d.ts +15 -0
  230. package/dist/s2/pagination.js +36 -0
  231. package/dist/s2/projector.js +6 -5
  232. package/dist/s2/snapshot-store.js +148 -163
  233. package/dist/s2/store.d.ts +3 -21
  234. package/dist/s2/store.js +71 -165
  235. package/dist/testing/index.d.ts +15 -0
  236. package/dist/testing/index.js +53 -16
  237. package/dist/workflow/definitions-store.d.ts +2 -3
  238. package/dist/workflow/definitions-store.js +2 -2
  239. package/dist/workflow/definitions.d.ts +78 -22
  240. package/dist/workflow/definitions.js +80 -14
  241. package/dist/workflow/effects.d.ts +359 -13
  242. package/dist/workflow/effects.js +214 -115
  243. package/dist/workflow/events.d.ts +149 -5
  244. package/dist/workflow/events.js +27 -5
  245. package/dist/workflow/graph.d.ts +56 -0
  246. package/dist/workflow/graph.js +207 -0
  247. package/dist/workflow/index.d.ts +1 -0
  248. package/dist/workflow/index.js +1 -0
  249. package/dist/workflow/module.d.ts +1047 -266
  250. package/dist/workflow/projections.d.ts +14 -12
  251. package/dist/workflow/projections.js +30 -8
  252. package/dist/workflow/scope.d.ts +146 -4
  253. package/dist/workflow/threads.d.ts +383 -155
  254. package/dist/workflow/threads.js +308 -110
  255. package/package.json +8 -2
  256. package/dist/runtime/events-api.d.ts +0 -4
  257. package/dist/runtime/events-api.js +0 -84
@@ -1,53 +1,70 @@
1
- import { Effect, Predicate } from 'effect';
1
+ import { Data, Effect, Predicate } from 'effect';
2
2
  import { EventStoreTag, makeMemoryEventStore, snapshotStoreOf, SnapshotStoreTag, } from '@swirls/looms/core';
3
- import { createRuntime } from './runtime.js';
4
- import { createFetchHandler, isLoomsApiPath, serveHttp } from './server.js';
3
+ import { createRuntime, } from './runtime.js';
4
+ import { createFetchHandler, isLoomsApiPath, serveHttp, } from './server.js';
5
+ class LoomsInitializationError extends Data.TaggedError('LoomsInitializationError') {
6
+ constructor(cause) {
7
+ super({
8
+ cause,
9
+ message: cause instanceof Error ? cause.message : String(cause),
10
+ });
11
+ this.name = 'LoomsInitializationError';
12
+ }
13
+ }
5
14
  export function createLooms(options = {}) {
6
15
  const modules = options.modules ?? [];
7
16
  let initPromise;
8
17
  let runningServer;
9
18
  const getInit = () => {
10
19
  if (!initPromise) {
11
- initPromise = (async () => {
12
- let store;
13
- if (!options.store) {
14
- store = await Effect.runPromise(makeMemoryEventStore);
15
- }
16
- else if (Predicate.isFunction(options.store)) {
17
- store = await options.store();
18
- }
19
- else {
20
- store = await options.store;
21
- }
20
+ initPromise = Effect.runPromise(Effect.gen(function* () {
21
+ const configuredStore = options.store;
22
+ const store = !configuredStore
23
+ ? yield* makeMemoryEventStore
24
+ : Predicate.isFunction(configuredStore)
25
+ ? yield* Effect.tryPromise({
26
+ try: configuredStore,
27
+ catch: (cause) => new LoomsInitializationError(cause),
28
+ })
29
+ : configuredStore instanceof Promise
30
+ ? yield* Effect.tryPromise({
31
+ try: () => configuredStore,
32
+ catch: (cause) => new LoomsInitializationError(cause),
33
+ })
34
+ : configuredStore;
22
35
  const runtime = createRuntime({
23
36
  modules,
24
37
  store,
25
38
  maxWakeIterations: options.maxWakeIterations,
39
+ maxPendingRunOperations: options.maxPendingRunOperations,
26
40
  snapshotEvery: options.snapshotEvery,
27
41
  snapshotStore: options.snapshotStore ?? snapshotStoreOf(store),
28
42
  trimAfterSnapshot: options.trimAfterSnapshot,
29
43
  runCacheSize: options.runCacheSize,
30
44
  scheduler: options.scheduler,
45
+ worker: options.worker,
46
+ observer: options.observer,
31
47
  });
32
48
  if (options.rescanTimers !== false) {
33
- await Effect.runPromise(Effect.provideService(runtime.rescanTimers(), EventStoreTag, store)).catch((err) => {
34
- console.error('[rescan timers error]', err);
35
- });
49
+ yield* runtime.rescanTimers.pipe(Effect.provideService(EventStoreTag, store), Effect.tapError(Effect.logError), Effect.ignore);
36
50
  }
37
- const fetchHandler = createFetchHandler({ runtime, store });
51
+ const fetchHandler = createFetchHandler({
52
+ runtime,
53
+ store,
54
+ authorize: options.authorize,
55
+ });
38
56
  return { runtime, store, snapshotStore: snapshotStoreOf(store), fetchHandler };
39
- })();
57
+ }));
40
58
  }
41
59
  return initPromise;
42
60
  };
43
- const runEffect = async (fn) => {
44
- const init = await getInit();
61
+ const runEffect = (fn) => getInit().then((init) => {
45
62
  let provided = Effect.provideService(fn(init), EventStoreTag, init.store);
46
63
  if (init.snapshotStore) {
47
64
  provided = Effect.provideService(provided, SnapshotStoreTag, init.snapshotStore);
48
65
  }
49
66
  return Effect.runPromise(provided);
50
- };
67
+ });
51
68
  const looms = {
52
69
  get runtime() {
53
70
  return getInit().then((i) => i.runtime);
@@ -55,14 +72,12 @@ export function createLooms(options = {}) {
55
72
  get store() {
56
73
  return getInit().then((i) => i.store);
57
74
  },
58
- ready: async () => {
59
- const init = await getInit();
60
- return { store: init.store, runtime: init.runtime };
61
- },
75
+ ready: () => getInit().then(({ store, runtime }) => ({ store, runtime })),
62
76
  startRun: (args) => runEffect((i) => i.runtime.startRun(args)),
63
77
  start: (definition, input, startOpts) => runEffect((i) => i.runtime.startRun({
64
78
  kind: definition.kind,
65
79
  definitionName: definition.name,
80
+ definitionVersion: definition.version,
66
81
  // SAFETY: input is validated against the definition's schema inside startRun.
67
82
  input: input,
68
83
  runId: startOpts?.runId,
@@ -73,16 +88,20 @@ export function createLooms(options = {}) {
73
88
  getEvents: (runId, opts) => runEffect((i) => i.runtime.getEvents(runId, opts)),
74
89
  signal: (runId, events, signalOpts) => runEffect((i) => i.runtime.signal(runId, events, signalOpts)),
75
90
  wake: (runId) => runEffect((i) => i.runtime.wake(runId)),
76
- rescanTimers: () => runEffect((i) => i.runtime.rescanTimers()),
91
+ rescanTimers: () => runEffect((i) => i.runtime.rescanTimers),
92
+ recoverDeadlines: () => runEffect((i) => i.runtime.recoverDeadlines),
93
+ inspectRun: (runId) => runEffect((i) => i.runtime.inspectRun(runId)),
94
+ retryEffect: (runId, effectId) => runEffect((i) => i.runtime.retryEffect(runId, effectId)),
95
+ listRuns: () => runEffect((i) => i.runtime.listRuns),
77
96
  project: (runId, definition) => runEffect((i) => i.runtime.project(runId, definition)),
78
97
  replayTo: (runId, seq) => runEffect((i) => i.runtime.replayTo(runId, seq)),
79
98
  cancel: (runId, threadId) => runEffect((i) => i.runtime.cancel(runId, threadId)),
80
- fetch: async (req) => {
99
+ workerCallback: (runId, input) => runEffect((i) => i.runtime.workerCallback(runId, input)),
100
+ fetch: (req) => {
81
101
  if (!isLoomsApiPath(new URL(req.url).pathname)) {
82
- return null;
102
+ return Promise.resolve(null);
83
103
  }
84
- const { fetchHandler } = await getInit();
85
- return fetchHandler(req);
104
+ return getInit().then(({ fetchHandler }) => fetchHandler(req));
86
105
  },
87
106
  serve: (serveOpts) => {
88
107
  if (runningServer) {
@@ -91,15 +110,14 @@ export function createLooms(options = {}) {
91
110
  runningServer = serveHttp((req) => looms.fetch(req), serveOpts);
92
111
  return runningServer;
93
112
  },
94
- stop: async () => {
113
+ stop: () => {
95
114
  if (runningServer) {
96
115
  runningServer.stop();
97
116
  runningServer = undefined;
98
117
  }
99
- if (initPromise) {
100
- const init = await initPromise;
101
- init.runtime.dispose();
102
- }
118
+ return initPromise
119
+ ? initPromise.then(({ runtime }) => Effect.runPromise(runtime.dispose))
120
+ : Promise.resolve();
103
121
  },
104
122
  };
105
123
  if (options.serve) {
@@ -0,0 +1,45 @@
1
+ export type RuntimeObservation = {
2
+ readonly type: 'run.start';
3
+ readonly runId: string;
4
+ readonly at: number;
5
+ } | {
6
+ readonly type: 'run.wake';
7
+ readonly runId: string;
8
+ readonly at: number;
9
+ } | {
10
+ readonly type: 'run.park' | 'run.terminal';
11
+ readonly runId: string;
12
+ readonly at: number;
13
+ readonly status: string;
14
+ } | {
15
+ readonly type: 'event.append';
16
+ readonly runId: string;
17
+ readonly at: number;
18
+ readonly eventCount: number;
19
+ readonly latencyMs: number;
20
+ readonly outcome: 'ok' | 'conflict' | 'error';
21
+ } | {
22
+ readonly type: 'snapshot.load' | 'snapshot.save' | 'snapshot.trim';
23
+ readonly runId: string;
24
+ readonly at: number;
25
+ readonly latencyMs: number;
26
+ readonly outcome: 'ok' | 'miss' | 'error';
27
+ readonly cursor?: number;
28
+ } | {
29
+ readonly type: 'timer.late';
30
+ readonly runId: string;
31
+ readonly at: number;
32
+ readonly timerId: string;
33
+ readonly latenessMs: number;
34
+ } | {
35
+ readonly type: 'effect.attempt' | 'effect.retry' | 'effect.timeout' | 'effect.heartbeat' | 'effect.cancellation';
36
+ readonly runId: string;
37
+ readonly at: number;
38
+ readonly effectId: string;
39
+ readonly attempt: number;
40
+ readonly detail?: string;
41
+ };
42
+ export interface RuntimeObserver {
43
+ observe(event: RuntimeObservation): void | Promise<void>;
44
+ }
45
+ export declare function notifyObserver(observer: RuntimeObserver | undefined, event: RuntimeObservation): void;
@@ -0,0 +1,11 @@
1
+ export function notifyObserver(observer, event) {
2
+ if (!observer) {
3
+ return;
4
+ }
5
+ try {
6
+ Promise.resolve(observer.observe(event)).catch(() => undefined);
7
+ }
8
+ catch {
9
+ // Observability must never change runtime behavior.
10
+ }
11
+ }
@@ -0,0 +1,55 @@
1
+ import { Effect } from 'effect';
2
+ import { EventStoreTrimmedError, type AppendableEvent, type AppendResult, type ComposedRegistry, type EventEnvelope, type EventStore, type EventStoreAppendError, type EventStoreError, type EventStoreTrimCoverage, type EventStoreTruncationError, type RunCursor, type SnapshotStore } from '@swirls/looms/core';
3
+ import { RuntimeExecutionError } from './errors';
4
+ import { type RunCursorCache } from './helpers';
5
+ import { type RuntimeObserver } from './observer';
6
+ import type { RunExecutionContexts } from './run-execution-context';
7
+ declare const LiveAppendError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
8
+ readonly _tag: "LiveAppendError";
9
+ } & Readonly<A>;
10
+ declare class LiveAppendError extends LiveAppendError_base<{
11
+ readonly message: string;
12
+ }> {
13
+ constructor();
14
+ }
15
+ export interface CommitPlan<A> {
16
+ readonly events: ReadonlyArray<EventEnvelope>;
17
+ readonly value: A;
18
+ readonly idempotencyKey?: string | undefined;
19
+ }
20
+ export interface CommitResult<A> {
21
+ readonly value: A;
22
+ /** The durable state the committed batch was planned against. */
23
+ readonly cursor: RunCursor;
24
+ /** `null` when the plan had nothing to append. */
25
+ readonly append: AppendResult | null;
26
+ }
27
+ export interface RuntimeRepositoryOptions {
28
+ readonly registry: ComposedRegistry;
29
+ readonly snapshotStore: SnapshotStore;
30
+ readonly trimAfterSnapshot?: {
31
+ readonly keepSnapshots: number;
32
+ readonly coverage?: (runId: string) => EventStoreTrimCoverage | Promise<EventStoreTrimCoverage>;
33
+ };
34
+ readonly keepSnapshots: number;
35
+ readonly runCache: RunCursorCache;
36
+ readonly executionContexts: RunExecutionContexts;
37
+ readonly observer?: RuntimeObserver;
38
+ }
39
+ export declare function createRuntimeRepository(options: RuntimeRepositoryOptions): {
40
+ appendObserved: (store: EventStore, runId: string, events: ReadonlyArray<AppendableEvent>, appendOptions?: Parameters<EventStore['append']>[2]) => Effect.Effect<AppendResult, EventStoreAppendError>;
41
+ commit: <A, E, R>(store: EventStore, runId: string, plan: (cursor: RunCursor) => Effect.Effect<CommitPlan<A>, E, R>) => Effect.Effect<CommitResult<A>, E | RuntimeExecutionError | EventStoreAppendError | EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, R>;
42
+ liveAppends: {
43
+ push: (key: string, event: AppendableEvent) => Effect.Effect<void, LiveAppendError | EventStoreAppendError, never>;
44
+ drain: (key: string) => Effect.Effect<void>;
45
+ clear: (key?: string) => void;
46
+ size: () => number;
47
+ };
48
+ loadCursor: (store: EventStore, runId: string) => Effect.Effect<RunCursor, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError>;
49
+ persistSnapshot: (store: EventStore, runId: string, cursor: RunCursor) => Effect.Effect<RunCursor>;
50
+ applyAppend: (store: EventStore, runId: string, cursor: RunCursor, batch: ReadonlyArray<AppendableEvent>, appendResult: AppendResult, commandRecords?: number) => Effect.Effect<RunCursor, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError>;
51
+ deleteCachedCursor: (runId: string) => void;
52
+ setCachedCursor: (runId: string, cursor: RunCursor) => void;
53
+ };
54
+ export type RuntimeRepository = ReturnType<typeof createRuntimeRepository>;
55
+ export {};
@@ -0,0 +1,278 @@
1
+ import { Clock, Data, Effect, Option } from 'effect';
2
+ import { advanceCursor, assignSequences, createCoalescingAppender, emptyCursor, EventStoreTrimmedError, hashRunState, trimEventStoreSafely, } from '@swirls/looms/core';
3
+ import { RuntimeExecutionError } from './errors.js';
4
+ import { stripSeq } from './helpers.js';
5
+ import { notifyObserver } from './observer.js';
6
+ class LiveAppendError extends Data.TaggedError('LiveAppendError') {
7
+ constructor() {
8
+ super({ message: 'Cannot append live events without a store' });
9
+ this.name = 'LiveAppendError';
10
+ }
11
+ }
12
+ function currentTimeMillis() {
13
+ return Math.floor(performance.timeOrigin + performance.now());
14
+ }
15
+ const MAX_COMMIT_CONFLICTS = 16;
16
+ function readRemaining(store, runId, events, expectedTail) {
17
+ return Effect.gen(function* () {
18
+ let collected = events;
19
+ while (collected.length > 0) {
20
+ const lastSeq = collected[collected.length - 1].seq;
21
+ const target = expectedTail ?? (yield* store.tail(runId));
22
+ if (lastSeq >= target) {
23
+ break;
24
+ }
25
+ const more = yield* store.read(runId, {
26
+ fromSeq: lastSeq + 1,
27
+ limit: expectedTail !== undefined ? Math.max(0, target - lastSeq) : undefined,
28
+ });
29
+ if (more.length === 0 || more[0].seq <= lastSeq) {
30
+ break;
31
+ }
32
+ collected = [...collected, ...more];
33
+ }
34
+ return collected;
35
+ });
36
+ }
37
+ export function createRuntimeRepository(options) {
38
+ const { registry, snapshotStore, trimAfterSnapshot, keepSnapshots, runCache, executionContexts } = options;
39
+ const observe = (event) => notifyObserver(options.observer, event);
40
+ const appendObserved = (store, runId, events, appendOptions) => Effect.suspend(() => {
41
+ const startedAt = currentTimeMillis();
42
+ return store.append(runId, events, appendOptions).pipe(Effect.tap(() => Effect.sync(() => observe({
43
+ type: 'event.append',
44
+ runId,
45
+ at: currentTimeMillis(),
46
+ eventCount: events.length,
47
+ latencyMs: currentTimeMillis() - startedAt,
48
+ outcome: 'ok',
49
+ }))), Effect.tapError((error) => Effect.sync(() => observe({
50
+ type: 'event.append',
51
+ runId,
52
+ at: currentTimeMillis(),
53
+ eventCount: events.length,
54
+ latencyMs: currentTimeMillis() - startedAt,
55
+ outcome: error._tag === 'EventStoreConflictError' ? 'conflict' : 'error',
56
+ }))));
57
+ });
58
+ const liveAppends = createCoalescingAppender((runId, batch) => {
59
+ const context = executionContexts.peek(runId);
60
+ if (!context?.liveStore) {
61
+ return Effect.fail(new LiveAppendError());
62
+ }
63
+ const store = context.liveStore;
64
+ return appendObserved(store, runId, batch).pipe(Effect.tap((result) => Effect.sync(() => {
65
+ context.liveCount += result.sequences.length;
66
+ })), Effect.asVoid);
67
+ });
68
+ const catchUp = (store, runId, cursor, expectedTail) => Effect.gen(function* () {
69
+ if (expectedTail !== undefined && cursor.seq === expectedTail) {
70
+ return cursor;
71
+ }
72
+ const fromSeq = cursor.seq + 1;
73
+ const limit = expectedTail !== undefined ? Math.max(0, expectedTail - cursor.seq) : undefined;
74
+ let delta = yield* store.read(runId, { fromSeq, limit });
75
+ if (delta.length === 0) {
76
+ return cursor;
77
+ }
78
+ const firstSeq = delta[0].seq;
79
+ if (firstSeq > fromSeq) {
80
+ if (store.bounds) {
81
+ const bounds = yield* store.bounds(runId);
82
+ if (bounds.head > fromSeq) {
83
+ return yield* new EventStoreTrimmedError(runId, bounds.head, fromSeq);
84
+ }
85
+ }
86
+ else {
87
+ return yield* new EventStoreTrimmedError(runId, firstSeq, fromSeq);
88
+ }
89
+ }
90
+ delta = yield* readRemaining(store, runId, delta, expectedTail);
91
+ return advanceCursor(cursor, delta, registry);
92
+ });
93
+ const loadCursorCold = (store, runId) => Effect.gen(function* () {
94
+ let cursor = emptyCursor(runId);
95
+ if (snapshotStore) {
96
+ const loadStartedAt = currentTimeMillis();
97
+ const snap = yield* snapshotStore.loadLatest(runId).pipe(Effect.tapError((error) => Effect.sync(() => {
98
+ observe({
99
+ type: 'snapshot.load',
100
+ runId,
101
+ at: currentTimeMillis(),
102
+ latencyMs: currentTimeMillis() - loadStartedAt,
103
+ outcome: 'error',
104
+ });
105
+ }).pipe(Effect.andThen(Effect.logWarning(`Snapshot load failed for run "${runId}"; replaying the log`, error)))), Effect.orElseSucceed(() => Option.none()));
106
+ if (Option.isSome(snap)) {
107
+ observe({
108
+ type: 'snapshot.load',
109
+ runId,
110
+ at: currentTimeMillis(),
111
+ latencyMs: currentTimeMillis() - loadStartedAt,
112
+ outcome: 'ok',
113
+ cursor: snap.value.cursor,
114
+ });
115
+ cursor = {
116
+ state: snap.value.state,
117
+ seq: snap.value.cursor,
118
+ durableSinceSnapshot: 0,
119
+ };
120
+ }
121
+ else {
122
+ observe({
123
+ type: 'snapshot.load',
124
+ runId,
125
+ at: currentTimeMillis(),
126
+ latencyMs: currentTimeMillis() - loadStartedAt,
127
+ outcome: 'miss',
128
+ });
129
+ }
130
+ }
131
+ const fromSeq = cursor.seq + 1;
132
+ let delta = yield* store.read(runId, { fromSeq });
133
+ if (cursor.seq === 0 && delta[0] && delta[0].seq > 1) {
134
+ if (store.bounds) {
135
+ const bounds = yield* store.bounds(runId);
136
+ if (bounds.head > 1) {
137
+ return yield* new EventStoreTrimmedError(runId, bounds.head, 1);
138
+ }
139
+ }
140
+ else {
141
+ return yield* new EventStoreTrimmedError(runId, delta[0].seq, 1);
142
+ }
143
+ }
144
+ if (delta.length > 0) {
145
+ delta = yield* readRemaining(store, runId, delta);
146
+ }
147
+ return advanceCursor(cursor, delta, registry);
148
+ });
149
+ const loadCursor = (store, runId) => Effect.gen(function* () {
150
+ const cached = runCache.get(runId);
151
+ if (cached) {
152
+ const tail = yield* store.tail(runId);
153
+ // A tail behind the cursor means the stream was recreated; the cache is poison.
154
+ if (tail < cached.seq) {
155
+ runCache.delete(runId);
156
+ return yield* loadCursorCold(store, runId);
157
+ }
158
+ if (tail === cached.seq) {
159
+ return cached;
160
+ }
161
+ const next = yield* catchUp(store, runId, cached, tail).pipe(Effect.catchTag('EventStoreTrimmedError', () => {
162
+ runCache.delete(runId);
163
+ return loadCursorCold(store, runId);
164
+ }));
165
+ runCache.set(runId, next);
166
+ return next;
167
+ }
168
+ const next = yield* loadCursorCold(store, runId);
169
+ runCache.set(runId, next);
170
+ return next;
171
+ });
172
+ /**
173
+ * Save the current state externally, keep only `keepSnapshots`, and optionally
174
+ * trim the event log behind the oldest kept snapshot. Failures are swallowed:
175
+ * a snapshot is an optimization, the log stays authoritative.
176
+ */
177
+ const persistSnapshot = (store, runId, cursor) => Effect.gen(function* () {
178
+ if (cursor.durableSinceSnapshot === 0) {
179
+ return cursor;
180
+ }
181
+ const takenAt = yield* Clock.currentTimeMillis;
182
+ const saveStartedAt = currentTimeMillis();
183
+ yield* snapshotStore.save({
184
+ runId,
185
+ cursor: cursor.seq,
186
+ stateHash: hashRunState(cursor.state),
187
+ takenAt,
188
+ state: cursor.state,
189
+ });
190
+ observe({
191
+ type: 'snapshot.save',
192
+ runId,
193
+ at: currentTimeMillis(),
194
+ latencyMs: currentTimeMillis() - saveStartedAt,
195
+ outcome: 'ok',
196
+ cursor: cursor.seq,
197
+ });
198
+ if (snapshotStore.prune) {
199
+ yield* snapshotStore.prune(runId, keepSnapshots);
200
+ }
201
+ const coverageForRun = trimAfterSnapshot?.coverage;
202
+ if (coverageForRun && store.trim && snapshotStore.listCursors) {
203
+ const cursors = yield* snapshotStore.listCursors(runId);
204
+ const oldestKept = cursors[Math.max(0, cursors.length - keepSnapshots)];
205
+ if (oldestKept !== undefined) {
206
+ const trimStartedAt = currentTimeMillis();
207
+ const coverage = yield* Effect.tryPromise({
208
+ try: () => Promise.resolve(coverageForRun(runId)),
209
+ catch: (cause) => new RuntimeExecutionError(cause instanceof Error ? cause.message : String(cause)),
210
+ });
211
+ yield* trimEventStoreSafely(store, runId, oldestKept + 1, coverage);
212
+ observe({
213
+ type: 'snapshot.trim',
214
+ runId,
215
+ at: currentTimeMillis(),
216
+ latencyMs: currentTimeMillis() - trimStartedAt,
217
+ outcome: 'ok',
218
+ cursor: oldestKept,
219
+ });
220
+ }
221
+ }
222
+ return { ...cursor, durableSinceSnapshot: 0 };
223
+ }).pipe(Effect.tapError((error) => Effect.logWarning(`Snapshot persistence failed for run "${runId}"`, error)), Effect.orElseSucceed(() => cursor));
224
+ const applyAppend = (store, runId, cursor, batch, appendResult, commandRecords = 0) => Effect.gen(function* () {
225
+ const echo = assignSequences(batch, appendResult);
226
+ const first = echo[0];
227
+ let next = cursor;
228
+ if (first && first.seq !== cursor.seq + 1 + commandRecords) {
229
+ const gap = first.seq - 1 - cursor.seq;
230
+ const context = executionContexts.get(runId);
231
+ if (context.liveCount === gap) {
232
+ next = { ...cursor, seq: first.seq - 1 };
233
+ }
234
+ else {
235
+ next = yield* catchUp(store, runId, cursor, first.seq - 1);
236
+ }
237
+ }
238
+ next = advanceCursor(next, echo, registry);
239
+ executionContexts.get(runId).liveCount = 0;
240
+ if (next.seq !== appendResult.tail) {
241
+ next = yield* catchUp(store, runId, next, appendResult.tail);
242
+ }
243
+ runCache.set(runId, next);
244
+ return next;
245
+ });
246
+ /**
247
+ * Plan a batch against the latest durable state and append it with optimistic concurrency,
248
+ * replanning from fresh state whenever another writer got there first.
249
+ */
250
+ const commit = (store, runId, plan) => Effect.gen(function* () {
251
+ for (let conflicts = 0; conflicts < MAX_COMMIT_CONFLICTS; conflicts += 1) {
252
+ const cursor = yield* loadCursor(store, runId);
253
+ const planned = yield* plan(cursor);
254
+ if (planned.events.length === 0) {
255
+ return { value: planned.value, cursor, append: null };
256
+ }
257
+ const append = yield* appendObserved(store, runId, stripSeq(planned.events), {
258
+ expectedTail: cursor.seq,
259
+ idempotency: planned.idempotencyKey ? { key: planned.idempotencyKey } : undefined,
260
+ }).pipe(Effect.asSome, Effect.catchTag('EventStoreConflictError', () => Effect.succeedNone));
261
+ if (Option.isSome(append)) {
262
+ return { value: planned.value, cursor, append: append.value };
263
+ }
264
+ runCache.delete(runId);
265
+ }
266
+ return yield* new RuntimeExecutionError(`Could not commit to run "${runId}" after ${MAX_COMMIT_CONFLICTS} conflicts`);
267
+ });
268
+ return {
269
+ appendObserved,
270
+ commit,
271
+ liveAppends,
272
+ loadCursor,
273
+ persistSnapshot,
274
+ applyAppend,
275
+ deleteCachedCursor: (runId) => runCache.delete(runId),
276
+ setCachedCursor: (runId, cursor) => runCache.set(runId, cursor),
277
+ };
278
+ }
@@ -0,0 +1,16 @@
1
+ import { type EventEnvelope, type EventOrigin, type OutstandingEffect, type RetryPolicy } from '@swirls/looms/core';
2
+ export declare function retryBackoff(retry: RetryPolicy, attempt: number): number;
3
+ export interface AttemptFailurePlan {
4
+ readonly event: EventEnvelope;
5
+ readonly retrying: boolean;
6
+ }
7
+ /** Schedule the next attempt when the retry policy allows one, otherwise fail the effect. */
8
+ export declare function planAttemptFailure(input: {
9
+ readonly runId: string;
10
+ readonly item: OutstandingEffect;
11
+ readonly attempt: number;
12
+ readonly error: string;
13
+ readonly now: number;
14
+ readonly retry: RetryPolicy | undefined;
15
+ readonly origin: EventOrigin;
16
+ }): AttemptFailurePlan;
@@ -0,0 +1,29 @@
1
+ import { createEvent, } from '@swirls/looms/core';
2
+ import { createEffectFailedEvent } from './helpers.js';
3
+ export function retryBackoff(retry, attempt) {
4
+ const base = Math.max(1, retry.backoffMs ?? 100);
5
+ const multiplier = Math.max(1, retry.backoffMultiplier ?? 2);
6
+ return Math.min(base * multiplier ** Math.max(0, attempt - 1), retry.maxBackoffMs ?? Number.MAX_SAFE_INTEGER);
7
+ }
8
+ /** Schedule the next attempt when the retry policy allows one, otherwise fail the effect. */
9
+ export function planAttemptFailure(input) {
10
+ const { runId, item, attempt, error, retry } = input;
11
+ if (!retry || attempt >= retry.maxAttempts) {
12
+ return { event: createEffectFailedEvent(runId, item, error), retrying: false };
13
+ }
14
+ return {
15
+ retrying: true,
16
+ event: createEvent(runId, {
17
+ type: 'runtime.effect.retry.scheduled',
18
+ payload: {
19
+ effectId: item.effectId,
20
+ attempt,
21
+ nextAttemptAt: input.now + retryBackoff(retry, attempt),
22
+ error,
23
+ },
24
+ threadId: item.threadId,
25
+ causationId: item.causingEventId,
26
+ origin: input.origin,
27
+ }),
28
+ };
29
+ }
@@ -0,0 +1,30 @@
1
+ import type { Exit } from 'effect';
2
+ import type { EventStore, RunState } from '@swirls/looms/core';
3
+ import type { WakeError } from './types';
4
+ export type WakeCompletion = Exit.Exit<RunState, WakeError>;
5
+ export interface ActiveEffect {
6
+ readonly controller: AbortController;
7
+ readonly threadId: string;
8
+ }
9
+ export interface RunIngressState {
10
+ readonly pending: number;
11
+ readonly tail: Promise<void>;
12
+ }
13
+ export interface RunExecutionContext {
14
+ readonly activeEffects: Map<string, ActiveEffect>;
15
+ waking: boolean;
16
+ wakeAgain: boolean;
17
+ wakeCompletion: Promise<WakeCompletion> | null;
18
+ resolveWakeCompletion: ((completion: WakeCompletion) => void) | null;
19
+ ingress: RunIngressState | null;
20
+ liveStore: EventStore | undefined;
21
+ liveCount: number;
22
+ }
23
+ export declare class RunExecutionContexts {
24
+ private readonly runs;
25
+ get(runId: string): RunExecutionContext;
26
+ peek(runId: string): RunExecutionContext | undefined;
27
+ /** Abort in-process effects owned by the given threads. */
28
+ abort(runId: string, threadIds: ReadonlySet<string>, message: string): void;
29
+ releaseIfIdle(runId: string): void;
30
+ }