@sonamu-kit/tasks 0.2.0 → 0.3.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 (215) hide show
  1. package/.oxlintrc.json +3 -0
  2. package/AGENTS.md +21 -0
  3. package/dist/backend.d.ts +126 -107
  4. package/dist/backend.d.ts.map +1 -1
  5. package/dist/backend.js +4 -1
  6. package/dist/backend.js.map +1 -1
  7. package/dist/client.d.ts +145 -132
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +219 -213
  10. package/dist/client.js.map +1 -1
  11. package/dist/config.d.ts +15 -8
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +22 -17
  14. package/dist/config.js.map +1 -1
  15. package/dist/core/duration.d.ts +5 -4
  16. package/dist/core/duration.d.ts.map +1 -1
  17. package/dist/core/duration.js +54 -59
  18. package/dist/core/duration.js.map +1 -1
  19. package/dist/core/error.d.ts +10 -7
  20. package/dist/core/error.d.ts.map +1 -1
  21. package/dist/core/error.js +21 -21
  22. package/dist/core/error.js.map +1 -1
  23. package/dist/core/json.d.ts +8 -3
  24. package/dist/core/json.d.ts.map +1 -1
  25. package/dist/core/result.d.ts +10 -14
  26. package/dist/core/result.d.ts.map +1 -1
  27. package/dist/core/result.js +21 -16
  28. package/dist/core/result.js.map +1 -1
  29. package/dist/core/retry.d.ts +37 -31
  30. package/dist/core/retry.d.ts.map +1 -1
  31. package/dist/core/retry.js +44 -51
  32. package/dist/core/retry.js.map +1 -1
  33. package/dist/core/schema.d.ts +57 -53
  34. package/dist/core/schema.d.ts.map +1 -1
  35. package/dist/core/step.d.ts +28 -78
  36. package/dist/core/step.d.ts.map +1 -1
  37. package/dist/core/step.js +53 -63
  38. package/dist/core/step.js.map +1 -1
  39. package/dist/core/workflow.d.ts +33 -61
  40. package/dist/core/workflow.d.ts.map +1 -1
  41. package/dist/core/workflow.js +31 -41
  42. package/dist/core/workflow.js.map +1 -1
  43. package/dist/database/backend.d.ts +53 -46
  44. package/dist/database/backend.d.ts.map +1 -1
  45. package/dist/database/backend.js +544 -577
  46. package/dist/database/backend.js.map +1 -1
  47. package/dist/database/base.js +48 -25
  48. package/dist/database/base.js.map +1 -1
  49. package/dist/database/migrations/20251212000000_0_init.d.ts +10 -0
  50. package/dist/database/migrations/20251212000000_0_init.d.ts.map +1 -0
  51. package/dist/database/migrations/20251212000000_0_init.js +8 -4
  52. package/dist/database/migrations/20251212000000_0_init.js.map +1 -1
  53. package/dist/database/migrations/20251212000000_1_tables.d.ts +10 -0
  54. package/dist/database/migrations/20251212000000_1_tables.d.ts.map +1 -0
  55. package/dist/database/migrations/20251212000000_1_tables.js +81 -83
  56. package/dist/database/migrations/20251212000000_1_tables.js.map +1 -1
  57. package/dist/database/migrations/20251212000000_2_fk.d.ts +10 -0
  58. package/dist/database/migrations/20251212000000_2_fk.d.ts.map +1 -0
  59. package/dist/database/migrations/20251212000000_2_fk.js +20 -43
  60. package/dist/database/migrations/20251212000000_2_fk.js.map +1 -1
  61. package/dist/database/migrations/20251212000000_3_indexes.d.ts +10 -0
  62. package/dist/database/migrations/20251212000000_3_indexes.d.ts.map +1 -0
  63. package/dist/database/migrations/20251212000000_3_indexes.js +88 -102
  64. package/dist/database/migrations/20251212000000_3_indexes.js.map +1 -1
  65. package/dist/database/pubsub.d.ts +7 -16
  66. package/dist/database/pubsub.d.ts.map +1 -1
  67. package/dist/database/pubsub.js +75 -73
  68. package/dist/database/pubsub.js.map +1 -1
  69. package/dist/execution.d.ts +20 -59
  70. package/dist/execution.d.ts.map +1 -1
  71. package/dist/execution.js +175 -188
  72. package/dist/execution.js.map +1 -1
  73. package/dist/index.d.ts +5 -8
  74. package/dist/index.js +5 -5
  75. package/dist/internal.d.ts +12 -13
  76. package/dist/internal.js +4 -4
  77. package/dist/registry.d.ts +33 -27
  78. package/dist/registry.d.ts.map +1 -1
  79. package/dist/registry.js +58 -49
  80. package/dist/registry.js.map +1 -1
  81. package/dist/worker.d.ts +57 -50
  82. package/dist/worker.d.ts.map +1 -1
  83. package/dist/worker.js +194 -199
  84. package/dist/worker.js.map +1 -1
  85. package/dist/workflow.d.ts +26 -30
  86. package/dist/workflow.d.ts.map +1 -1
  87. package/dist/workflow.js +20 -15
  88. package/dist/workflow.js.map +1 -1
  89. package/nodemon.json +1 -1
  90. package/package.json +17 -19
  91. package/src/backend.ts +25 -9
  92. package/src/chaos.test.ts +3 -1
  93. package/src/client.test.ts +2 -0
  94. package/src/client.ts +30 -8
  95. package/src/config.test.ts +1 -0
  96. package/src/config.ts +3 -2
  97. package/src/core/duration.test.ts +2 -1
  98. package/src/core/duration.ts +1 -1
  99. package/src/core/error.test.ts +1 -0
  100. package/src/core/error.ts +1 -1
  101. package/src/core/result.test.ts +1 -0
  102. package/src/core/retry.test.ts +3 -2
  103. package/src/core/retry.ts +1 -1
  104. package/src/core/schema.ts +2 -2
  105. package/src/core/step.test.ts +2 -1
  106. package/src/core/step.ts +4 -3
  107. package/src/core/workflow.test.ts +2 -1
  108. package/src/core/workflow.ts +4 -3
  109. package/src/database/backend.test.ts +1 -0
  110. package/src/database/backend.testsuite.ts +44 -40
  111. package/src/database/backend.ts +207 -25
  112. package/src/database/base.test.ts +41 -0
  113. package/src/database/base.ts +51 -2
  114. package/src/database/migrations/20251212000000_0_init.ts +2 -1
  115. package/src/database/migrations/20251212000000_1_tables.ts +2 -1
  116. package/src/database/migrations/20251212000000_2_fk.ts +2 -1
  117. package/src/database/migrations/20251212000000_3_indexes.ts +2 -1
  118. package/src/database/pubsub.test.ts +6 -3
  119. package/src/database/pubsub.ts +55 -33
  120. package/src/execution.test.ts +2 -0
  121. package/src/execution.ts +49 -10
  122. package/src/internal.ts +15 -15
  123. package/src/practices/01-remote-workflow.ts +1 -0
  124. package/src/registry.test.ts +1 -0
  125. package/src/registry.ts +1 -1
  126. package/src/testing/connection.ts +3 -1
  127. package/src/worker.test.ts +2 -0
  128. package/src/worker.ts +30 -9
  129. package/src/workflow.test.ts +1 -0
  130. package/src/workflow.ts +3 -3
  131. package/templates/openworkflow.config.ts +2 -1
  132. package/tsdown.config.ts +31 -0
  133. package/.swcrc +0 -17
  134. package/dist/chaos.test.d.ts +0 -2
  135. package/dist/chaos.test.d.ts.map +0 -1
  136. package/dist/chaos.test.js +0 -92
  137. package/dist/chaos.test.js.map +0 -1
  138. package/dist/client.test.d.ts +0 -2
  139. package/dist/client.test.d.ts.map +0 -1
  140. package/dist/client.test.js +0 -340
  141. package/dist/client.test.js.map +0 -1
  142. package/dist/config.test.d.ts +0 -2
  143. package/dist/config.test.d.ts.map +0 -1
  144. package/dist/config.test.js +0 -24
  145. package/dist/config.test.js.map +0 -1
  146. package/dist/core/duration.test.d.ts +0 -2
  147. package/dist/core/duration.test.d.ts.map +0 -1
  148. package/dist/core/duration.test.js +0 -265
  149. package/dist/core/duration.test.js.map +0 -1
  150. package/dist/core/error.test.d.ts +0 -2
  151. package/dist/core/error.test.d.ts.map +0 -1
  152. package/dist/core/error.test.js +0 -63
  153. package/dist/core/error.test.js.map +0 -1
  154. package/dist/core/json.js +0 -3
  155. package/dist/core/json.js.map +0 -1
  156. package/dist/core/result.test.d.ts +0 -2
  157. package/dist/core/result.test.d.ts.map +0 -1
  158. package/dist/core/result.test.js +0 -19
  159. package/dist/core/result.test.js.map +0 -1
  160. package/dist/core/retry.test.d.ts +0 -2
  161. package/dist/core/retry.test.d.ts.map +0 -1
  162. package/dist/core/retry.test.js +0 -198
  163. package/dist/core/retry.test.js.map +0 -1
  164. package/dist/core/schema.js +0 -4
  165. package/dist/core/schema.js.map +0 -1
  166. package/dist/core/step.test.d.ts +0 -2
  167. package/dist/core/step.test.d.ts.map +0 -1
  168. package/dist/core/step.test.js +0 -356
  169. package/dist/core/step.test.js.map +0 -1
  170. package/dist/core/workflow.test.d.ts +0 -2
  171. package/dist/core/workflow.test.d.ts.map +0 -1
  172. package/dist/core/workflow.test.js +0 -172
  173. package/dist/core/workflow.test.js.map +0 -1
  174. package/dist/database/backend.test.d.ts +0 -2
  175. package/dist/database/backend.test.d.ts.map +0 -1
  176. package/dist/database/backend.test.js +0 -19
  177. package/dist/database/backend.test.js.map +0 -1
  178. package/dist/database/backend.testsuite.d.ts +0 -20
  179. package/dist/database/backend.testsuite.d.ts.map +0 -1
  180. package/dist/database/backend.testsuite.js +0 -1280
  181. package/dist/database/backend.testsuite.js.map +0 -1
  182. package/dist/database/base.d.ts +0 -12
  183. package/dist/database/base.d.ts.map +0 -1
  184. package/dist/database/pubsub.test.d.ts +0 -2
  185. package/dist/database/pubsub.test.d.ts.map +0 -1
  186. package/dist/database/pubsub.test.js +0 -86
  187. package/dist/database/pubsub.test.js.map +0 -1
  188. package/dist/execution.test.d.ts +0 -2
  189. package/dist/execution.test.d.ts.map +0 -1
  190. package/dist/execution.test.js +0 -662
  191. package/dist/execution.test.js.map +0 -1
  192. package/dist/index.d.ts.map +0 -1
  193. package/dist/index.js.map +0 -1
  194. package/dist/internal.d.ts.map +0 -1
  195. package/dist/internal.js.map +0 -1
  196. package/dist/practices/01-remote-workflow.d.ts +0 -2
  197. package/dist/practices/01-remote-workflow.d.ts.map +0 -1
  198. package/dist/practices/01-remote-workflow.js +0 -70
  199. package/dist/practices/01-remote-workflow.js.map +0 -1
  200. package/dist/registry.test.d.ts +0 -2
  201. package/dist/registry.test.d.ts.map +0 -1
  202. package/dist/registry.test.js +0 -95
  203. package/dist/registry.test.js.map +0 -1
  204. package/dist/testing/connection.d.ts +0 -7
  205. package/dist/testing/connection.d.ts.map +0 -1
  206. package/dist/testing/connection.js +0 -39
  207. package/dist/testing/connection.js.map +0 -1
  208. package/dist/worker.test.d.ts +0 -2
  209. package/dist/worker.test.d.ts.map +0 -1
  210. package/dist/worker.test.js +0 -1164
  211. package/dist/worker.test.js.map +0 -1
  212. package/dist/workflow.test.d.ts +0 -2
  213. package/dist/workflow.test.d.ts.map +0 -1
  214. package/dist/workflow.test.js +0 -73
  215. package/dist/workflow.test.js.map +0 -1
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
+
2
3
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
3
- import type { Backend } from "..//backend";
4
- import type { SerializableRetryPolicy } from "../core/retry";
5
- import type { StepAttempt } from "../core/step";
6
- import type { WorkflowRun } from "../core/workflow";
4
+
5
+ import { type Backend } from "..//backend";
6
+ import { type SerializableRetryPolicy } from "../core/retry";
7
+ import { type StepAttempt } from "../core/step";
8
+ import { type WorkflowRun } from "../core/workflow";
7
9
 
8
10
  /**
9
11
  * Options for the Backend test suite.
@@ -138,7 +140,7 @@ export function testBackend(options: TestBackendOptions): void {
138
140
  // p2
139
141
  const page2 = await backend.listWorkflowRuns({
140
142
  limit: 2,
141
- // biome-ignore lint/style/noNonNullAssertion: for test
143
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
142
144
  after: page1.pagination.next!,
143
145
  });
144
146
  expect(page2.data).toHaveLength(2);
@@ -150,7 +152,7 @@ export function testBackend(options: TestBackendOptions): void {
150
152
  // p3
151
153
  const page3 = await backend.listWorkflowRuns({
152
154
  limit: 2,
153
- // biome-ignore lint/style/noNonNullAssertion: for test
155
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
154
156
  after: page2.pagination.next!,
155
157
  });
156
158
  expect(page3.data).toHaveLength(1);
@@ -161,7 +163,7 @@ export function testBackend(options: TestBackendOptions): void {
161
163
  // p2 again
162
164
  const page2Back = await backend.listWorkflowRuns({
163
165
  limit: 2,
164
- // biome-ignore lint/style/noNonNullAssertion: for test
166
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
165
167
  before: page3.pagination.prev!,
166
168
  });
167
169
  expect(page2Back.data).toHaveLength(2);
@@ -203,7 +205,7 @@ export function testBackend(options: TestBackendOptions): void {
203
205
 
204
206
  const page2 = await backend.listWorkflowRuns({
205
207
  limit: 2,
206
- // biome-ignore lint/style/noNonNullAssertion: for test
208
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
207
209
  after: page1.pagination.next!,
208
210
  });
209
211
  expect(page2.data).toHaveLength(2);
@@ -213,7 +215,7 @@ export function testBackend(options: TestBackendOptions): void {
213
215
 
214
216
  const page3 = await backend.listWorkflowRuns({
215
217
  limit: 2,
216
- // biome-ignore lint/style/noNonNullAssertion: for test
218
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
217
219
  after: page2.pagination.next!,
218
220
  });
219
221
  expect(page3.data).toHaveLength(1);
@@ -222,7 +224,7 @@ export function testBackend(options: TestBackendOptions): void {
222
224
 
223
225
  const page2Back = await backend.listWorkflowRuns({
224
226
  limit: 2,
225
- // biome-ignore lint/style/noNonNullAssertion: for test
227
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
226
228
  before: page3.pagination.prev!,
227
229
  });
228
230
  expect(page2Back.data).toHaveLength(2);
@@ -678,7 +680,7 @@ export function testBackend(options: TestBackendOptions): void {
678
680
 
679
681
  const created = await backend.createStepAttempt({
680
682
  workflowRunId: expected.workflowRunId,
681
- // biome-ignore lint/style/noNonNullAssertion: for test
683
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
682
684
  workerId: workflowRun.workerId!,
683
685
  stepName: expected.stepName,
684
686
  kind: expected.kind,
@@ -704,7 +706,7 @@ export function testBackend(options: TestBackendOptions): void {
704
706
 
705
707
  const created = await backend.createStepAttempt({
706
708
  workflowRunId: claimed.id,
707
- // biome-ignore lint/style/noNonNullAssertion: for test
709
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
708
710
  workerId: claimed.workerId!,
709
711
  stepName: randomUUID(),
710
712
  kind: "function",
@@ -733,7 +735,7 @@ export function testBackend(options: TestBackendOptions): void {
733
735
 
734
736
  const first = await backend.createStepAttempt({
735
737
  workflowRunId: claimed.id,
736
- // biome-ignore lint/style/noNonNullAssertion: for test
738
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
737
739
  workerId: claimed.workerId!,
738
740
  stepName: randomUUID(),
739
741
  kind: "function",
@@ -743,7 +745,7 @@ export function testBackend(options: TestBackendOptions): void {
743
745
  await backend.completeStepAttempt({
744
746
  workflowRunId: claimed.id,
745
747
  stepAttemptId: first.id,
746
- // biome-ignore lint/style/noNonNullAssertion: for test
748
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
747
749
  workerId: claimed.workerId!,
748
750
  output: { ok: true },
749
751
  });
@@ -752,7 +754,7 @@ export function testBackend(options: TestBackendOptions): void {
752
754
 
753
755
  const second = await backend.createStepAttempt({
754
756
  workflowRunId: claimed.id,
755
- // biome-ignore lint/style/noNonNullAssertion: for test
757
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
756
758
  workerId: claimed.workerId!,
757
759
  stepName: randomUUID(),
758
760
  kind: "function",
@@ -773,7 +775,7 @@ export function testBackend(options: TestBackendOptions): void {
773
775
  for (let i = 0; i < 5; i++) {
774
776
  await backend.createStepAttempt({
775
777
  workflowRunId: claimed.id,
776
- // biome-ignore lint/style/noNonNullAssertion: for test
778
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
777
779
  workerId: claimed.workerId!,
778
780
  stepName: `step-${String(i)}`,
779
781
  kind: "function",
@@ -799,7 +801,7 @@ export function testBackend(options: TestBackendOptions): void {
799
801
  const page2 = await backend.listStepAttempts({
800
802
  workflowRunId: claimed.id,
801
803
  limit: 2,
802
- // biome-ignore lint/style/noNonNullAssertion: for test
804
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
803
805
  after: page1.pagination.next!,
804
806
  });
805
807
  expect(page2.data).toHaveLength(2);
@@ -812,7 +814,7 @@ export function testBackend(options: TestBackendOptions): void {
812
814
  const page3 = await backend.listStepAttempts({
813
815
  workflowRunId: claimed.id,
814
816
  limit: 2,
815
- // biome-ignore lint/style/noNonNullAssertion: for test
817
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
816
818
  after: page2.pagination.next!,
817
819
  });
818
820
  expect(page3.data).toHaveLength(1);
@@ -824,7 +826,7 @@ export function testBackend(options: TestBackendOptions): void {
824
826
  const page2Back = await backend.listStepAttempts({
825
827
  workflowRunId: claimed.id,
826
828
  limit: 2,
827
- // biome-ignore lint/style/noNonNullAssertion: for test
829
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
828
830
  before: page3.pagination.prev!,
829
831
  });
830
832
  expect(page2Back.data).toHaveLength(2);
@@ -848,7 +850,7 @@ export function testBackend(options: TestBackendOptions): void {
848
850
  const claimed = await createClaimedWorkflowRun(backend);
849
851
  await backend.createStepAttempt({
850
852
  workflowRunId: claimed.id,
851
- // biome-ignore lint/style/noNonNullAssertion: for test
853
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
852
854
  workerId: claimed.workerId!,
853
855
  stepName: "step-1",
854
856
  kind: "function",
@@ -880,7 +882,7 @@ export function testBackend(options: TestBackendOptions): void {
880
882
 
881
883
  const created = await backend.createStepAttempt({
882
884
  workflowRunId: claimed.id,
883
- // biome-ignore lint/style/noNonNullAssertion: for test
885
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
884
886
  workerId: claimed.workerId!,
885
887
  stepName: randomUUID(),
886
888
  kind: "function",
@@ -892,15 +894,16 @@ export function testBackend(options: TestBackendOptions): void {
892
894
  const completed = await backend.completeStepAttempt({
893
895
  workflowRunId: claimed.id,
894
896
  stepAttemptId: created.id,
895
- // biome-ignore lint/style/noNonNullAssertion: for test
897
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
896
898
  workerId: claimed.workerId!,
897
899
  output,
898
900
  });
899
901
 
900
- expect(completed.status).toBe("completed");
901
- expect(completed.output).toEqual(output);
902
- expect(completed.error).toBeNull();
903
- expect(completed.finishedAt).not.toBeNull();
902
+ expect(completed).not.toBeNull();
903
+ expect(completed?.status).toBe("completed");
904
+ expect(completed?.output).toEqual(output);
905
+ expect(completed?.error).toBeNull();
906
+ expect(completed?.finishedAt).not.toBeNull();
904
907
 
905
908
  const fetched = await backend.getStepAttempt({
906
909
  stepAttemptId: created.id,
@@ -924,7 +927,7 @@ export function testBackend(options: TestBackendOptions): void {
924
927
 
925
928
  const stepAttempt = await backend.createStepAttempt({
926
929
  workflowRunId: claimed.id,
927
- // biome-ignore lint/style/noNonNullAssertion: for test
930
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
928
931
  workerId: claimed.workerId!,
929
932
  stepName: randomUUID(),
930
933
  kind: "function",
@@ -935,7 +938,7 @@ export function testBackend(options: TestBackendOptions): void {
935
938
  // complete the workflow so it's no longer running
936
939
  await backend.completeWorkflowRun({
937
940
  workflowRunId: claimed.id,
938
- // biome-ignore lint/style/noNonNullAssertion: for test
941
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
939
942
  workerId: claimed.workerId!,
940
943
  output: null,
941
944
  });
@@ -945,7 +948,7 @@ export function testBackend(options: TestBackendOptions): void {
945
948
  backend.completeStepAttempt({
946
949
  workflowRunId: claimed.id,
947
950
  stepAttemptId: stepAttempt.id,
948
- // biome-ignore lint/style/noNonNullAssertion: for test
951
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
949
952
  workerId: claimed.workerId!,
950
953
  output: { foo: "bar" },
951
954
  }),
@@ -962,7 +965,7 @@ export function testBackend(options: TestBackendOptions): void {
962
965
  backend.completeStepAttempt({
963
966
  workflowRunId: claimed.id,
964
967
  stepAttemptId: randomUUID(),
965
- // biome-ignore lint/style/noNonNullAssertion: for test
968
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
966
969
  workerId: claimed.workerId!,
967
970
  output: { foo: "bar" },
968
971
  }),
@@ -986,7 +989,7 @@ export function testBackend(options: TestBackendOptions): void {
986
989
 
987
990
  const created = await backend.createStepAttempt({
988
991
  workflowRunId: claimed.id,
989
- // biome-ignore lint/style/noNonNullAssertion: for test
992
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
990
993
  workerId: claimed.workerId!,
991
994
  stepName: randomUUID(),
992
995
  kind: "function",
@@ -998,15 +1001,16 @@ export function testBackend(options: TestBackendOptions): void {
998
1001
  const failed = await backend.failStepAttempt({
999
1002
  workflowRunId: claimed.id,
1000
1003
  stepAttemptId: created.id,
1001
- // biome-ignore lint/style/noNonNullAssertion: for test
1004
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
1002
1005
  workerId: claimed.workerId!,
1003
1006
  error,
1004
1007
  });
1005
1008
 
1006
- expect(failed.status).toBe("failed");
1007
- expect(failed.error).toEqual(error);
1008
- expect(failed.output).toBeNull();
1009
- expect(failed.finishedAt).not.toBeNull();
1009
+ expect(failed).not.toBeNull();
1010
+ expect(failed?.status).toBe("failed");
1011
+ expect(failed?.error).toEqual(error);
1012
+ expect(failed?.output).toBeNull();
1013
+ expect(failed?.finishedAt).not.toBeNull();
1010
1014
 
1011
1015
  const fetched = await backend.getStepAttempt({
1012
1016
  stepAttemptId: created.id,
@@ -1030,7 +1034,7 @@ export function testBackend(options: TestBackendOptions): void {
1030
1034
 
1031
1035
  const stepAttempt = await backend.createStepAttempt({
1032
1036
  workflowRunId: claimed.id,
1033
- // biome-ignore lint/style/noNonNullAssertion: for test
1037
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
1034
1038
  workerId: claimed.workerId!,
1035
1039
  stepName: randomUUID(),
1036
1040
  kind: "function",
@@ -1041,7 +1045,7 @@ export function testBackend(options: TestBackendOptions): void {
1041
1045
  // complete the workflow so it's no longer running
1042
1046
  await backend.completeWorkflowRun({
1043
1047
  workflowRunId: claimed.id,
1044
- // biome-ignore lint/style/noNonNullAssertion: for test
1048
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
1045
1049
  workerId: claimed.workerId!,
1046
1050
  output: null,
1047
1051
  });
@@ -1051,7 +1055,7 @@ export function testBackend(options: TestBackendOptions): void {
1051
1055
  backend.failStepAttempt({
1052
1056
  workflowRunId: claimed.id,
1053
1057
  stepAttemptId: stepAttempt.id,
1054
- // biome-ignore lint/style/noNonNullAssertion: for test
1058
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
1055
1059
  workerId: claimed.workerId!,
1056
1060
  error: { message: "nope" },
1057
1061
  }),
@@ -1068,7 +1072,7 @@ export function testBackend(options: TestBackendOptions): void {
1068
1072
  backend.failStepAttempt({
1069
1073
  workflowRunId: claimed.id,
1070
1074
  stepAttemptId: randomUUID(),
1071
- // biome-ignore lint/style/noNonNullAssertion: for test
1075
+ // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion -- for test
1072
1076
  workerId: claimed.workerId!,
1073
1077
  error: { message: "nope" },
1074
1078
  }),
@@ -1,6 +1,9 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
  import { camelize } from "inflection";
3
- import knex, { type Knex } from "knex";
3
+ import knex from "knex";
4
+ import { type Knex } from "knex";
5
+
6
+ import { DEFAULT_NAMESPACE_ID } from "../backend";
4
7
  import {
5
8
  type Backend,
6
9
  type CancelWorkflowRunParams,
@@ -9,7 +12,6 @@ import {
9
12
  type CompleteWorkflowRunParams,
10
13
  type CreateStepAttemptParams,
11
14
  type CreateWorkflowRunParams,
12
- DEFAULT_NAMESPACE_ID,
13
15
  type ExtendWorkflowRunLeaseParams,
14
16
  type FailStepAttemptParams,
15
17
  type FailWorkflowRunParams,
@@ -18,13 +20,17 @@ import {
18
20
  type ListStepAttemptsParams,
19
21
  type ListWorkflowRunsParams,
20
22
  type PaginatedResponse,
23
+ type PauseWorkflowRunParams,
24
+ type ResumeWorkflowRunParams,
21
25
  type SleepWorkflowRunParams,
22
26
  } from "../backend";
23
- import { mergeRetryPolicy, type SerializableRetryPolicy } from "../core/retry";
24
- import type { StepAttempt } from "../core/step";
25
- import type { WorkflowRun } from "../core/workflow";
27
+ import { mergeRetryPolicy } from "../core/retry";
28
+ import { type SerializableRetryPolicy } from "../core/retry";
29
+ import { type StepAttempt } from "../core/step";
30
+ import { type WorkflowRun } from "../core/workflow";
26
31
  import { DEFAULT_SCHEMA, migrate } from "./base";
27
- import { type OnSubscribed, PostgresPubSub } from "./pubsub";
32
+ import { PostgresPubSub } from "./pubsub";
33
+ import { type OnSubscribed } from "./pubsub";
28
34
 
29
35
  export const DEFAULT_LISTEN_CHANNEL = "new_tasks" as const;
30
36
  const DEFAULT_PAGINATION_PAGE_SIZE = 100 as const;
@@ -155,6 +161,8 @@ export class BackendPostgres implements Backend {
155
161
  await this.pubsub?.destroy();
156
162
  this.pubsub = null;
157
163
  await this.knex.destroy();
164
+ this._knex = null;
165
+ this.initialized = false;
158
166
  }
159
167
 
160
168
  async createWorkflowRun(params: CreateWorkflowRunParams): Promise<WorkflowRun> {
@@ -253,6 +261,8 @@ export class BackendPostgres implements Backend {
253
261
  });
254
262
  const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
255
263
  const { after, before } = params;
264
+ const order = params.order ?? "asc";
265
+ const reverseOrder = order === "asc" ? "desc" : "asc";
256
266
 
257
267
  let cursor: Cursor | null = null;
258
268
  if (after) {
@@ -261,10 +271,10 @@ export class BackendPostgres implements Backend {
261
271
  cursor = decodeCursor(before);
262
272
  }
263
273
 
264
- const qb = this.buildListWorkflowRunsWhere(params, cursor);
274
+ const qb = this.buildListWorkflowRunsWhere(params, cursor, order);
265
275
  const rows = await qb
266
- .orderBy("created_at", before ? "desc" : "asc")
267
- .orderBy("id", before ? "desc" : "asc")
276
+ .orderBy("created_at", before ? reverseOrder : order)
277
+ .orderBy("id", before ? reverseOrder : order)
268
278
  .limit(limit + 1);
269
279
 
270
280
  return this.processPaginationResults(
@@ -275,7 +285,11 @@ export class BackendPostgres implements Backend {
275
285
  );
276
286
  }
277
287
 
278
- private buildListWorkflowRunsWhere(params: ListWorkflowRunsParams, cursor: Cursor | null) {
288
+ private buildListWorkflowRunsWhere(
289
+ params: ListWorkflowRunsParams,
290
+ cursor: Cursor | null,
291
+ order: "asc" | "desc",
292
+ ) {
279
293
  const { after } = params;
280
294
  const qb = this.knex
281
295
  .withSchema(DEFAULT_SCHEMA)
@@ -283,13 +297,28 @@ export class BackendPostgres implements Backend {
283
297
  .where("namespace_id", this.namespaceId);
284
298
 
285
299
  if (cursor) {
286
- const operator = after ? ">" : "<";
287
- return qb.whereRaw(`("created_at", "id") ${operator} (?, ?)`, [
300
+ // asc: after ">", before "<"
301
+ // desc: after → "<", before → ">"
302
+ const operator = (order === "asc") === !!after ? ">" : "<";
303
+ qb.whereRaw(`("created_at", "id") ${operator} (?, ?)`, [
288
304
  cursor.createdAt.toISOString(),
289
305
  cursor.id,
290
306
  ]);
291
307
  }
292
308
 
309
+ if (params.status && params.status.length > 0) {
310
+ qb.whereIn("status", params.status);
311
+ }
312
+ if (params.workflowName) {
313
+ qb.where("workflow_name", params.workflowName);
314
+ }
315
+ if (params.createdAfter) {
316
+ qb.where("created_at", ">=", params.createdAfter);
317
+ }
318
+ if (params.createdBefore) {
319
+ qb.where("created_at", "<=", params.createdBefore);
320
+ }
321
+
293
322
  return qb;
294
323
  }
295
324
 
@@ -381,6 +410,11 @@ export class BackendPostgres implements Backend {
381
410
  .returning("*");
382
411
 
383
412
  if (!updated) {
413
+ const wr = await this.getWorkflowRun({ workflowRunId: params.workflowRunId });
414
+ if (wr && (wr.status === "paused" || wr.status === "canceled")) {
415
+ throw new Error("Workflow run is paused or canceled");
416
+ }
417
+
384
418
  logger.error("Failed to extend lease for workflow run: {params}", { params });
385
419
  throw new Error("Failed to extend lease for workflow run");
386
420
  }
@@ -575,7 +609,7 @@ export class BackendPostgres implements Backend {
575
609
  .table("workflow_runs")
576
610
  .where("namespace_id", this.namespaceId)
577
611
  .where("id", params.workflowRunId)
578
- .whereIn("status", ["pending", "running", "sleeping"])
612
+ .whereIn("status", ["pending", "running", "sleeping", "paused"])
579
613
  .update({
580
614
  status: "canceled",
581
615
  worker_id: null,
@@ -618,6 +652,111 @@ export class BackendPostgres implements Backend {
618
652
  return updated;
619
653
  }
620
654
 
655
+ async pauseWorkflowRun(params: PauseWorkflowRunParams): Promise<WorkflowRun> {
656
+ if (!this.initialized) {
657
+ throw new Error("Backend not initialized");
658
+ }
659
+
660
+ logger.info("Pausing workflow run: {workflowRunId}", { workflowRunId: params.workflowRunId });
661
+
662
+ const [updated] = await this.knex
663
+ .withSchema(DEFAULT_SCHEMA)
664
+ .table("workflow_runs")
665
+ .where("namespace_id", this.namespaceId)
666
+ .where("id", params.workflowRunId)
667
+ .whereIn("status", ["pending", "running", "sleeping"])
668
+ .update({
669
+ status: "paused",
670
+ worker_id: null,
671
+ available_at: null,
672
+ updated_at: this.knex.fn.now(),
673
+ })
674
+ .returning("*");
675
+
676
+ if (!updated) {
677
+ const existing = await this.getWorkflowRun({
678
+ workflowRunId: params.workflowRunId,
679
+ });
680
+ if (!existing) {
681
+ throw new Error(`Workflow run ${params.workflowRunId} does not exist`);
682
+ }
683
+
684
+ // 이미 paused이면 멱등하게 반환합니다.
685
+ if (existing.status === "paused") {
686
+ return existing;
687
+ }
688
+
689
+ // 터미널 상태에서는 pause할 수 없습니다.
690
+ // 'succeeded' status is deprecated
691
+ if (["succeeded", "completed", "failed", "canceled"].includes(existing.status)) {
692
+ logger.error("Cannot pause workflow run: {params} with status {status}", {
693
+ params,
694
+ status: existing.status,
695
+ });
696
+ throw new Error(
697
+ `Cannot pause workflow run ${params.workflowRunId} with status ${existing.status}`,
698
+ );
699
+ }
700
+
701
+ logger.error("Failed to pause workflow run: {params}", { params });
702
+ throw new Error("Failed to pause workflow run");
703
+ }
704
+
705
+ return updated;
706
+ }
707
+
708
+ async resumeWorkflowRun(params: ResumeWorkflowRunParams): Promise<WorkflowRun> {
709
+ if (!this.initialized) {
710
+ throw new Error("Backend not initialized");
711
+ }
712
+
713
+ logger.info("Resuming workflow run: {workflowRunId}", { workflowRunId: params.workflowRunId });
714
+
715
+ const [updated] = await this.knex
716
+ .withSchema(DEFAULT_SCHEMA)
717
+ .table("workflow_runs")
718
+ .where("namespace_id", this.namespaceId)
719
+ .where("id", params.workflowRunId)
720
+ .where("status", "paused")
721
+ .update({
722
+ status: "pending",
723
+ available_at: this.knex.fn.now(),
724
+ updated_at: this.knex.fn.now(),
725
+ })
726
+ .returning("*");
727
+
728
+ if (!updated) {
729
+ const existing = await this.getWorkflowRun({
730
+ workflowRunId: params.workflowRunId,
731
+ });
732
+ if (!existing) {
733
+ throw new Error(`Workflow run ${params.workflowRunId} does not exist`);
734
+ }
735
+
736
+ // 이미 pending/running이면 멱등하게 반환합니다.
737
+ if (existing.status === "pending" || existing.status === "running") {
738
+ return existing;
739
+ }
740
+
741
+ // 터미널 상태에서는 resume할 수 없습니다.
742
+ // 'succeeded' status is deprecated
743
+ if (["succeeded", "completed", "failed", "canceled"].includes(existing.status)) {
744
+ logger.error("Cannot resume workflow run: {params} with status {status}", {
745
+ params,
746
+ status: existing.status,
747
+ });
748
+ throw new Error(
749
+ `Cannot resume workflow run ${params.workflowRunId} with status ${existing.status}`,
750
+ );
751
+ }
752
+
753
+ logger.error("Failed to resume workflow run: {params}", { params });
754
+ throw new Error("Failed to resume workflow run");
755
+ }
756
+
757
+ return updated;
758
+ }
759
+
621
760
  async createStepAttempt(params: CreateStepAttemptParams): Promise<StepAttempt> {
622
761
  if (!this.initialized) {
623
762
  throw new Error("Backend not initialized");
@@ -685,6 +824,8 @@ export class BackendPostgres implements Backend {
685
824
 
686
825
  const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
687
826
  const { after, before } = params;
827
+ const order = params.order ?? "asc";
828
+ const reverseOrder = order === "asc" ? "desc" : "asc";
688
829
 
689
830
  let cursor: Cursor | null = null;
690
831
  if (after) {
@@ -693,10 +834,10 @@ export class BackendPostgres implements Backend {
693
834
  cursor = decodeCursor(before);
694
835
  }
695
836
 
696
- const qb = this.buildListStepAttemptsWhere(params, cursor);
837
+ const qb = this.buildListStepAttemptsWhere(params, cursor, order);
697
838
  const rows = await qb
698
- .orderBy("created_at", before ? "desc" : "asc")
699
- .orderBy("id", before ? "desc" : "asc")
839
+ .orderBy("created_at", before ? reverseOrder : order)
840
+ .orderBy("id", before ? reverseOrder : order)
700
841
  .limit(limit + 1);
701
842
 
702
843
  return this.processPaginationResults(
@@ -707,7 +848,11 @@ export class BackendPostgres implements Backend {
707
848
  );
708
849
  }
709
850
 
710
- private buildListStepAttemptsWhere(params: ListStepAttemptsParams, cursor: Cursor | null) {
851
+ private buildListStepAttemptsWhere(
852
+ params: ListStepAttemptsParams,
853
+ cursor: Cursor | null,
854
+ order: "asc" | "desc",
855
+ ) {
711
856
  const { after } = params;
712
857
  const qb = this.knex
713
858
  .withSchema(DEFAULT_SCHEMA)
@@ -716,7 +861,9 @@ export class BackendPostgres implements Backend {
716
861
  .where("workflow_run_id", params.workflowRunId);
717
862
 
718
863
  if (cursor) {
719
- const operator = after ? ">" : "<";
864
+ // asc: after ">", before "<"
865
+ // desc: after → "<", before → ">"
866
+ const operator = (order === "asc") === !!after ? ">" : "<";
720
867
  return qb.whereRaw(`("created_at", "id") ${operator} (?, ?)`, [
721
868
  cursor.createdAt.toISOString(),
722
869
  cursor.id,
@@ -767,8 +914,10 @@ export class BackendPostgres implements Backend {
767
914
  };
768
915
  }
769
916
 
770
- // NOTE: 실제 서비스에서 이게 되는 것 같은데, 쿼리 등을 체크할 필요가 있음.
771
- async completeStepAttempt(params: CompleteStepAttemptParams): Promise<StepAttempt> {
917
+ // WHERE 조건에 wr.status='running', sa.status='running'이 포함되어 있어,
918
+ // 외부에서 워크플로우 상태가 변경된 경우(pause/cancel) null을 반환합니다.
919
+ // 예상하지 못한 이유로 실패한 경우에는 에러를 로깅합니다.
920
+ async completeStepAttempt(params: CompleteStepAttemptParams): Promise<StepAttempt | null> {
772
921
  if (!this.initialized) {
773
922
  throw new Error("Backend not initialized");
774
923
  }
@@ -801,14 +950,13 @@ export class BackendPostgres implements Backend {
801
950
  .returning("sa.*");
802
951
 
803
952
  if (!updated) {
804
- logger.error("Failed to mark step attempt completed: {params}", { params });
805
- throw new Error("Failed to mark step attempt completed");
953
+ return this.handleStepAttemptUpdateMiss("completed", params);
806
954
  }
807
955
 
808
956
  return updated;
809
957
  }
810
958
 
811
- async failStepAttempt(params: FailStepAttemptParams): Promise<StepAttempt> {
959
+ async failStepAttempt(params: FailStepAttemptParams): Promise<StepAttempt | null> {
812
960
  if (!this.initialized) {
813
961
  throw new Error("Backend not initialized");
814
962
  }
@@ -842,12 +990,46 @@ export class BackendPostgres implements Backend {
842
990
  .returning("sa.*");
843
991
 
844
992
  if (!updated) {
845
- logger.error("Failed to mark step attempt failed: {params}", { params });
846
- throw new Error("Failed to mark step attempt failed");
993
+ return this.handleStepAttemptUpdateMiss("failed", params);
847
994
  }
848
995
 
849
996
  return updated;
850
997
  }
998
+
999
+ /**
1000
+ * completeStepAttempt/failStepAttempt에서 UPDATE가 0건일 때,
1001
+ * 외부 상태 변경(pause/cancel)에 의한 것인지 판단합니다.
1002
+ * - 외부 상태 변경이면 해당 step의 상태도 워크플로우와 동일하게 맞추고 null을 반환합니다.
1003
+ * - 그 외에는 예상하지 못한 상황이므로 에러를 throw합니다.
1004
+ */
1005
+ private async handleStepAttemptUpdateMiss(
1006
+ method: string,
1007
+ params: { workflowRunId: string; stepAttemptId: string; workerId: string },
1008
+ ): Promise<null> {
1009
+ const wr = await this.getWorkflowRun({ workflowRunId: params.workflowRunId });
1010
+
1011
+ // 워크플로우가 외부에서 paused/canceled된 경우 → step 상태도 동일하게 갱신하고 null 반환
1012
+ if (wr && (wr.status === "paused" || wr.status === "canceled")) {
1013
+ await this.knex
1014
+ .withSchema(DEFAULT_SCHEMA)
1015
+ .table("step_attempts")
1016
+ .where("namespace_id", this.namespaceId)
1017
+ .where("id", params.stepAttemptId)
1018
+ .whereIn("status", ["running", "paused"])
1019
+ .update({
1020
+ status: wr.status,
1021
+ updated_at: this.knex.fn.now(),
1022
+ });
1023
+ return null;
1024
+ }
1025
+
1026
+ // 그 외(워크플로우가 여전히 running인데 UPDATE가 안 된 경우 등) → 예상 못한 상황
1027
+ logger.error("Failed to mark step attempt {method}: {params}", {
1028
+ method,
1029
+ params,
1030
+ });
1031
+ throw new Error(`Failed to mark step attempt ${method}`);
1032
+ }
851
1033
  }
852
1034
 
853
1035
  /**