alepha 0.19.3 → 0.19.4

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/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +99 -43
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +257 -40
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +5 -5
  14. package/dist/api/notifications/index.browser.js +0 -1
  15. package/dist/api/notifications/index.browser.js.map +1 -1
  16. package/dist/api/notifications/index.d.ts +3 -3
  17. package/dist/api/notifications/index.d.ts.map +1 -1
  18. package/dist/api/notifications/index.js +0 -1
  19. package/dist/api/notifications/index.js.map +1 -1
  20. package/dist/api/parameters/index.browser.js +112 -1
  21. package/dist/api/parameters/index.browser.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +90 -3
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +79 -12
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/{billing → api/payments}/index.d.ts +67 -49
  27. package/dist/api/payments/index.d.ts.map +1 -0
  28. package/dist/{billing → api/payments}/index.js +108 -74
  29. package/dist/api/payments/index.js.map +1 -0
  30. package/dist/api/subscriptions/index.d.ts +1692 -0
  31. package/dist/api/subscriptions/index.d.ts.map +1 -0
  32. package/dist/api/subscriptions/index.js +1870 -0
  33. package/dist/api/subscriptions/index.js.map +1 -0
  34. package/dist/api/users/index.d.ts +18 -2
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +167 -34
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.d.ts +13 -13
  39. package/dist/api/workflows/index.browser.js +246 -0
  40. package/dist/api/workflows/index.browser.js.map +1 -0
  41. package/dist/api/workflows/index.d.ts +1618 -0
  42. package/dist/api/workflows/index.d.ts.map +1 -0
  43. package/dist/api/workflows/index.js +1504 -0
  44. package/dist/api/workflows/index.js.map +1 -0
  45. package/dist/cli/core/index.d.ts +44 -28
  46. package/dist/cli/core/index.d.ts.map +1 -1
  47. package/dist/cli/core/index.js +16 -61
  48. package/dist/cli/core/index.js.map +1 -1
  49. package/dist/cli/vendor/index.d.ts +31 -8
  50. package/dist/cli/vendor/index.d.ts.map +1 -1
  51. package/dist/cli/vendor/index.js +79 -24
  52. package/dist/cli/vendor/index.js.map +1 -1
  53. package/dist/core/index.browser.js +21 -2
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +33 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +21 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/index.native.js +21 -2
  60. package/dist/core/index.native.js.map +1 -1
  61. package/dist/core/index.workerd.js +21 -2
  62. package/dist/core/index.workerd.js.map +1 -1
  63. package/dist/email/smtp/index.js +24 -8
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/orm/core/index.browser.js +0 -18
  66. package/dist/orm/core/index.browser.js.map +1 -1
  67. package/dist/orm/core/index.bun.js +0 -17
  68. package/dist/orm/core/index.bun.js.map +1 -1
  69. package/dist/orm/core/index.d.ts +1 -13
  70. package/dist/orm/core/index.d.ts.map +1 -1
  71. package/dist/orm/core/index.js +0 -17
  72. package/dist/orm/core/index.js.map +1 -1
  73. package/dist/orm/postgres/index.bun.js +3 -3
  74. package/dist/orm/postgres/index.bun.js.map +1 -1
  75. package/dist/orm/postgres/index.d.ts.map +1 -1
  76. package/dist/orm/postgres/index.js +3 -3
  77. package/dist/orm/postgres/index.js.map +1 -1
  78. package/dist/react/router/index.browser.js +25 -3
  79. package/dist/react/router/index.browser.js.map +1 -1
  80. package/dist/react/router/index.d.ts +16 -1
  81. package/dist/react/router/index.d.ts.map +1 -1
  82. package/dist/react/router/index.js +25 -3
  83. package/dist/react/router/index.js.map +1 -1
  84. package/dist/security/index.d.ts +28 -0
  85. package/dist/security/index.d.ts.map +1 -1
  86. package/dist/security/index.js +28 -0
  87. package/dist/security/index.js.map +1 -1
  88. package/package.json +37 -20
  89. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  90. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  91. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  92. package/src/api/invitations/entities/invitations.ts +33 -0
  93. package/src/api/invitations/index.ts +65 -0
  94. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  95. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  96. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  97. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  98. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  99. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  100. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  101. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  102. package/src/api/invitations/services/InvitationService.ts +556 -0
  103. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  104. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  105. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  106. package/src/api/jobs/index.ts +0 -3
  107. package/src/api/jobs/primitives/$job.ts +22 -11
  108. package/src/api/jobs/providers/JobProvider.ts +229 -19
  109. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  110. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  111. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  112. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  113. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  114. package/src/api/jobs/services/JobService.ts +51 -12
  115. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  116. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  117. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  118. package/src/api/parameters/index.browser.ts +12 -0
  119. package/src/api/parameters/primitives/$parameter.ts +20 -3
  120. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  121. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  122. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  123. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  124. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  125. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  126. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  127. package/src/{billing → api/payments}/index.ts +31 -25
  128. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  129. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  130. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  131. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  132. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  133. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  134. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  135. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  136. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  137. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  138. package/src/api/subscriptions/index.ts +144 -0
  139. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  140. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  141. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  142. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  143. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  144. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  145. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  146. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  147. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  148. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  149. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  150. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  151. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  152. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  153. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  154. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  155. package/src/api/subscriptions/services/BillingService.ts +437 -0
  156. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  157. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  158. package/src/api/subscriptions/services/UsageService.ts +118 -0
  159. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  160. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  161. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  162. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  163. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  164. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  165. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  166. package/src/api/users/controllers/UserController.ts +3 -8
  167. package/src/api/users/notifications/UserNotifications.ts +23 -0
  168. package/src/api/users/schemas/loginSchema.ts +1 -1
  169. package/src/api/users/services/CredentialService.ts +51 -4
  170. package/src/api/users/services/RegistrationService.ts +38 -9
  171. package/src/api/users/services/SessionService.ts +62 -9
  172. package/src/api/users/services/UserService.ts +21 -12
  173. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  174. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  175. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  176. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  177. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  178. package/src/api/workflows/index.browser.ts +22 -0
  179. package/src/api/workflows/index.ts +124 -0
  180. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  181. package/src/api/workflows/primitives/$workflow.ts +202 -0
  182. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  183. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  184. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  185. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  186. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  187. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  188. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  189. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  190. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  191. package/src/api/workflows/services/WorkflowService.ts +382 -0
  192. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  193. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  194. package/src/cli/vendor/services/VendorService.ts +126 -27
  195. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  196. package/src/core/providers/SchemaValidator.ts +1 -1
  197. package/src/core/providers/TypeProvider.ts +46 -3
  198. package/src/orm/__tests__/enums.spec.ts +22 -29
  199. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  200. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  202. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  203. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  204. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  205. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  206. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  207. package/src/security/primitives/$secure.ts +28 -0
  208. package/dist/billing/index.d.ts.map +0 -1
  209. package/dist/billing/index.js.map +0 -1
  210. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  211. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  212. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  213. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  214. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  215. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -2020,4 +2020,880 @@ describe("$job v2", () => {
2020
2020
  expect(handler).toHaveBeenCalledTimes(3);
2021
2021
  });
2022
2022
  });
2023
+
2024
+ // ----- Concurrency enforcement -----
2025
+
2026
+ describe("concurrency enforcement", () => {
2027
+ it("should execute serially with concurrency: 1", async () => {
2028
+ let maxRunning = 0;
2029
+ let currentRunning = 0;
2030
+
2031
+ class App {
2032
+ repo = $repository(jobExecutionEntity);
2033
+ myJob = $job({
2034
+ schema: t.object({ id: t.text() }),
2035
+ concurrency: 1,
2036
+ handler: async () => {
2037
+ currentRunning++;
2038
+ maxRunning = Math.max(maxRunning, currentRunning);
2039
+ await new Promise((r) => setTimeout(r, 50));
2040
+ currentRunning--;
2041
+ },
2042
+ });
2043
+ }
2044
+
2045
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2046
+ const app = alepha.inject(App);
2047
+ await alepha.start();
2048
+
2049
+ await app.myJob.push([{ id: "1" }, { id: "2" }, { id: "3" }]);
2050
+
2051
+ await vi.waitFor(
2052
+ async () => {
2053
+ const completed = await app.repo.findMany({
2054
+ where: { jobName: "App.myJob", status: "completed" },
2055
+ });
2056
+ expect(completed).toHaveLength(3);
2057
+ },
2058
+ { timeout: 10_000 },
2059
+ );
2060
+
2061
+ expect(maxRunning).toBe(1);
2062
+ });
2063
+
2064
+ it("should allow parallel execution up to concurrency limit", async () => {
2065
+ let maxRunning = 0;
2066
+ let currentRunning = 0;
2067
+
2068
+ class App {
2069
+ repo = $repository(jobExecutionEntity);
2070
+ myJob = $job({
2071
+ schema: t.object({ id: t.text() }),
2072
+ concurrency: 2,
2073
+ handler: async () => {
2074
+ currentRunning++;
2075
+ maxRunning = Math.max(maxRunning, currentRunning);
2076
+ await new Promise((r) => setTimeout(r, 50));
2077
+ currentRunning--;
2078
+ },
2079
+ });
2080
+ }
2081
+
2082
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2083
+ const app = alepha.inject(App);
2084
+ await alepha.start();
2085
+
2086
+ await app.myJob.push([
2087
+ { id: "1" },
2088
+ { id: "2" },
2089
+ { id: "3" },
2090
+ { id: "4" },
2091
+ ]);
2092
+
2093
+ await vi.waitFor(
2094
+ async () => {
2095
+ const completed = await app.repo.findMany({
2096
+ where: { jobName: "App.myJob", status: "completed" },
2097
+ });
2098
+ expect(completed).toHaveLength(4);
2099
+ },
2100
+ { timeout: 10_000 },
2101
+ );
2102
+
2103
+ expect(maxRunning).toBeLessThanOrEqual(2);
2104
+ });
2105
+
2106
+ it("should dispatch next pending after completion", async () => {
2107
+ const order: string[] = [];
2108
+
2109
+ class App {
2110
+ repo = $repository(jobExecutionEntity);
2111
+ myJob = $job({
2112
+ schema: t.object({ id: t.text() }),
2113
+ concurrency: 1,
2114
+ handler: async ({ items }) => {
2115
+ order.push(items[0]?.payload.id ?? "cron");
2116
+ },
2117
+ });
2118
+ }
2119
+
2120
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2121
+ const app = alepha.inject(App);
2122
+ await alepha.start();
2123
+
2124
+ await app.myJob.push({ id: "a" });
2125
+ await app.myJob.push({ id: "b" });
2126
+
2127
+ await vi.waitFor(
2128
+ async () => {
2129
+ const completed = await app.repo.findMany({
2130
+ where: { jobName: "App.myJob", status: "completed" },
2131
+ });
2132
+ expect(completed).toHaveLength(2);
2133
+ },
2134
+ { timeout: 10_000 },
2135
+ );
2136
+
2137
+ expect(order).toEqual(["a", "b"]);
2138
+ });
2139
+ });
2140
+
2141
+ // ----- Pause / Resume -----
2142
+
2143
+ describe("pause / resume", () => {
2144
+ it("should not dispatch while paused", async () => {
2145
+ const handler = vi.fn();
2146
+
2147
+ class App {
2148
+ repo = $repository(jobExecutionEntity);
2149
+ myJob = $job({
2150
+ schema: t.object({ value: t.text() }),
2151
+ handler,
2152
+ });
2153
+ }
2154
+
2155
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2156
+ const app = alepha.inject(App);
2157
+ await alepha.start();
2158
+
2159
+ app.myJob.pause();
2160
+ expect(app.myJob.paused).toBe(true);
2161
+
2162
+ await app.myJob.push({ value: "test" });
2163
+
2164
+ // Item should stay pending
2165
+ const executions = await app.repo.findMany({
2166
+ where: { jobName: "App.myJob" },
2167
+ });
2168
+ expect(executions).toHaveLength(1);
2169
+ expect(executions[0].status).toBe("pending");
2170
+ expect(handler).not.toHaveBeenCalled();
2171
+
2172
+ // Cleanup: resume so nothing leaks
2173
+ await app.myJob.resume();
2174
+ });
2175
+
2176
+ it("should accept pushes while paused and process on resume", async () => {
2177
+ const handler = vi.fn();
2178
+
2179
+ class App {
2180
+ repo = $repository(jobExecutionEntity);
2181
+ myJob = $job({
2182
+ schema: t.object({ value: t.text() }),
2183
+ handler,
2184
+ });
2185
+ }
2186
+
2187
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2188
+ const app = alepha.inject(App);
2189
+ await alepha.start();
2190
+
2191
+ app.myJob.pause();
2192
+
2193
+ await app.myJob.push({ value: "a" });
2194
+ await app.myJob.push({ value: "b" });
2195
+
2196
+ expect(handler).not.toHaveBeenCalled();
2197
+
2198
+ await app.myJob.resume();
2199
+ expect(app.myJob.paused).toBe(false);
2200
+
2201
+ await vi.waitFor(
2202
+ async () => {
2203
+ const completed = await app.repo.findMany({
2204
+ where: { jobName: "App.myJob", status: "completed" },
2205
+ });
2206
+ expect(completed).toHaveLength(2);
2207
+ },
2208
+ { timeout: 10_000 },
2209
+ );
2210
+
2211
+ expect(handler).toHaveBeenCalledTimes(2);
2212
+ });
2213
+
2214
+ it("should report paused status via JobService registry", async () => {
2215
+ class App {
2216
+ jobService = $inject(JobService);
2217
+ myJob = $job({
2218
+ schema: t.object({ value: t.text() }),
2219
+ handler: async () => {},
2220
+ });
2221
+ }
2222
+
2223
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2224
+ const app = alepha.inject(App);
2225
+ await alepha.start();
2226
+
2227
+ const before = app.jobService.getRegistry();
2228
+ expect(before[0].paused).toBe(false);
2229
+
2230
+ app.myJob.pause();
2231
+
2232
+ const after = app.jobService.getRegistry();
2233
+ expect(after[0].paused).toBe(true);
2234
+
2235
+ await app.myJob.resume();
2236
+ });
2237
+
2238
+ it("should expose paused jobs via getPausedJobs", async () => {
2239
+ class App {
2240
+ jobService = $inject(JobService);
2241
+ jobA = $job({
2242
+ schema: t.object({ v: t.text() }),
2243
+ handler: async () => {},
2244
+ });
2245
+ jobB = $job({
2246
+ schema: t.object({ v: t.text() }),
2247
+ handler: async () => {},
2248
+ });
2249
+ }
2250
+
2251
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2252
+ const app = alepha.inject(App);
2253
+ await alepha.start();
2254
+
2255
+ app.jobA.pause();
2256
+ expect(app.jobService.getPausedJobs()).toEqual(["App.jobA"]);
2257
+
2258
+ app.jobB.pause();
2259
+ expect(app.jobService.getPausedJobs()).toHaveLength(2);
2260
+
2261
+ await app.jobA.resume();
2262
+ expect(app.jobService.getPausedJobs()).toEqual(["App.jobB"]);
2263
+
2264
+ await app.jobB.resume();
2265
+ });
2266
+ });
2267
+
2268
+ // ----- Priority-ordered dispatch -----
2269
+
2270
+ describe("priority-ordered dispatch", () => {
2271
+ it("should dispatch higher priority jobs first", async () => {
2272
+ const order: string[] = [];
2273
+
2274
+ class App {
2275
+ repo = $repository(jobExecutionEntity);
2276
+ myJob = $job({
2277
+ schema: t.object({ label: t.text() }),
2278
+ concurrency: 1,
2279
+ handler: async ({ items }) => {
2280
+ order.push(items[0].payload.label);
2281
+ },
2282
+ });
2283
+ }
2284
+
2285
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2286
+ const app = alepha.inject(App);
2287
+ await alepha.start();
2288
+
2289
+ // Pause to queue items without dispatching
2290
+ app.myJob.pause();
2291
+
2292
+ await app.myJob.push({ label: "low" }, { priority: "low" });
2293
+ await app.myJob.push({ label: "critical" }, { priority: "critical" });
2294
+ await app.myJob.push({ label: "normal" });
2295
+
2296
+ // Resume triggers dispatch in priority order
2297
+ await app.myJob.resume();
2298
+
2299
+ await vi.waitFor(
2300
+ async () => {
2301
+ const completed = await app.repo.findMany({
2302
+ where: { jobName: "App.myJob", status: "completed" },
2303
+ });
2304
+ expect(completed).toHaveLength(3);
2305
+ },
2306
+ { timeout: 10_000 },
2307
+ );
2308
+
2309
+ expect(order).toEqual(["critical", "normal", "low"]);
2310
+ });
2311
+ });
2312
+
2313
+ // ----- Bulk pushMany -----
2314
+
2315
+ describe("bulk pushMany", () => {
2316
+ it("should bulk-create non-keyed items", async () => {
2317
+ const handler = vi.fn();
2318
+
2319
+ class App {
2320
+ repo = $repository(jobExecutionEntity);
2321
+ myJob = $job({
2322
+ schema: t.object({ n: t.integer() }),
2323
+ handler,
2324
+ });
2325
+ }
2326
+
2327
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2328
+ const app = alepha.inject(App);
2329
+ await alepha.start();
2330
+
2331
+ const ids = await app.myJob.pushMany([
2332
+ { payload: { n: 1 } },
2333
+ { payload: { n: 2 } },
2334
+ { payload: { n: 3 } },
2335
+ ]);
2336
+
2337
+ expect(ids).toHaveLength(3);
2338
+
2339
+ await vi.waitFor(
2340
+ async () => {
2341
+ const completed = await app.repo.findMany({
2342
+ where: { jobName: "App.myJob", status: "completed" },
2343
+ });
2344
+ expect(completed).toHaveLength(3);
2345
+ },
2346
+ { timeout: 10_000 },
2347
+ );
2348
+ });
2349
+
2350
+ it("should handle mixed keyed and non-keyed items", async () => {
2351
+ class App {
2352
+ repo = $repository(jobExecutionEntity);
2353
+ myJob = $job({
2354
+ schema: t.object({ v: t.text() }),
2355
+ handler: async () => {},
2356
+ });
2357
+ }
2358
+
2359
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2360
+ const app = alepha.inject(App);
2361
+ await alepha.start();
2362
+
2363
+ const ids = await app.myJob.pushMany([
2364
+ { payload: { v: "keyed" }, key: "k1", delay: [1, "hour"] },
2365
+ { payload: { v: "bulk1" } },
2366
+ { payload: { v: "bulk2" } },
2367
+ ]);
2368
+
2369
+ expect(ids).toHaveLength(3);
2370
+
2371
+ const keyed = await app.repo.findById(ids[0]);
2372
+ expect(keyed?.key).toBe("k1");
2373
+ expect(keyed?.status).toBe("scheduled");
2374
+
2375
+ await vi.waitFor(async () => {
2376
+ const completed = await app.repo.findMany({
2377
+ where: { jobName: "App.myJob", status: "completed" },
2378
+ });
2379
+ expect(completed).toHaveLength(2);
2380
+ });
2381
+ });
2382
+
2383
+ it("should reject all items if one payload is invalid", async () => {
2384
+ class App {
2385
+ repo = $repository(jobExecutionEntity);
2386
+ myJob = $job({
2387
+ schema: t.object({ n: t.integer() }),
2388
+ handler: async () => {},
2389
+ });
2390
+ }
2391
+
2392
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2393
+ const app = alepha.inject(App);
2394
+ await alepha.start();
2395
+
2396
+ await expect(
2397
+ app.myJob.pushMany([
2398
+ { payload: { n: 1 } },
2399
+ { payload: { n: "not-a-number" as any } },
2400
+ ]),
2401
+ ).rejects.toThrow();
2402
+
2403
+ // First item should not have been created either (validation is upfront)
2404
+ const all = await app.repo.findMany({
2405
+ where: { jobName: "App.myJob" },
2406
+ });
2407
+ expect(all).toHaveLength(0);
2408
+ });
2409
+
2410
+ it("should return empty array for empty input", async () => {
2411
+ class App {
2412
+ myJob = $job({
2413
+ schema: t.object({ v: t.text() }),
2414
+ handler: async () => {},
2415
+ });
2416
+ }
2417
+
2418
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2419
+ const app = alepha.inject(App);
2420
+ await alepha.start();
2421
+
2422
+ const ids = await app.myJob.pushMany([]);
2423
+ expect(ids).toEqual([]);
2424
+ });
2425
+ });
2426
+
2427
+ // ----- Graceful drain -----
2428
+
2429
+ describe("graceful drain", () => {
2430
+ it("should wait for in-flight jobs before aborting on stop", async () => {
2431
+ let completed = false;
2432
+
2433
+ class App {
2434
+ repo = $repository(jobExecutionEntity);
2435
+ myJob = $job({
2436
+ schema: t.object({ value: t.text() }),
2437
+ handler: async () => {
2438
+ await new Promise((r) => setTimeout(r, 100));
2439
+ completed = true;
2440
+ },
2441
+ });
2442
+ }
2443
+
2444
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2445
+ const app = alepha.inject(App);
2446
+ await alepha.start();
2447
+
2448
+ await app.myJob.push({ value: "test" });
2449
+
2450
+ await vi.waitFor(async () => {
2451
+ const running = await app.repo.findMany({
2452
+ where: { jobName: "App.myJob", status: "running" },
2453
+ });
2454
+ expect(running).toHaveLength(1);
2455
+ });
2456
+
2457
+ // Stop should drain (wait for the in-flight job)
2458
+ await alepha.stop();
2459
+
2460
+ expect(completed).toBe(true);
2461
+ });
2462
+
2463
+ it("should stop cleanly when no jobs are in-flight", async () => {
2464
+ class App {
2465
+ myJob = $job({
2466
+ schema: t.object({ value: t.text() }),
2467
+ handler: async () => {},
2468
+ });
2469
+ }
2470
+
2471
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2472
+ alepha.inject(App);
2473
+ await alepha.start();
2474
+ await alepha.stop();
2475
+ });
2476
+ });
2477
+
2478
+ // ----- JobService reporting -----
2479
+
2480
+ describe("JobService reporting", () => {
2481
+ it("should find executions with status filter", async () => {
2482
+ class App {
2483
+ repo = $repository(jobExecutionEntity);
2484
+ jobService = $inject(JobService);
2485
+ myJob = $job({
2486
+ schema: t.object({ value: t.text() }),
2487
+ handler: async () => {},
2488
+ });
2489
+ }
2490
+
2491
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2492
+ const app = alepha.inject(App);
2493
+ await alepha.start();
2494
+
2495
+ await app.myJob.push({ value: "test" });
2496
+
2497
+ await vi.waitFor(async () => {
2498
+ const completed = await app.repo.findMany({
2499
+ where: { jobName: "App.myJob", status: "completed" },
2500
+ });
2501
+ expect(completed).toHaveLength(1);
2502
+ });
2503
+
2504
+ // Push a delayed one (stays scheduled)
2505
+ await app.myJob.push({ value: "later" }, { delay: [1, "hour"] });
2506
+
2507
+ const completedPage = await app.jobService.findExecutions({
2508
+ status: "completed",
2509
+ });
2510
+ expect(completedPage.content.length).toBe(1);
2511
+ expect(completedPage.content[0].status).toBe("completed");
2512
+
2513
+ const scheduledPage = await app.jobService.findExecutions({
2514
+ status: "scheduled",
2515
+ });
2516
+ expect(scheduledPage.content.length).toBe(1);
2517
+ });
2518
+
2519
+ it("should find executions with priority filter", async () => {
2520
+ class App {
2521
+ repo = $repository(jobExecutionEntity);
2522
+ jobService = $inject(JobService);
2523
+ myJob = $job({
2524
+ schema: t.object({ value: t.text() }),
2525
+ handler: async () => {},
2526
+ });
2527
+ }
2528
+
2529
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2530
+ const app = alepha.inject(App);
2531
+ await alepha.start();
2532
+
2533
+ await app.myJob.push(
2534
+ { value: "hi" },
2535
+ { priority: "high", delay: [1, "hour"] },
2536
+ );
2537
+ await app.myJob.push(
2538
+ { value: "lo" },
2539
+ { priority: "low", delay: [1, "hour"] },
2540
+ );
2541
+
2542
+ const highPage = await app.jobService.findExecutions({
2543
+ priority: "high",
2544
+ });
2545
+ expect(highPage.content).toHaveLength(1);
2546
+ expect(highPage.content[0].priority).toBe(1);
2547
+ });
2548
+
2549
+ it("should return execution detail with logs", async () => {
2550
+ class App {
2551
+ log = $logger();
2552
+ repo = $repository(jobExecutionEntity);
2553
+ logRepo = $repository(jobExecutionLogEntity);
2554
+ jobService = $inject(JobService);
2555
+ myJob = $job({
2556
+ schema: t.object({ value: t.text() }),
2557
+ handler: async () => {
2558
+ this.log.info("hello from handler");
2559
+ },
2560
+ });
2561
+ }
2562
+
2563
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2564
+ const app = alepha.inject(App);
2565
+ await alepha.start();
2566
+
2567
+ await app.myJob.push({ value: "test" });
2568
+
2569
+ await vi.waitFor(async () => {
2570
+ const completed = await app.repo.findMany({
2571
+ where: { jobName: "App.myJob", status: "completed" },
2572
+ });
2573
+ expect(completed).toHaveLength(1);
2574
+ });
2575
+
2576
+ const completed = await app.repo.findMany({
2577
+ where: { jobName: "App.myJob", status: "completed" },
2578
+ });
2579
+ const detail = await app.jobService.getExecution(completed[0].id);
2580
+
2581
+ expect(detail.status).toBe("completed");
2582
+ expect(detail.can).toBeDefined();
2583
+ expect(detail.logs).toBeDefined();
2584
+ expect(detail.logs!.some((l) => l.message === "hello from handler")).toBe(
2585
+ true,
2586
+ );
2587
+ });
2588
+
2589
+ it("should return cron jobs with last execution info", async () => {
2590
+ class App {
2591
+ repo = $repository(jobExecutionEntity);
2592
+ jobService = $inject(JobService);
2593
+ cronJob = $job({
2594
+ cron: "0 0 * * *",
2595
+ handler: async () => {},
2596
+ });
2597
+ }
2598
+
2599
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2600
+ const app = alepha.inject(App);
2601
+ await alepha.start();
2602
+
2603
+ await app.cronJob.trigger();
2604
+
2605
+ await vi.waitFor(async () => {
2606
+ const completed = await app.repo.findMany({
2607
+ where: { jobName: "App.cronJob", status: "completed" },
2608
+ });
2609
+ expect(completed).toHaveLength(1);
2610
+ });
2611
+
2612
+ const cronJobs = await app.jobService.getCronJobs();
2613
+ expect(cronJobs).toHaveLength(1);
2614
+ expect(cronJobs[0].name).toBe("App.cronJob");
2615
+ expect(cronJobs[0].cron).toBe("0 0 * * *");
2616
+ expect(cronJobs[0].lastExecution).toBeDefined();
2617
+ expect(cronJobs[0].lastExecution!.status).toBe("completed");
2618
+ });
2619
+
2620
+ it("should return queue depth per job", async () => {
2621
+ class App {
2622
+ repo = $repository(jobExecutionEntity);
2623
+ jobService = $inject(JobService);
2624
+ myJob = $job({
2625
+ schema: t.object({ value: t.text() }),
2626
+ handler: async () => {},
2627
+ });
2628
+ }
2629
+
2630
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2631
+ const app = alepha.inject(App);
2632
+ await alepha.start();
2633
+
2634
+ // Create a scheduled execution (stays in queue)
2635
+ await app.myJob.push({ value: "test" }, { delay: [1, "hour"] });
2636
+
2637
+ const depths = await app.jobService.getQueueDepth();
2638
+ const depth = depths.find((d) => d.jobName === "App.myJob");
2639
+ expect(depth).toBeDefined();
2640
+ expect(depth!.scheduled).toBe(1);
2641
+ expect(depth!.pending).toBe(0);
2642
+ });
2643
+
2644
+ it("should return top failures", async () => {
2645
+ class App {
2646
+ repo = $repository(jobExecutionEntity);
2647
+ jobService = $inject(JobService);
2648
+ myJob = $job({
2649
+ schema: t.object({ value: t.text() }),
2650
+ handler: async () => {
2651
+ throw new Error("always fails");
2652
+ },
2653
+ });
2654
+ }
2655
+
2656
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2657
+ const app = alepha.inject(App);
2658
+ await alepha.start();
2659
+
2660
+ await app.myJob.push({ value: "test" });
2661
+
2662
+ await vi.waitFor(async () => {
2663
+ const dead = await app.repo.findMany({
2664
+ where: { jobName: "App.myJob", status: "dead" },
2665
+ });
2666
+ expect(dead).toHaveLength(1);
2667
+ });
2668
+
2669
+ const failures = await app.jobService.getTopFailures();
2670
+ expect(failures.length).toBeGreaterThanOrEqual(1);
2671
+ const entry = failures.find((f) => f.jobName === "App.myJob");
2672
+ expect(entry).toBeDefined();
2673
+ expect(entry!.failures).toBe(1);
2674
+ expect(entry!.lastError).toBe("always fails");
2675
+ });
2676
+
2677
+ it("should return activity over time", async () => {
2678
+ class App {
2679
+ repo = $repository(jobExecutionEntity);
2680
+ jobService = $inject(JobService);
2681
+ myJob = $job({
2682
+ schema: t.object({ value: t.text() }),
2683
+ handler: async () => {},
2684
+ });
2685
+ }
2686
+
2687
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2688
+ const app = alepha.inject(App);
2689
+ await alepha.start();
2690
+
2691
+ await app.myJob.push({ value: "test" });
2692
+
2693
+ await vi.waitFor(async () => {
2694
+ const completed = await app.repo.findMany({
2695
+ where: { jobName: "App.myJob", status: "completed" },
2696
+ });
2697
+ expect(completed).toHaveLength(1);
2698
+ });
2699
+
2700
+ const activity = await app.jobService.getActivity(7);
2701
+ expect(activity).toHaveLength(7);
2702
+
2703
+ // Today should have at least 1 completed
2704
+ const today = activity[activity.length - 1];
2705
+ expect(today.completed).toBeGreaterThanOrEqual(1);
2706
+ });
2707
+
2708
+ it("should return job registry with type classification", async () => {
2709
+ class App {
2710
+ jobService = $inject(JobService);
2711
+ pushOnly = $job({
2712
+ schema: t.object({ v: t.text() }),
2713
+ handler: async () => {},
2714
+ });
2715
+ cronOnly = $job({
2716
+ cron: "0 0 * * *",
2717
+ handler: async () => {},
2718
+ });
2719
+ both = $job({
2720
+ schema: t.object({ v: t.text() }),
2721
+ cron: "0 0 * * *",
2722
+ handler: async () => {},
2723
+ });
2724
+ }
2725
+
2726
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2727
+ const app = alepha.inject(App);
2728
+ await alepha.start();
2729
+
2730
+ const registry = app.jobService.getRegistry();
2731
+ expect(registry).toHaveLength(3);
2732
+
2733
+ const push = registry.find((r) => r.name === "App.pushOnly");
2734
+ const cron = registry.find((r) => r.name === "App.cronOnly");
2735
+ const dual = registry.find((r) => r.name === "App.both");
2736
+
2737
+ expect(push!.type).toBe("push");
2738
+ expect(cron!.type).toBe("cron");
2739
+ expect(dual!.type).toBe("both");
2740
+ });
2741
+ });
2742
+
2743
+ // ----- Log purge -----
2744
+
2745
+ describe("log purge", () => {
2746
+ it("should delete old completed executions and their logs", async () => {
2747
+ class App {
2748
+ log = $logger();
2749
+ repo = $repository(jobExecutionEntity);
2750
+ logRepo = $repository(jobExecutionLogEntity);
2751
+ myJob = $job({
2752
+ schema: t.object({ value: t.text() }),
2753
+ handler: async () => {
2754
+ this.log.info("some log");
2755
+ },
2756
+ });
2757
+ }
2758
+
2759
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2760
+ const app = alepha.inject(App);
2761
+ await alepha.start();
2762
+
2763
+ await app.myJob.push({ value: "test" });
2764
+
2765
+ await vi.waitFor(async () => {
2766
+ const completed = await app.repo.findMany({
2767
+ where: { jobName: "App.myJob", status: "completed" },
2768
+ });
2769
+ expect(completed).toHaveLength(1);
2770
+ });
2771
+
2772
+ // Backdate completedAt to 60 days ago (retention is 30 days)
2773
+ const completed = await app.repo.findMany({
2774
+ where: { jobName: "App.myJob", status: "completed" },
2775
+ });
2776
+ const sixtyDaysAgo = new Date(Date.now() - 60 * 86_400_000).toISOString();
2777
+ await app.repo.updateById(completed[0].id, {
2778
+ completedAt: sixtyDaysAgo,
2779
+ });
2780
+
2781
+ // Verify log exists before purge
2782
+ const logBefore = await app.logRepo.findById(completed[0].id);
2783
+ expect(logBefore).toBeDefined();
2784
+
2785
+ // Run purge
2786
+ const provider = alepha.inject(JobProvider) as any;
2787
+ await provider.logPurge();
2788
+
2789
+ // Both execution and log should be deleted
2790
+ const execAfter = await app.repo.findById(completed[0].id);
2791
+ expect(execAfter).toBeUndefined();
2792
+
2793
+ const logAfter = await app.logRepo.findById(completed[0].id);
2794
+ expect(logAfter).toBeUndefined();
2795
+ });
2796
+
2797
+ it("should preserve recent executions", async () => {
2798
+ class App {
2799
+ repo = $repository(jobExecutionEntity);
2800
+ myJob = $job({
2801
+ schema: t.object({ value: t.text() }),
2802
+ handler: async () => {},
2803
+ });
2804
+ }
2805
+
2806
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2807
+ const app = alepha.inject(App);
2808
+ await alepha.start();
2809
+
2810
+ await app.myJob.push({ value: "test" });
2811
+
2812
+ await vi.waitFor(async () => {
2813
+ const completed = await app.repo.findMany({
2814
+ where: { jobName: "App.myJob", status: "completed" },
2815
+ });
2816
+ expect(completed).toHaveLength(1);
2817
+ });
2818
+
2819
+ // Run purge — recent execution should survive
2820
+ const provider = alepha.inject(JobProvider) as any;
2821
+ await provider.logPurge();
2822
+
2823
+ const all = await app.repo.findMany({
2824
+ where: { jobName: "App.myJob" },
2825
+ });
2826
+ expect(all).toHaveLength(1);
2827
+ });
2828
+ });
2829
+
2830
+ // ----- Locked sweeps -----
2831
+
2832
+ describe("locked sweeps", () => {
2833
+ it("should skip sweep when lock is held by another worker", async () => {
2834
+ class App {
2835
+ repo = $repository(jobExecutionEntity);
2836
+ myJob = $job({
2837
+ schema: t.object({ value: t.text() }),
2838
+ handler: async () => {},
2839
+ });
2840
+ }
2841
+
2842
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2843
+ const app = alepha.inject(App);
2844
+ await alepha.start();
2845
+
2846
+ // Insert a stale pending record
2847
+ const staleTime = new Date(Date.now() - 600_000).toISOString();
2848
+ await app.repo.create({
2849
+ jobName: "App.myJob",
2850
+ payload: { value: "stale" },
2851
+ status: "pending",
2852
+ priority: 2,
2853
+ maxAttempts: 1,
2854
+ createdAt: staleTime,
2855
+ updatedAt: staleTime,
2856
+ });
2857
+
2858
+ // Simulate another worker holding the lock
2859
+ const provider = alepha.inject(JobProvider) as any;
2860
+ await provider.lockProvider.set(
2861
+ "_alepha:jobs:recovery-lock",
2862
+ "other-worker,2026-01-01T00:00:00.000Z",
2863
+ false,
2864
+ 300_000,
2865
+ );
2866
+
2867
+ // Recovery sweep should skip (lock held)
2868
+ await provider.recoverySweep();
2869
+
2870
+ // Job should still be pending (not dispatched)
2871
+ const pending = await app.repo.findMany({
2872
+ where: { jobName: "App.myJob", status: "pending" },
2873
+ });
2874
+ expect(pending).toHaveLength(1);
2875
+ });
2876
+
2877
+ it("should release lock after sweep completes", async () => {
2878
+ class App {
2879
+ repo = $repository(jobExecutionEntity);
2880
+ myJob = $job({
2881
+ schema: t.object({ value: t.text() }),
2882
+ handler: async () => {},
2883
+ });
2884
+ }
2885
+
2886
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
2887
+ const app = alepha.inject(App);
2888
+ await alepha.start();
2889
+
2890
+ const provider = alepha.inject(JobProvider) as any;
2891
+
2892
+ // Run sweep (should acquire and release lock)
2893
+ await provider.recoverySweep();
2894
+
2895
+ // Second sweep should also succeed (lock was released)
2896
+ await provider.recoverySweep();
2897
+ });
2898
+ });
2023
2899
  });