@swirls/looms 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/actor/cell.d.ts +21 -2
  3. package/dist/actor/cell.js +15 -3
  4. package/dist/actor/host.d.ts +10 -2
  5. package/dist/actor/host.js +27 -4
  6. package/dist/actor/index.d.ts +2 -1
  7. package/dist/actor/index.js +1 -1
  8. package/dist/actor/routing.d.ts +3 -0
  9. package/dist/actor/routing.js +13 -2
  10. package/dist/agent/debugger/agent-chat.js +3 -30
  11. package/dist/agent/debugger/streaming-text.d.ts +6 -0
  12. package/dist/agent/debugger/streaming-text.js +33 -0
  13. package/dist/agent/definitions-store.d.ts +1 -1
  14. package/dist/agent/definitions-store.js +1 -1
  15. package/dist/agent/definitions.d.ts +48 -0
  16. package/dist/agent/definitions.js +44 -0
  17. package/dist/agent/effects.d.ts +184 -0
  18. package/dist/agent/effects.js +172 -40
  19. package/dist/agent/events.d.ts +89 -0
  20. package/dist/agent/events.js +25 -0
  21. package/dist/agent/module.d.ts +819 -407
  22. package/dist/agent/module.js +5 -1
  23. package/dist/agent/scope.d.ts +88 -0
  24. package/dist/agent/threads.js +39 -3
  25. package/dist/agent/types.d.ts +4 -0
  26. package/dist/approval/effects.d.ts +40 -0
  27. package/dist/approval/module.d.ts +80 -0
  28. package/dist/client/client.d.ts +468 -119
  29. package/dist/client/client.js +214 -21
  30. package/dist/client/index.d.ts +1 -1
  31. package/dist/client/sse.d.ts +1 -1
  32. package/dist/client/sse.js +2 -2
  33. package/dist/cloudflare/durable-object.d.ts +66 -2
  34. package/dist/cloudflare/durable-object.js +96 -1
  35. package/dist/cloudflare/event-store.d.ts +7 -3
  36. package/dist/cloudflare/event-store.js +16 -3
  37. package/dist/cloudflare/index.d.ts +7 -1
  38. package/dist/cloudflare/index.js +7 -1
  39. package/dist/cloudflare/memory-queue.d.ts +18 -0
  40. package/dist/cloudflare/memory-queue.js +53 -0
  41. package/dist/cloudflare/queue-consumer.d.ts +52 -0
  42. package/dist/cloudflare/queue-consumer.js +102 -0
  43. package/dist/cloudflare/queue-message.d.ts +31 -0
  44. package/dist/cloudflare/queue-message.js +33 -0
  45. package/dist/cloudflare/queue-pull.d.ts +62 -0
  46. package/dist/cloudflare/queue-pull.js +130 -0
  47. package/dist/cloudflare/queue-worker.d.ts +39 -0
  48. package/dist/cloudflare/queue-worker.js +62 -0
  49. package/dist/cloudflare/r2-blob-store.d.ts +33 -0
  50. package/dist/cloudflare/r2-blob-store.js +60 -0
  51. package/dist/cloudflare/router.d.ts +4 -1
  52. package/dist/cloudflare/router.js +8 -3
  53. package/dist/cloudflare/websocket.d.ts +27 -0
  54. package/dist/cloudflare/websocket.js +215 -0
  55. package/dist/core/attempts.d.ts +8 -0
  56. package/dist/core/attempts.js +27 -0
  57. package/dist/core/blob-offload.d.ts +21 -0
  58. package/dist/core/blob-offload.js +143 -0
  59. package/dist/core/blob-store.d.ts +45 -0
  60. package/dist/core/blob-store.js +164 -0
  61. package/dist/core/blobs.d.ts +37 -0
  62. package/dist/core/blobs.js +160 -0
  63. package/dist/core/definition-resolver.d.ts +40 -0
  64. package/dist/core/definition-resolver.js +129 -0
  65. package/dist/core/definition-store.d.ts +6 -0
  66. package/dist/core/definition-store.js +37 -1
  67. package/dist/core/effects.d.ts +86 -3
  68. package/dist/core/effects.js +99 -3
  69. package/dist/core/envelope.d.ts +9 -0
  70. package/dist/core/envelope.js +4 -0
  71. package/dist/core/event-codec.js +3 -0
  72. package/dist/core/fold.js +83 -1
  73. package/dist/core/ids.d.ts +2 -0
  74. package/dist/core/ids.js +4 -0
  75. package/dist/core/index.d.ts +10 -0
  76. package/dist/core/index.js +9 -0
  77. package/dist/core/launcher.d.ts +97 -0
  78. package/dist/core/launcher.js +67 -0
  79. package/dist/core/module-scope.d.ts +7 -3
  80. package/dist/core/module-scope.js +3 -0
  81. package/dist/core/module.d.ts +10 -0
  82. package/dist/core/projection.d.ts +5 -4
  83. package/dist/core/projection.js +4 -0
  84. package/dist/core/protocol.d.ts +36 -1
  85. package/dist/core/protocol.js +35 -0
  86. package/dist/core/published.d.ts +2 -0
  87. package/dist/core/published.js +4 -1
  88. package/dist/{react → core}/register.d.ts +9 -8
  89. package/dist/core/replay.d.ts +96 -0
  90. package/dist/core/run-context.d.ts +33 -0
  91. package/dist/core/run-context.js +23 -0
  92. package/dist/core/sqlite-store.js +57 -5
  93. package/dist/core/state.d.ts +60 -0
  94. package/dist/core/state.js +19 -0
  95. package/dist/core/store.d.ts +6 -0
  96. package/dist/core/store.js +7 -0
  97. package/dist/core/subscription.d.ts +97 -0
  98. package/dist/core/subscription.js +224 -0
  99. package/dist/core/thread.d.ts +7 -0
  100. package/dist/core/thread.js +1 -0
  101. package/dist/core/trace.d.ts +21 -0
  102. package/dist/core/trace.js +45 -0
  103. package/dist/core/worker-callback.d.ts +25 -1
  104. package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
  105. package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
  106. package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
  107. package/dist/debugger/app/index.html +2 -2
  108. package/dist/debugger/components/event-inspector.d.ts +3 -1
  109. package/dist/debugger/components/event-inspector.js +25 -4
  110. package/dist/debugger/components/event-stream.d.ts +4 -1
  111. package/dist/debugger/components/event-stream.js +2 -2
  112. package/dist/debugger/components/run-tree.d.ts +5 -2
  113. package/dist/debugger/components/run-tree.js +10 -4
  114. package/dist/debugger/context.d.ts +8 -1
  115. package/dist/debugger/context.js +15 -3
  116. package/dist/debugger/events/protocol-summarize.js +18 -3
  117. package/dist/debugger/index.d.ts +1 -1
  118. package/dist/debugger/index.js +1 -1
  119. package/dist/debugger/shell/debugger-shell.js +1 -1
  120. package/dist/debugger/shell/events-pane.js +4 -3
  121. package/dist/debugger/styles.css +1 -0
  122. package/dist/evaluator/definitions-store.d.ts +3 -4
  123. package/dist/evaluator/definitions-store.js +3 -3
  124. package/dist/evaluator/definitions.d.ts +4 -1
  125. package/dist/evaluator/definitions.js +5 -2
  126. package/dist/evaluator/effects.d.ts +41 -0
  127. package/dist/evaluator/effects.js +6 -2
  128. package/dist/evaluator/module.d.ts +84 -0
  129. package/dist/evaluator/module.js +7 -3
  130. package/dist/evaluator/threads.d.ts +2 -0
  131. package/dist/evaluator/threads.js +4 -0
  132. package/dist/projectors/event-log.d.ts +87 -0
  133. package/dist/projectors/event-log.js +396 -0
  134. package/dist/projectors/index-model.d.ts +12 -0
  135. package/dist/projectors/index-model.js +12 -3
  136. package/dist/projectors/memory.js +7 -0
  137. package/dist/projectors/postgres.d.ts +1 -0
  138. package/dist/projectors/postgres.js +32 -15
  139. package/dist/projectors/schema.d.ts +5 -0
  140. package/dist/projectors/schema.js +26 -1
  141. package/dist/projectors/sqlite.js +47 -19
  142. package/dist/projectors/with-projectors.d.ts +7 -1
  143. package/dist/projectors/with-projectors.js +13 -3
  144. package/dist/react/derived.d.ts +26 -8
  145. package/dist/react/derived.js +23 -26
  146. package/dist/react/index.d.ts +3 -3
  147. package/dist/react/index.js +2 -2
  148. package/dist/react/materialize.js +5 -1
  149. package/dist/react/react.d.ts +16 -9
  150. package/dist/react/react.js +47 -23
  151. package/dist/react/store.d.ts +14 -6
  152. package/dist/react/store.js +104 -12
  153. package/dist/react/tables.d.ts +4 -1
  154. package/dist/runtime/cancellation.d.ts +2 -1
  155. package/dist/runtime/cancellation.js +35 -2
  156. package/dist/runtime/child-run.d.ts +10 -0
  157. package/dist/runtime/child-run.js +58 -0
  158. package/dist/runtime/deadline-transition-planner.js +11 -2
  159. package/dist/runtime/dispatch-effect.d.ts +2 -2
  160. package/dist/runtime/dispatch-effect.js +43 -19
  161. package/dist/runtime/errors.d.ts +14 -1
  162. package/dist/runtime/errors.js +10 -0
  163. package/dist/runtime/filesystem-blob-store.d.ts +6 -0
  164. package/dist/runtime/filesystem-blob-store.js +59 -0
  165. package/dist/runtime/helpers.d.ts +15 -6
  166. package/dist/runtime/helpers.js +46 -6
  167. package/dist/runtime/index.d.ts +4 -2
  168. package/dist/runtime/index.js +3 -1
  169. package/dist/runtime/local-effect-executor.d.ts +11 -2
  170. package/dist/runtime/local-effect-executor.js +80 -4
  171. package/dist/runtime/looms.d.ts +38 -4
  172. package/dist/runtime/looms.js +17 -3
  173. package/dist/runtime/repository.d.ts +5 -4
  174. package/dist/runtime/repository.js +5 -2
  175. package/dist/runtime/retry-policy.d.ts +5 -0
  176. package/dist/runtime/retry-policy.js +10 -2
  177. package/dist/runtime/run-start.d.ts +1 -1
  178. package/dist/runtime/run-start.js +10 -0
  179. package/dist/runtime/runtime.d.ts +2 -2
  180. package/dist/runtime/runtime.js +191 -68
  181. package/dist/runtime/server/auth.d.ts +3 -0
  182. package/dist/runtime/server/fetch-handler.d.ts +2 -2
  183. package/dist/runtime/server/fetch-handler.js +35 -4
  184. package/dist/runtime/server/grant.d.ts +49 -0
  185. package/dist/runtime/server/grant.js +177 -0
  186. package/dist/runtime/server/routes/effects.js +47 -5
  187. package/dist/runtime/server/routes/runs.js +6 -1
  188. package/dist/runtime/server/run-route.d.ts +18 -0
  189. package/dist/runtime/server/run-route.js +26 -0
  190. package/dist/runtime/server.d.ts +3 -1
  191. package/dist/runtime/server.js +3 -1
  192. package/dist/runtime/sse.js +26 -4
  193. package/dist/runtime/subscription.d.ts +8 -0
  194. package/dist/runtime/subscription.js +23 -0
  195. package/dist/runtime/types.d.ts +87 -8
  196. package/dist/runtime/wake-engine.d.ts +4 -2
  197. package/dist/runtime/wake-engine.js +57 -6
  198. package/dist/runtime/worker-callbacks.d.ts +12 -2
  199. package/dist/runtime/worker-callbacks.js +102 -72
  200. package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
  201. package/dist/runtime/worker-effect-dispatch.js +84 -18
  202. package/dist/schedule/debugger/index.d.ts +4 -0
  203. package/dist/schedule/debugger/index.js +11 -0
  204. package/dist/schedule/debugger/schedule-view.d.ts +3 -0
  205. package/dist/schedule/debugger/schedule-view.js +68 -0
  206. package/dist/schedule/debugger/summarize.d.ts +2 -0
  207. package/dist/schedule/debugger/summarize.js +43 -0
  208. package/dist/schedule/definitions-store.d.ts +10 -0
  209. package/dist/schedule/definitions-store.js +8 -0
  210. package/dist/schedule/definitions.d.ts +57 -0
  211. package/dist/schedule/definitions.js +30 -0
  212. package/dist/schedule/effects.d.ts +756 -0
  213. package/dist/schedule/effects.js +244 -0
  214. package/dist/schedule/ensure.d.ts +55 -0
  215. package/dist/schedule/ensure.js +109 -0
  216. package/dist/schedule/events.d.ts +116 -0
  217. package/dist/schedule/events.js +49 -0
  218. package/dist/schedule/index.d.ts +11 -0
  219. package/dist/schedule/index.js +11 -0
  220. package/dist/schedule/module.d.ts +1474 -0
  221. package/dist/schedule/module.js +22 -0
  222. package/dist/schedule/projections.d.ts +55 -0
  223. package/dist/schedule/projections.js +88 -0
  224. package/dist/schedule/scope.d.ts +121 -0
  225. package/dist/schedule/scope.js +7 -0
  226. package/dist/schedule/signals.d.ts +25 -0
  227. package/dist/schedule/signals.js +33 -0
  228. package/dist/schedule/specs.d.ts +59 -0
  229. package/dist/schedule/specs.js +231 -0
  230. package/dist/schedule/threads.d.ts +25 -0
  231. package/dist/schedule/threads.js +259 -0
  232. package/dist/testing/corpus-module.d.ts +34 -0
  233. package/dist/testing/corpus-module.js +87 -0
  234. package/dist/testing/index.d.ts +2 -0
  235. package/dist/testing/index.js +5 -6
  236. package/dist/testing/replay-corpus.d.ts +22 -0
  237. package/dist/testing/replay-corpus.js +50 -0
  238. package/dist/workflow/definitions-store.d.ts +1 -1
  239. package/dist/workflow/definitions-store.js +1 -1
  240. package/dist/workflow/definitions.d.ts +14 -0
  241. package/dist/workflow/effects.d.ts +122 -0
  242. package/dist/workflow/effects.js +16 -6
  243. package/dist/workflow/events.d.ts +42 -0
  244. package/dist/workflow/events.js +4 -0
  245. package/dist/workflow/module.d.ts +345 -32
  246. package/dist/workflow/module.js +7 -3
  247. package/dist/workflow/scope.d.ts +42 -0
  248. package/dist/workflow/threads.d.ts +72 -0
  249. package/dist/workflow/threads.js +106 -38
  250. package/package.json +11 -2
  251. package/dist/debugger/app/assets/index-BevQYa-U.js +0 -22
  252. package/dist/debugger/app/assets/routes-B17Om93i.js +0 -47
  253. /package/dist/{react → core}/register.js +0 -0
@@ -0,0 +1,22 @@
1
+ import { defineModule, makeDefinitionStore } from '@swirls/looms/core';
2
+ import { ScheduleDefinitionsLive } from './definitions-store.js';
3
+ import { fireEffect, planEffect } from './effects.js';
4
+ import { scheduleStatus } from './projections.js';
5
+ import { scheduleModule } from './scope.js';
6
+ import { DEFAULT_MIN_INTERVAL_MS } from './specs.js';
7
+ import { createScheduleThread } from './threads.js';
8
+ export function schedule(options = {}) {
9
+ const listed = options.definitions ?? [];
10
+ const definitions = makeDefinitionStore('schedule', listed);
11
+ const minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
12
+ return defineModule(scheduleModule, () => ({
13
+ definitions: listed,
14
+ threads: { schedule: createScheduleThread(definitions, minIntervalMs) },
15
+ effects: { plan: planEffect, fire: fireEffect },
16
+ projections: { scheduleStatus },
17
+ services: () => ScheduleDefinitionsLive(definitions),
18
+ bindHost(host) {
19
+ definitions.useResolver(host.definitionResolver);
20
+ },
21
+ }));
22
+ }
@@ -0,0 +1,55 @@
1
+ import { Schema } from 'effect';
2
+ export declare const ScheduleRecentSchema: Schema.Struct<{
3
+ readonly at: Schema.Finite;
4
+ readonly runId: Schema.NullOr<Schema.String>;
5
+ readonly outcome: Schema.Union<readonly [Schema.Literal<"fired">, Schema.Literal<"skipped">]>;
6
+ readonly reason: Schema.optional<Schema.String>;
7
+ }>;
8
+ export type ScheduleRecent = Schema.Schema.Type<typeof ScheduleRecentSchema>;
9
+ export declare const ScheduleStatusSchema: Schema.Struct<{
10
+ readonly spec: Schema.NullOr<Schema.Union<readonly [Schema.Struct<{
11
+ readonly type: Schema.Literal<"cron">;
12
+ readonly expr: Schema.String;
13
+ readonly tz: Schema.optional<Schema.String>;
14
+ }>, Schema.Struct<{
15
+ readonly type: Schema.Literal<"every">;
16
+ readonly everyMs: Schema.Finite;
17
+ readonly anchor: Schema.optional<Schema.Finite>;
18
+ }>, Schema.Struct<{
19
+ readonly type: Schema.Literal<"once">;
20
+ readonly at: Schema.Finite;
21
+ }>]>>;
22
+ readonly status: Schema.Union<readonly [Schema.Literal<"running">, Schema.Literal<"paused">, Schema.Literal<"retired">, Schema.Literal<"completed">, Schema.Literal<"failed">, Schema.Literal<"cancelled">]>;
23
+ readonly nextAt: Schema.NullOr<Schema.Finite>;
24
+ readonly fireCount: Schema.Finite;
25
+ readonly recent: Schema.$Array<Schema.Struct<{
26
+ readonly at: Schema.Finite;
27
+ readonly runId: Schema.NullOr<Schema.String>;
28
+ readonly outcome: Schema.Union<readonly [Schema.Literal<"fired">, Schema.Literal<"skipped">]>;
29
+ readonly reason: Schema.optional<Schema.String>;
30
+ }>>;
31
+ }>;
32
+ export type ScheduleStatus = Schema.Schema.Type<typeof ScheduleStatusSchema>;
33
+ export declare const scheduleStatus: import("@swirls/looms/core").ProjectionDefinition<{
34
+ readonly spec: {
35
+ readonly type: "cron";
36
+ readonly expr: string;
37
+ readonly tz?: string | undefined;
38
+ } | {
39
+ readonly type: "every";
40
+ readonly everyMs: number;
41
+ readonly anchor?: number | undefined;
42
+ } | {
43
+ readonly type: "once";
44
+ readonly at: number;
45
+ } | null;
46
+ readonly status: "cancelled" | "completed" | "failed" | "paused" | "retired" | "running";
47
+ readonly nextAt: number | null;
48
+ readonly fireCount: number;
49
+ readonly recent: readonly {
50
+ readonly at: number;
51
+ readonly runId: string | null;
52
+ readonly outcome: "fired" | "skipped";
53
+ readonly reason?: string | undefined;
54
+ }[];
55
+ }>;
@@ -0,0 +1,88 @@
1
+ import { Schema } from 'effect';
2
+ import { scheduleModule } from './scope.js';
3
+ import { ScheduleSpecSchema } from './specs.js';
4
+ const RECENT_LIMIT = 20;
5
+ export const ScheduleRecentSchema = Schema.Struct({
6
+ at: Schema.Finite,
7
+ runId: Schema.NullOr(Schema.String),
8
+ outcome: Schema.Union([Schema.Literal('fired'), Schema.Literal('skipped')]),
9
+ reason: Schema.optional(Schema.String),
10
+ });
11
+ export const ScheduleStatusSchema = Schema.Struct({
12
+ spec: Schema.NullOr(ScheduleSpecSchema),
13
+ status: Schema.Union([
14
+ Schema.Literal('running'),
15
+ Schema.Literal('paused'),
16
+ Schema.Literal('retired'),
17
+ Schema.Literal('completed'),
18
+ Schema.Literal('failed'),
19
+ Schema.Literal('cancelled'),
20
+ ]),
21
+ nextAt: Schema.NullOr(Schema.Finite),
22
+ fireCount: Schema.Finite,
23
+ recent: Schema.Array(ScheduleRecentSchema),
24
+ });
25
+ export const scheduleStatus = scheduleModule.projection({
26
+ name: 'scheduleStatus',
27
+ shape: ScheduleStatusSchema,
28
+ initialState: {
29
+ spec: null,
30
+ status: 'running',
31
+ nextAt: null,
32
+ fireCount: 0,
33
+ recent: [],
34
+ },
35
+ reduce(state, event) {
36
+ switch (event.type) {
37
+ case 'schedule.planned':
38
+ return { ...state, spec: event.payload.spec, nextAt: event.payload.nextAt };
39
+ case 'schedule.updated':
40
+ return event.payload.spec === undefined ? state : { ...state, spec: event.payload.spec };
41
+ case 'schedule.fired':
42
+ return {
43
+ ...state,
44
+ nextAt: event.payload.nextAt,
45
+ fireCount: state.fireCount + 1,
46
+ recent: remember(state.recent, {
47
+ at: event.payload.at,
48
+ runId: event.payload.runId,
49
+ outcome: 'fired',
50
+ }),
51
+ };
52
+ case 'schedule.skipped':
53
+ return {
54
+ ...state,
55
+ nextAt: event.payload.nextAt,
56
+ recent: remember(state.recent, {
57
+ at: event.payload.at,
58
+ runId: null,
59
+ outcome: 'skipped',
60
+ reason: event.payload.reason,
61
+ }),
62
+ };
63
+ case 'schedule.paused':
64
+ return {
65
+ ...state,
66
+ status: state.status === 'retired' ? 'retired' : 'paused',
67
+ nextAt: null,
68
+ };
69
+ case 'schedule.resumed':
70
+ return { ...state, status: 'running' };
71
+ case 'schedule.retired':
72
+ return { ...state, status: 'retired', nextAt: null };
73
+ case 'runtime.thread.completed':
74
+ return { ...state, status: 'completed', nextAt: null };
75
+ case 'runtime.thread.failed':
76
+ return { ...state, status: 'failed', nextAt: null };
77
+ case 'runtime.run.cancelled':
78
+ case 'runtime.thread.cancelled':
79
+ return { ...state, status: 'cancelled', nextAt: null };
80
+ default:
81
+ return state;
82
+ }
83
+ },
84
+ });
85
+ function remember(recent, item) {
86
+ const next = [...recent, item];
87
+ return next.length > RECENT_LIMIT ? next.slice(next.length - RECENT_LIMIT) : next;
88
+ }
@@ -0,0 +1,121 @@
1
+ import { type EventOf } from '@swirls/looms/core';
2
+ export declare const scheduleModule: import("@swirls/looms/core").ModuleScope<"schedule", import("@swirls/looms/core").ScopeUniverse<import("@swirls/looms/core").EventCatalog<"schedule", {
3
+ planned: import("effect/Schema").Struct<{
4
+ readonly nextAt: import("effect/Schema").NullOr<import("effect/Schema").Finite>;
5
+ readonly generation: import("effect/Schema").Finite;
6
+ readonly spec: import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
7
+ readonly type: import("effect/Schema").Literal<"cron">;
8
+ readonly expr: import("effect/Schema").String;
9
+ readonly tz: import("effect/Schema").optional<import("effect/Schema").String>;
10
+ }>, import("effect/Schema").Struct<{
11
+ readonly type: import("effect/Schema").Literal<"every">;
12
+ readonly everyMs: import("effect/Schema").Finite;
13
+ readonly anchor: import("effect/Schema").optional<import("effect/Schema").Finite>;
14
+ }>, import("effect/Schema").Struct<{
15
+ readonly type: import("effect/Schema").Literal<"once">;
16
+ readonly at: import("effect/Schema").Finite;
17
+ }>]>;
18
+ }>;
19
+ fired: import("effect/Schema").Struct<{
20
+ readonly at: import("effect/Schema").Finite;
21
+ readonly runId: import("effect/Schema").String;
22
+ readonly nextAt: import("effect/Schema").NullOr<import("effect/Schema").Finite>;
23
+ readonly generation: import("effect/Schema").Finite;
24
+ readonly manual: import("effect/Schema").Boolean;
25
+ readonly triggerId: import("effect/Schema").optional<import("effect/Schema").String>;
26
+ }>;
27
+ skipped: import("effect/Schema").Struct<{
28
+ readonly at: import("effect/Schema").Finite;
29
+ readonly reason: import("effect/Schema").String;
30
+ readonly nextAt: import("effect/Schema").NullOr<import("effect/Schema").Finite>;
31
+ readonly generation: import("effect/Schema").Finite;
32
+ readonly manual: import("effect/Schema").Boolean;
33
+ readonly triggerId: import("effect/Schema").optional<import("effect/Schema").String>;
34
+ }>;
35
+ updated: import("effect/Schema").Struct<{
36
+ readonly spec: import("effect/Schema").optional<import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
37
+ readonly type: import("effect/Schema").Literal<"cron">;
38
+ readonly expr: import("effect/Schema").String;
39
+ readonly tz: import("effect/Schema").optional<import("effect/Schema").String>;
40
+ }>, import("effect/Schema").Struct<{
41
+ readonly type: import("effect/Schema").Literal<"every">;
42
+ readonly everyMs: import("effect/Schema").Finite;
43
+ readonly anchor: import("effect/Schema").optional<import("effect/Schema").Finite>;
44
+ }>, import("effect/Schema").Struct<{
45
+ readonly type: import("effect/Schema").Literal<"once">;
46
+ readonly at: import("effect/Schema").Finite;
47
+ }>]>>;
48
+ readonly input: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Codec<import("effect/Schema").Json, import("effect/Schema").Json, never, never>>>;
49
+ readonly endAt: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Finite>>;
50
+ readonly maxFires: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Finite>>;
51
+ readonly definitionName: import("effect/Schema").optional<import("effect/Schema").String>;
52
+ readonly definitionVersion: import("effect/Schema").optional<import("effect/Schema").String>;
53
+ }>;
54
+ paused: import("effect/Schema").Struct<{}>;
55
+ resumed: import("effect/Schema").Struct<{}>;
56
+ retired: import("effect/Schema").Struct<{}>;
57
+ triggered: import("effect/Schema").Struct<{}>;
58
+ rejected: import("effect/Schema").Struct<{
59
+ readonly reason: import("effect/Schema").String;
60
+ }>;
61
+ }>, undefined>, import("@swirls/looms/core").EventCatalog<"schedule", {
62
+ planned: import("effect/Schema").Struct<{
63
+ readonly nextAt: import("effect/Schema").NullOr<import("effect/Schema").Finite>;
64
+ readonly generation: import("effect/Schema").Finite;
65
+ readonly spec: import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
66
+ readonly type: import("effect/Schema").Literal<"cron">;
67
+ readonly expr: import("effect/Schema").String;
68
+ readonly tz: import("effect/Schema").optional<import("effect/Schema").String>;
69
+ }>, import("effect/Schema").Struct<{
70
+ readonly type: import("effect/Schema").Literal<"every">;
71
+ readonly everyMs: import("effect/Schema").Finite;
72
+ readonly anchor: import("effect/Schema").optional<import("effect/Schema").Finite>;
73
+ }>, import("effect/Schema").Struct<{
74
+ readonly type: import("effect/Schema").Literal<"once">;
75
+ readonly at: import("effect/Schema").Finite;
76
+ }>]>;
77
+ }>;
78
+ fired: import("effect/Schema").Struct<{
79
+ readonly at: import("effect/Schema").Finite;
80
+ readonly runId: import("effect/Schema").String;
81
+ readonly nextAt: import("effect/Schema").NullOr<import("effect/Schema").Finite>;
82
+ readonly generation: import("effect/Schema").Finite;
83
+ readonly manual: import("effect/Schema").Boolean;
84
+ readonly triggerId: import("effect/Schema").optional<import("effect/Schema").String>;
85
+ }>;
86
+ skipped: import("effect/Schema").Struct<{
87
+ readonly at: import("effect/Schema").Finite;
88
+ readonly reason: import("effect/Schema").String;
89
+ readonly nextAt: import("effect/Schema").NullOr<import("effect/Schema").Finite>;
90
+ readonly generation: import("effect/Schema").Finite;
91
+ readonly manual: import("effect/Schema").Boolean;
92
+ readonly triggerId: import("effect/Schema").optional<import("effect/Schema").String>;
93
+ }>;
94
+ updated: import("effect/Schema").Struct<{
95
+ readonly spec: import("effect/Schema").optional<import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
96
+ readonly type: import("effect/Schema").Literal<"cron">;
97
+ readonly expr: import("effect/Schema").String;
98
+ readonly tz: import("effect/Schema").optional<import("effect/Schema").String>;
99
+ }>, import("effect/Schema").Struct<{
100
+ readonly type: import("effect/Schema").Literal<"every">;
101
+ readonly everyMs: import("effect/Schema").Finite;
102
+ readonly anchor: import("effect/Schema").optional<import("effect/Schema").Finite>;
103
+ }>, import("effect/Schema").Struct<{
104
+ readonly type: import("effect/Schema").Literal<"once">;
105
+ readonly at: import("effect/Schema").Finite;
106
+ }>]>>;
107
+ readonly input: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Codec<import("effect/Schema").Json, import("effect/Schema").Json, never, never>>>;
108
+ readonly endAt: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Finite>>;
109
+ readonly maxFires: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Finite>>;
110
+ readonly definitionName: import("effect/Schema").optional<import("effect/Schema").String>;
111
+ readonly definitionVersion: import("effect/Schema").optional<import("effect/Schema").String>;
112
+ }>;
113
+ paused: import("effect/Schema").Struct<{}>;
114
+ resumed: import("effect/Schema").Struct<{}>;
115
+ retired: import("effect/Schema").Struct<{}>;
116
+ triggered: import("effect/Schema").Struct<{}>;
117
+ rejected: import("effect/Schema").Struct<{
118
+ readonly reason: import("effect/Schema").String;
119
+ }>;
120
+ }>, undefined>;
121
+ export type ScheduleEvent = EventOf<typeof scheduleModule>;
@@ -0,0 +1,7 @@
1
+ import { createModuleScope } from '@swirls/looms/core';
2
+ import { scheduleCatalog } from './events.js';
3
+ export const scheduleModule = createModuleScope({
4
+ namespace: 'schedule',
5
+ protocolVersion: '1.0.0',
6
+ events: scheduleCatalog,
7
+ });
@@ -0,0 +1,25 @@
1
+ import type { EventInput, JsonValue } from '@swirls/looms/core';
2
+ import type { ScheduleSpec } from './specs';
3
+ export interface ScheduleUpdate {
4
+ readonly spec?: ScheduleSpec;
5
+ readonly input?: JsonValue | null;
6
+ /** `null` clears the bound. Omit the field to leave it unchanged. */
7
+ readonly endAt?: number | null;
8
+ /** `null` clears the bound. Omit the field to leave it unchanged. */
9
+ readonly maxFires?: number | null;
10
+ readonly definitionName?: string;
11
+ readonly definitionVersion?: string;
12
+ }
13
+ /** Replace the spec, stored input, or bounds. A no-op when nothing changed. */
14
+ export declare function update(patch: ScheduleUpdate): EventInput;
15
+ /** Drop the armed fire and delete the alarm. A later `resume` rebases from now. */
16
+ export declare function pause(): EventInput;
17
+ /** Arm the next fire after now. Missed fires are not caught up. */
18
+ export declare function resume(): EventInput;
19
+ /**
20
+ * Park a removed schedule without cancelling it, so the same run id can be resumed.
21
+ * Cancel remains the permanent delete.
22
+ */
23
+ export declare function retire(): EventInput;
24
+ /** Start one target run now, without moving the armed fire. */
25
+ export declare function trigger(): EventInput;
@@ -0,0 +1,33 @@
1
+ import { scheduleModule } from './scope.js';
2
+ /** Replace the spec, stored input, or bounds. A no-op when nothing changed. */
3
+ export function update(patch) {
4
+ return scheduleModule.input('updated', {
5
+ ...(patch.spec === undefined ? undefined : { spec: patch.spec }),
6
+ ...(patch.input === undefined ? undefined : { input: patch.input }),
7
+ ...(patch.endAt === undefined ? undefined : { endAt: patch.endAt }),
8
+ ...(patch.maxFires === undefined ? undefined : { maxFires: patch.maxFires }),
9
+ ...(patch.definitionName === undefined ? undefined : { definitionName: patch.definitionName }),
10
+ ...(patch.definitionVersion === undefined
11
+ ? undefined
12
+ : { definitionVersion: patch.definitionVersion }),
13
+ });
14
+ }
15
+ /** Drop the armed fire and delete the alarm. A later `resume` rebases from now. */
16
+ export function pause() {
17
+ return scheduleModule.input('paused', {});
18
+ }
19
+ /** Arm the next fire after now. Missed fires are not caught up. */
20
+ export function resume() {
21
+ return scheduleModule.input('resumed', {});
22
+ }
23
+ /**
24
+ * Park a removed schedule without cancelling it, so the same run id can be resumed.
25
+ * Cancel remains the permanent delete.
26
+ */
27
+ export function retire() {
28
+ return scheduleModule.input('retired', {});
29
+ }
30
+ /** Start one target run now, without moving the armed fire. */
31
+ export function trigger() {
32
+ return scheduleModule.input('triggered', {});
33
+ }
@@ -0,0 +1,59 @@
1
+ import { Schema } from 'effect';
2
+ /** Default minimum gap between fires. Cron's own minute is the floor. */
3
+ export declare const DEFAULT_MIN_INTERVAL_MS = 60000;
4
+ /** A late alarm within this window still counts as on time. */
5
+ export declare const MISFIRE_GRACE_MS = 1000;
6
+ export declare const CronSpecSchema: Schema.Struct<{
7
+ readonly type: Schema.Literal<"cron">;
8
+ readonly expr: Schema.String;
9
+ readonly tz: Schema.optional<Schema.String>;
10
+ }>;
11
+ export declare const EverySpecSchema: Schema.Struct<{
12
+ readonly type: Schema.Literal<"every">;
13
+ readonly everyMs: Schema.Finite;
14
+ readonly anchor: Schema.optional<Schema.Finite>;
15
+ }>;
16
+ export declare const OnceSpecSchema: Schema.Struct<{
17
+ readonly type: Schema.Literal<"once">;
18
+ readonly at: Schema.Finite;
19
+ }>;
20
+ export declare const ScheduleSpecSchema: Schema.Union<readonly [Schema.Struct<{
21
+ readonly type: Schema.Literal<"cron">;
22
+ readonly expr: Schema.String;
23
+ readonly tz: Schema.optional<Schema.String>;
24
+ }>, Schema.Struct<{
25
+ readonly type: Schema.Literal<"every">;
26
+ readonly everyMs: Schema.Finite;
27
+ readonly anchor: Schema.optional<Schema.Finite>;
28
+ }>, Schema.Struct<{
29
+ readonly type: Schema.Literal<"once">;
30
+ readonly at: Schema.Finite;
31
+ }>]>;
32
+ export type ScheduleSpec = Schema.Schema.Type<typeof ScheduleSpecSchema>;
33
+ export type CronSpec = Schema.Schema.Type<typeof CronSpecSchema>;
34
+ export type EverySpec = Schema.Schema.Type<typeof EverySpecSchema>;
35
+ export type OnceSpec = Schema.Schema.Type<typeof OnceSpecSchema>;
36
+ export interface EveryOptions {
37
+ /** Epoch millisecond the interval is aligned to. Defaults to the unix epoch. */
38
+ readonly anchor?: number;
39
+ }
40
+ /** A cron expression. `tz` is an IANA name; omitted expressions are evaluated in UTC. */
41
+ export declare function cron(expr: string, options?: {
42
+ readonly tz?: string;
43
+ }): CronSpec;
44
+ /** A fixed interval. `duration` is milliseconds, or a string such as `15m`, `1h`, or `30s`. */
45
+ export declare function every(duration: string | number, options?: EveryOptions): EverySpec;
46
+ /** One fire at an absolute epoch millisecond. */
47
+ export declare function once(at: number): OnceSpec;
48
+ /**
49
+ * The first fire strictly after `after`. Invalid specs and an exhausted `once` return null.
50
+ * This is pure: it does not read the clock.
51
+ */
52
+ export declare function nextFireAt(spec: ScheduleSpec, after: number): number | null;
53
+ /** A short label for the debugger and logs. */
54
+ export declare function describeSpec(spec: ScheduleSpec): string;
55
+ /**
56
+ * Why `spec` cannot be armed, or undefined when it is allowed.
57
+ * `once` has no gap. A gap equal to `minIntervalMs` is allowed.
58
+ */
59
+ export declare function specRejection(spec: ScheduleSpec, minIntervalMs: number): string | undefined;
@@ -0,0 +1,231 @@
1
+ import { Cron, Result, Schema } from 'effect';
2
+ /** Default minimum gap between fires. Cron's own minute is the floor. */
3
+ export const DEFAULT_MIN_INTERVAL_MS = 60_000;
4
+ /** A late alarm within this window still counts as on time. */
5
+ export const MISFIRE_GRACE_MS = 1_000;
6
+ const GAP_SAMPLE_STARTS = [
7
+ 1_768_435_200_000, 1_774_656_000_000, 1_772_841_600_000, 1_792_800_000_000, 1_793_491_200_000,
8
+ ];
9
+ const GAP_SAMPLE_COUNT = 32;
10
+ const DURATION_UNITS = {
11
+ ms: 1,
12
+ s: 1_000,
13
+ m: 60_000,
14
+ h: 3_600_000,
15
+ d: 86_400_000,
16
+ };
17
+ export const CronSpecSchema = Schema.Struct({
18
+ type: Schema.Literal('cron'),
19
+ expr: Schema.String,
20
+ tz: Schema.optional(Schema.String),
21
+ });
22
+ export const EverySpecSchema = Schema.Struct({
23
+ type: Schema.Literal('every'),
24
+ everyMs: Schema.Finite,
25
+ anchor: Schema.optional(Schema.Finite),
26
+ });
27
+ export const OnceSpecSchema = Schema.Struct({
28
+ type: Schema.Literal('once'),
29
+ at: Schema.Finite,
30
+ });
31
+ export const ScheduleSpecSchema = Schema.Union([CronSpecSchema, EverySpecSchema, OnceSpecSchema]);
32
+ /** A cron expression. `tz` is an IANA name; omitted expressions are evaluated in UTC. */
33
+ export function cron(expr, options) {
34
+ const tz = options?.tz;
35
+ return tz === undefined ? { type: 'cron', expr } : { type: 'cron', expr, tz };
36
+ }
37
+ /** A fixed interval. `duration` is milliseconds, or a string such as `15m`, `1h`, or `30s`. */
38
+ export function every(duration, options) {
39
+ const everyMs = isMilliseconds(duration) ? duration : parseDuration(duration);
40
+ if (!Number.isFinite(everyMs) || everyMs <= 0) {
41
+ throw new Error('every() duration must be a positive number of milliseconds');
42
+ }
43
+ const anchor = options?.anchor;
44
+ if (anchor !== undefined && !Number.isFinite(anchor)) {
45
+ throw new Error('every() anchor must be a finite epoch timestamp');
46
+ }
47
+ return anchor === undefined ? { type: 'every', everyMs } : { type: 'every', everyMs, anchor };
48
+ }
49
+ /** One fire at an absolute epoch millisecond. */
50
+ export function once(at) {
51
+ if (!Number.isFinite(at)) {
52
+ throw new Error('once() at must be a finite epoch timestamp');
53
+ }
54
+ return { type: 'once', at };
55
+ }
56
+ /**
57
+ * The first fire strictly after `after`. Invalid specs and an exhausted `once` return null.
58
+ * This is pure: it does not read the clock.
59
+ */
60
+ export function nextFireAt(spec, after) {
61
+ switch (spec.type) {
62
+ case 'cron':
63
+ return nextCronAt(spec, after);
64
+ case 'every':
65
+ return nextEveryAt(spec, after);
66
+ case 'once':
67
+ return spec.at > after ? spec.at : null;
68
+ default: {
69
+ const unexpected = spec;
70
+ return unexpected;
71
+ }
72
+ }
73
+ }
74
+ /** A short label for the debugger and logs. */
75
+ export function describeSpec(spec) {
76
+ switch (spec.type) {
77
+ case 'cron':
78
+ return spec.tz === undefined ? spec.expr : `${spec.expr} (${spec.tz})`;
79
+ case 'every':
80
+ return `every ${formatDuration(spec.everyMs)}`;
81
+ case 'once':
82
+ return `once at ${spec.at}`;
83
+ default: {
84
+ const unexpected = spec;
85
+ return unexpected;
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Why `spec` cannot be armed, or undefined when it is allowed.
91
+ * `once` has no gap. A gap equal to `minIntervalMs` is allowed.
92
+ */
93
+ export function specRejection(spec, minIntervalMs) {
94
+ switch (spec.type) {
95
+ case 'cron': {
96
+ const parsed = parseCron(spec);
97
+ if (Result.isFailure(parsed)) {
98
+ return `invalid cron expression "${spec.expr}"`;
99
+ }
100
+ return gapRejection(smallestCronGap(parsed.success, minIntervalMs), minIntervalMs, spec.expr);
101
+ }
102
+ case 'every': {
103
+ if (!Number.isFinite(spec.everyMs) || spec.everyMs <= 0) {
104
+ return 'every() duration must be a positive number of milliseconds';
105
+ }
106
+ if (spec.anchor !== undefined && !Number.isFinite(spec.anchor)) {
107
+ return 'every() anchor must be a finite epoch timestamp';
108
+ }
109
+ return gapRejection(spec.everyMs, minIntervalMs, describeSpec(spec));
110
+ }
111
+ case 'once':
112
+ return Number.isFinite(spec.at) ? undefined : 'once() at must be a finite epoch timestamp';
113
+ default: {
114
+ const unexpected = spec;
115
+ return unexpected;
116
+ }
117
+ }
118
+ }
119
+ function gapRejection(gapMs, minIntervalMs, label) {
120
+ if (minIntervalMs <= 0 || gapMs >= minIntervalMs) {
121
+ return undefined;
122
+ }
123
+ return `schedule "${label}" repeats every ${gapMs}ms, below the minimum ${minIntervalMs}ms`;
124
+ }
125
+ function parseCron(spec) {
126
+ return Cron.parse(spec.expr, spec.tz ?? 'UTC');
127
+ }
128
+ function nextCronAt(spec, after) {
129
+ const parsed = parseCron(spec);
130
+ if (Result.isFailure(parsed)) {
131
+ return null;
132
+ }
133
+ try {
134
+ return Cron.next(parsed.success, after).getTime();
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function nextEveryAt(spec, after) {
141
+ if (!Number.isFinite(spec.everyMs) || spec.everyMs <= 0) {
142
+ return null;
143
+ }
144
+ const anchor = spec.anchor ?? 0;
145
+ if (after < anchor) {
146
+ return anchor;
147
+ }
148
+ const steps = Math.floor((after - anchor) / spec.everyMs) + 1;
149
+ return anchor + steps * spec.everyMs;
150
+ }
151
+ function smallestCronGap(parsed, minIntervalMs) {
152
+ if (parsed.seconds.size === 1 && minIntervalMs <= DEFAULT_MIN_INTERVAL_MS) {
153
+ return DEFAULT_MIN_INTERVAL_MS;
154
+ }
155
+ let smallest = Number.POSITIVE_INFINITY;
156
+ if (parsed.seconds.size > 1) {
157
+ const seconds = [...parsed.seconds].toSorted((a, b) => a - b);
158
+ for (let index = 1; index < seconds.length; index += 1) {
159
+ const current = seconds[index];
160
+ const prev = seconds[index - 1];
161
+ if (current !== undefined && prev !== undefined) {
162
+ smallest = Math.min(smallest, (current - prev) * 1_000);
163
+ }
164
+ }
165
+ if (smallest < minIntervalMs) {
166
+ return smallest;
167
+ }
168
+ }
169
+ for (const start of GAP_SAMPLE_STARTS) {
170
+ let cursor = start;
171
+ let previous;
172
+ for (let index = 0; index < GAP_SAMPLE_COUNT; index += 1) {
173
+ let next;
174
+ try {
175
+ next = Cron.next(parsed, cursor).getTime();
176
+ }
177
+ catch {
178
+ return 0;
179
+ }
180
+ if (previous !== undefined) {
181
+ smallest = Math.min(smallest, next - previous);
182
+ }
183
+ previous = next;
184
+ cursor = next;
185
+ }
186
+ }
187
+ return smallest;
188
+ }
189
+ function parseDuration(duration) {
190
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(duration.trim());
191
+ const amount = match?.[1];
192
+ const unit = match?.[2];
193
+ if (amount === undefined || unit === undefined) {
194
+ throw new Error(`every() duration "${duration}" must be a number of milliseconds or a string like "15m"`);
195
+ }
196
+ const multiplier = durationUnitMs(unit);
197
+ if (multiplier === undefined) {
198
+ throw new Error(`every() duration "${duration}" must be a number of milliseconds or a string like "15m"`);
199
+ }
200
+ return Number(amount) * multiplier;
201
+ }
202
+ function isMilliseconds(duration) {
203
+ return Number.isFinite(duration);
204
+ }
205
+ function durationUnitMs(unit) {
206
+ switch (unit) {
207
+ case 'ms':
208
+ case 's':
209
+ case 'm':
210
+ case 'h':
211
+ case 'd':
212
+ return DURATION_UNITS[unit];
213
+ default:
214
+ return undefined;
215
+ }
216
+ }
217
+ function formatDuration(ms) {
218
+ if (ms % 86_400_000 === 0) {
219
+ return `${ms / 86_400_000}d`;
220
+ }
221
+ if (ms % 3_600_000 === 0) {
222
+ return `${ms / 3_600_000}h`;
223
+ }
224
+ if (ms % 60_000 === 0) {
225
+ return `${ms / 60_000}m`;
226
+ }
227
+ if (ms % 1_000 === 0) {
228
+ return `${ms / 1_000}s`;
229
+ }
230
+ return `${ms}ms`;
231
+ }