alepha 0.15.4 → 0.15.5

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 (282) hide show
  1. package/README.md +26 -11
  2. package/dist/api/audits/index.d.ts +3 -3
  3. package/dist/api/audits/index.js +3 -3
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +3 -3
  6. package/dist/api/files/index.js +3 -3
  7. package/dist/api/files/index.js.map +1 -1
  8. package/dist/api/jobs/index.d.ts +47 -4
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js +100 -5
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/keys/index.d.ts +3 -3
  13. package/dist/api/keys/index.js +3 -3
  14. package/dist/api/keys/index.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.js +3 -3
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +263 -263
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/parameters/index.js +31 -30
  21. package/dist/api/parameters/index.js.map +1 -1
  22. package/dist/api/users/index.d.ts +373 -67
  23. package/dist/api/users/index.d.ts.map +1 -1
  24. package/dist/api/users/index.js +273 -72
  25. package/dist/api/users/index.js.map +1 -1
  26. package/dist/api/verifications/index.d.ts +3 -3
  27. package/dist/api/verifications/index.js +3 -3
  28. package/dist/api/verifications/index.js.map +1 -1
  29. package/dist/batch/index.d.ts +7 -7
  30. package/dist/batch/index.js +3 -3
  31. package/dist/batch/index.js.map +1 -1
  32. package/dist/bucket/index.d.ts +3 -3
  33. package/dist/bucket/index.js +6 -6
  34. package/dist/bucket/index.js.map +1 -1
  35. package/dist/cache/core/index.d.ts +3 -3
  36. package/dist/cache/core/index.js +3 -3
  37. package/dist/cache/core/index.js.map +1 -1
  38. package/dist/cli/index.d.ts +5607 -20
  39. package/dist/cli/index.d.ts.map +1 -1
  40. package/dist/cli/index.js +103 -89
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/command/index.d.ts +11 -4
  43. package/dist/command/index.d.ts.map +1 -1
  44. package/dist/command/index.js +8 -6
  45. package/dist/command/index.js.map +1 -1
  46. package/dist/core/index.browser.js.map +1 -1
  47. package/dist/core/index.d.ts +4 -8
  48. package/dist/core/index.d.ts.map +1 -1
  49. package/dist/core/index.js +3 -3
  50. package/dist/core/index.js.map +1 -1
  51. package/dist/core/index.native.js.map +1 -1
  52. package/dist/datetime/index.d.ts +3 -3
  53. package/dist/datetime/index.js +3 -3
  54. package/dist/datetime/index.js.map +1 -1
  55. package/dist/email/index.d.ts +3 -3
  56. package/dist/email/index.js +8 -8
  57. package/dist/email/index.js.map +1 -1
  58. package/dist/fake/index.d.ts +3 -3
  59. package/dist/fake/index.js +3 -3
  60. package/dist/fake/index.js.map +1 -1
  61. package/dist/lock/core/index.d.ts +3 -3
  62. package/dist/lock/core/index.js +3 -3
  63. package/dist/lock/core/index.js.map +1 -1
  64. package/dist/logger/index.d.ts +3 -3
  65. package/dist/logger/index.js +6 -3
  66. package/dist/logger/index.js.map +1 -1
  67. package/dist/mcp/index.d.ts +3 -3
  68. package/dist/mcp/index.js +3 -3
  69. package/dist/mcp/index.js.map +1 -1
  70. package/dist/orm/index.d.ts +12 -12
  71. package/dist/orm/index.js +4 -4
  72. package/dist/orm/index.js.map +1 -1
  73. package/dist/queue/core/index.d.ts +3 -3
  74. package/dist/queue/core/index.js +3 -3
  75. package/dist/queue/core/index.js.map +1 -1
  76. package/dist/react/auth/index.d.ts +3 -3
  77. package/dist/react/auth/index.js +3 -3
  78. package/dist/react/auth/index.js.map +1 -1
  79. package/dist/react/core/index.d.ts +3 -3
  80. package/dist/react/core/index.js +3 -3
  81. package/dist/react/core/index.js.map +1 -1
  82. package/dist/react/form/index.d.ts +3 -3
  83. package/dist/react/form/index.js +3 -3
  84. package/dist/react/form/index.js.map +1 -1
  85. package/dist/react/head/index.d.ts +3 -3
  86. package/dist/react/head/index.js +3 -3
  87. package/dist/react/head/index.js.map +1 -1
  88. package/dist/react/i18n/index.d.ts +3 -3
  89. package/dist/react/i18n/index.js +3 -3
  90. package/dist/react/i18n/index.js.map +1 -1
  91. package/dist/react/intro/index.css +337 -0
  92. package/dist/react/intro/index.css.map +1 -0
  93. package/dist/react/intro/index.d.ts +10 -0
  94. package/dist/react/intro/index.d.ts.map +1 -0
  95. package/dist/react/intro/index.js +222 -0
  96. package/dist/react/intro/index.js.map +1 -0
  97. package/dist/react/router/index.browser.js +2 -2
  98. package/dist/react/router/index.browser.js.map +1 -1
  99. package/dist/react/router/index.d.ts +1 -1
  100. package/dist/react/router/index.d.ts.map +1 -1
  101. package/dist/react/router/index.js +5 -5
  102. package/dist/react/router/index.js.map +1 -1
  103. package/dist/redis/index.d.ts +17 -17
  104. package/dist/redis/index.js +3 -3
  105. package/dist/redis/index.js.map +1 -1
  106. package/dist/retry/index.d.ts +3 -3
  107. package/dist/retry/index.js +3 -3
  108. package/dist/retry/index.js.map +1 -1
  109. package/dist/scheduler/index.d.ts +3 -3
  110. package/dist/scheduler/index.js +3 -3
  111. package/dist/scheduler/index.js.map +1 -1
  112. package/dist/security/index.d.ts +3 -3
  113. package/dist/security/index.js +5 -5
  114. package/dist/security/index.js.map +1 -1
  115. package/dist/server/auth/index.d.ts +3 -3
  116. package/dist/server/auth/index.js +3 -3
  117. package/dist/server/auth/index.js.map +1 -1
  118. package/dist/server/cache/index.d.ts +3 -3
  119. package/dist/server/cache/index.js +3 -3
  120. package/dist/server/cache/index.js.map +1 -1
  121. package/dist/server/compress/index.d.ts +3 -3
  122. package/dist/server/compress/index.js +3 -3
  123. package/dist/server/compress/index.js.map +1 -1
  124. package/dist/server/cookies/index.d.ts +3 -3
  125. package/dist/server/cookies/index.js +3 -3
  126. package/dist/server/cookies/index.js.map +1 -1
  127. package/dist/server/core/index.d.ts +5 -16
  128. package/dist/server/core/index.d.ts.map +1 -1
  129. package/dist/server/core/index.js +13 -29
  130. package/dist/server/core/index.js.map +1 -1
  131. package/dist/server/cors/index.d.ts +3 -3
  132. package/dist/server/cors/index.js +3 -3
  133. package/dist/server/cors/index.js.map +1 -1
  134. package/dist/server/health/index.d.ts +20 -20
  135. package/dist/server/health/index.js +3 -3
  136. package/dist/server/health/index.js.map +1 -1
  137. package/dist/server/helmet/index.d.ts +3 -3
  138. package/dist/server/helmet/index.js +3 -3
  139. package/dist/server/helmet/index.js.map +1 -1
  140. package/dist/server/links/index.d.ts +42 -42
  141. package/dist/server/links/index.d.ts.map +1 -1
  142. package/dist/server/links/index.js +3 -3
  143. package/dist/server/links/index.js.map +1 -1
  144. package/dist/server/metrics/index.d.ts +3 -3
  145. package/dist/server/metrics/index.js +3 -3
  146. package/dist/server/metrics/index.js.map +1 -1
  147. package/dist/server/multipart/index.d.ts +3 -3
  148. package/dist/server/multipart/index.js +3 -3
  149. package/dist/server/multipart/index.js.map +1 -1
  150. package/dist/server/proxy/index.d.ts +3 -3
  151. package/dist/server/proxy/index.js +3 -3
  152. package/dist/server/proxy/index.js.map +1 -1
  153. package/dist/server/rate-limit/index.d.ts +3 -3
  154. package/dist/server/rate-limit/index.js +3 -3
  155. package/dist/server/rate-limit/index.js.map +1 -1
  156. package/dist/server/static/index.d.ts +3 -3
  157. package/dist/server/static/index.js +6 -6
  158. package/dist/server/static/index.js.map +1 -1
  159. package/dist/server/swagger/index.d.ts +3 -3
  160. package/dist/server/swagger/index.js +6 -6
  161. package/dist/server/swagger/index.js.map +1 -1
  162. package/dist/sms/index.d.ts +3 -3
  163. package/dist/sms/index.js +6 -6
  164. package/dist/sms/index.js.map +1 -1
  165. package/dist/system/index.d.ts +3 -3
  166. package/dist/system/index.js +3 -3
  167. package/dist/system/index.js.map +1 -1
  168. package/dist/thread/index.d.ts +3 -3
  169. package/dist/thread/index.js +3 -3
  170. package/dist/thread/index.js.map +1 -1
  171. package/dist/topic/core/index.d.ts +3 -3
  172. package/dist/topic/core/index.js +3 -3
  173. package/dist/topic/core/index.js.map +1 -1
  174. package/dist/vite/index.d.ts +6284 -3
  175. package/dist/vite/index.d.ts.map +1 -1
  176. package/dist/websocket/index.d.ts +3 -3
  177. package/dist/websocket/index.js +3 -3
  178. package/dist/websocket/index.js.map +1 -1
  179. package/package.json +7 -2
  180. package/src/api/audits/index.ts +3 -3
  181. package/src/api/files/index.ts +3 -3
  182. package/src/api/jobs/controllers/AdminJobController.ts +15 -2
  183. package/src/api/jobs/index.ts +4 -3
  184. package/src/api/jobs/services/JobAudits.spec.ts +89 -0
  185. package/src/api/jobs/services/JobAudits.ts +101 -0
  186. package/src/api/keys/index.ts +3 -3
  187. package/src/api/notifications/index.ts +3 -3
  188. package/src/api/parameters/index.ts +5 -3
  189. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1 -1
  190. package/src/api/users/__tests__/ApiKeys.spec.ts +1 -1
  191. package/src/api/users/__tests__/EmailVerification.spec.ts +16 -1
  192. package/src/api/users/__tests__/PasswordReset.spec.ts +11 -0
  193. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -0
  194. package/src/api/users/index.ts +8 -9
  195. package/src/api/users/primitives/$realm.ts +117 -19
  196. package/src/api/users/providers/RealmProvider.ts +15 -7
  197. package/src/api/users/services/CredentialService.spec.ts +11 -0
  198. package/src/api/users/services/CredentialService.ts +47 -24
  199. package/src/api/users/services/IdentityService.ts +12 -4
  200. package/src/api/users/services/RegistrationService.spec.ts +11 -0
  201. package/src/api/users/services/RegistrationService.ts +33 -12
  202. package/src/api/users/services/SessionService.ts +83 -12
  203. package/src/api/users/services/UserAudits.ts +47 -0
  204. package/src/api/users/services/UserFiles.ts +19 -0
  205. package/src/api/users/services/UserJobs.spec.ts +107 -0
  206. package/src/api/users/services/UserJobs.ts +62 -0
  207. package/src/api/users/services/UserParameters.ts +23 -0
  208. package/src/api/users/services/UserService.ts +34 -17
  209. package/src/api/verifications/index.ts +3 -3
  210. package/src/batch/index.ts +3 -3
  211. package/src/bucket/index.ts +3 -3
  212. package/src/cache/core/index.ts +3 -3
  213. package/src/cli/commands/db.ts +9 -0
  214. package/src/cli/commands/init.spec.ts +2 -17
  215. package/src/cli/commands/init.ts +37 -1
  216. package/src/cli/providers/ViteDevServerProvider.ts +5 -2
  217. package/src/cli/services/AlephaCliUtils.ts +17 -0
  218. package/src/cli/services/PackageManagerUtils.ts +15 -1
  219. package/src/cli/services/ProjectScaffolder.ts +8 -13
  220. package/src/cli/templates/agentMd.ts +2 -25
  221. package/src/cli/templates/apiAppSecurityTs.ts +37 -2
  222. package/src/cli/templates/mainCss.ts +2 -32
  223. package/src/cli/templates/webAppRouterTs.ts +5 -5
  224. package/src/cli/templates/webHomeComponentTsx.ts +10 -0
  225. package/src/command/helpers/Runner.ts +14 -1
  226. package/src/command/index.ts +3 -3
  227. package/src/core/helpers/primitive.ts +0 -5
  228. package/src/core/index.ts +3 -3
  229. package/src/datetime/index.ts +3 -3
  230. package/src/email/index.ts +3 -3
  231. package/src/email/providers/LocalEmailProvider.ts +2 -2
  232. package/src/fake/index.ts +3 -3
  233. package/src/lock/core/index.ts +3 -3
  234. package/src/logger/index.ts +3 -3
  235. package/src/logger/providers/PrettyFormatterProvider.ts +7 -0
  236. package/src/mcp/index.ts +3 -3
  237. package/src/orm/index.ts +3 -3
  238. package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
  239. package/src/queue/core/index.ts +3 -3
  240. package/src/react/auth/index.ts +3 -3
  241. package/src/react/core/index.ts +3 -3
  242. package/src/react/form/index.ts +3 -3
  243. package/src/react/head/index.ts +3 -3
  244. package/src/react/i18n/index.ts +3 -3
  245. package/src/react/intro/components/GettingStarted.css +334 -0
  246. package/src/react/intro/components/GettingStarted.tsx +276 -0
  247. package/src/react/intro/index.ts +1 -0
  248. package/src/react/router/index.browser.ts +2 -0
  249. package/src/react/router/index.ts +2 -0
  250. package/src/redis/index.ts +3 -3
  251. package/src/retry/index.ts +3 -3
  252. package/src/router/index.ts +3 -3
  253. package/src/scheduler/index.ts +3 -3
  254. package/src/security/index.ts +3 -3
  255. package/src/security/providers/JwtProvider.ts +2 -2
  256. package/src/server/auth/index.ts +3 -3
  257. package/src/server/cache/index.ts +3 -3
  258. package/src/server/compress/index.ts +3 -3
  259. package/src/server/cookies/index.ts +3 -3
  260. package/src/server/core/index.ts +3 -3
  261. package/src/server/core/primitives/$action.spec.ts +3 -2
  262. package/src/server/core/primitives/$action.ts +6 -2
  263. package/src/server/core/providers/NodeHttpServerProvider.ts +2 -15
  264. package/src/server/core/providers/ServerProvider.ts +4 -2
  265. package/src/server/core/providers/ServerRouterProvider.ts +5 -27
  266. package/src/server/cors/index.ts +3 -3
  267. package/src/server/health/index.ts +3 -3
  268. package/src/server/helmet/index.ts +3 -3
  269. package/src/server/links/index.ts +3 -3
  270. package/src/server/metrics/index.ts +3 -3
  271. package/src/server/multipart/index.ts +3 -3
  272. package/src/server/proxy/index.ts +3 -3
  273. package/src/server/rate-limit/index.ts +3 -3
  274. package/src/server/static/index.ts +3 -3
  275. package/src/server/swagger/index.ts +3 -3
  276. package/src/sms/index.ts +3 -3
  277. package/src/system/index.ts +3 -3
  278. package/src/thread/index.ts +3 -3
  279. package/src/topic/core/index.ts +3 -3
  280. package/src/websocket/index.ts +3 -3
  281. package/src/cli/templates/webHelloComponentTsx.ts +0 -30
  282. /package/src/api/users/{notifications → services}/UserNotifications.ts +0 -0
@@ -234,9 +234,9 @@ declare class VerificationJobs {
234
234
  //#endregion
235
235
  //#region ../../src/api/verifications/index.d.ts
236
236
  /**
237
- * | type | quality | stability |
238
- * |------|---------|-----------|
239
- * | backend | standard | stable |
237
+ * | Stability | Since | Runtime |
238
+ * |-----------|-------|---------|
239
+ * | 3 - stable | 0.13.0 | node, bun, workerd|
240
240
  *
241
241
  * Email and phone verification workflows.
242
242
  *
@@ -382,9 +382,9 @@ var VerificationJobs = class {
382
382
  //#endregion
383
383
  //#region ../../src/api/verifications/index.ts
384
384
  /**
385
- * | type | quality | stability |
386
- * |------|---------|-----------|
387
- * | backend | standard | stable |
385
+ * | Stability | Since | Runtime |
386
+ * |-----------|-------|---------|
387
+ * | 3 - stable | 0.13.0 | node, bun, workerd|
388
388
  *
389
389
  * Email and phone verification workflows.
390
390
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/api/verifications/schemas/requestVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/validateVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/verificationTypeEnumSchema.ts","../../../src/api/verifications/entities/verifications.ts","../../../src/api/verifications/schemas/verificationSettingsSchema.ts","../../../src/api/verifications/parameters/VerificationParameters.ts","../../../src/api/verifications/services/VerificationService.ts","../../../src/api/verifications/controllers/VerificationController.ts","../../../src/api/verifications/jobs/VerificationJobs.ts","../../../src/api/verifications/index.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const requestVerificationCodeResponseSchema = t.object({\n token: t.string({\n description:\n \"The verification token (6-digit code for phone, UUID for email). The caller should send this to the user via their preferred notification method.\",\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before your verification token expires.\",\n }),\n verificationCooldown: t.integer({\n description:\n \"Cooldown period in seconds before you can request another verification.\",\n }),\n maxVerificationAttempts: t.integer({\n description:\n \"Maximum number of verification attempts allowed before the token is locked.\",\n }),\n});\n\nexport type RequestVerificationResponse = Static<\n typeof requestVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const validateVerificationCodeResponseSchema = t.object({\n ok: t.boolean({\n description: \"Indicates whether the verification was successful.\",\n }),\n alreadyVerified: t.optional(\n t.boolean({\n description: \"Indicates whether the target was already verified.\",\n }),\n ),\n});\n\nexport type ValidateVerificationCodeResponse = Static<\n typeof validateVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationTypeEnumSchema = t.enum([\"code\", \"link\"]);\nexport type VerificationTypeEnum = Static<typeof verificationTypeEnumSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport const verifications = $entity({\n name: \"verification\",\n schema: t.object({\n id: db.primaryKey(t.bigint()),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n version: db.version(),\n\n type: verificationTypeEnumSchema,\n\n target: t.text({\n description: \"Can be a phone (E.164 format) or email address\",\n }),\n\n code: t.text({\n description: \"Hashed verification token (n-digit code or UUID)\",\n }),\n\n verifiedAt: t.optional(\n t.datetime({\n description: \"When it was successfully verified\",\n }),\n ),\n\n attempts: db.default(\n t.integer({\n description: \"Number of failed attempts (to prevent brute-force)\",\n }),\n 0,\n ),\n }),\n indexes: [\n \"createdAt\",\n {\n columns: [\"target\", \"code\"],\n },\n ],\n});\n\nexport const verificationEntitySchema = verifications.schema;\nexport const verificationEntityInsertSchema = verifications.insertSchema;\nexport type VerificationEntity = Static<typeof verifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationSettingsSchema = t.object({\n code: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeLength: t.integer({\n description: \"Length of the verification code.\",\n minimum: 4,\n maximum: 12,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification code expires.\",\n minimum: 60, // 1 minute\n maximum: 3600, // 1 hour\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to code verifications.\",\n },\n ),\n link: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification token expires.\",\n minimum: 60, // 1 minute\n maximum: 7200, // 2 hours\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to link verifications.\",\n },\n ),\n purgeDays: t.integer({\n description:\n \"Number of days after which expired verifications are automatically deleted. Set to 0 to disable auto-deletion.\",\n minimum: 0,\n maximum: 365,\n }),\n});\n\nexport type VerificationSettings = Static<typeof verificationSettingsSchema>;\n","import { $atom, $use, type Static } from \"alepha\";\nimport {\n type VerificationSettings,\n verificationSettingsSchema,\n} from \"../schemas/verificationSettingsSchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Verification settings configuration atom\n */\nexport const verificationOptions = $atom({\n name: \"alepha.api.verifications.options\",\n schema: verificationSettingsSchema,\n default: {\n code: {\n maxAttempts: 5,\n codeLength: 6,\n codeExpiration: 300, // 5 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n link: {\n maxAttempts: 3, // Lower since UUIDs are harder to guess\n codeExpiration: 1800, // 30 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n purgeDays: 1,\n },\n});\n\nexport type VerificationOptions = Static<typeof verificationOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [verificationOptions.key]: VerificationOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class VerificationParameters {\n protected readonly options = $use(verificationOptions);\n\n public get<K extends keyof VerificationSettings>(\n key: K,\n ): VerificationSettings[K] {\n return this.options[key];\n }\n}\n","import { createHash, randomInt, randomUUID } from \"node:crypto\";\nimport { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { BadRequestError, NotFoundError } from \"alepha/server\";\nimport {\n type VerificationEntity,\n verifications,\n} from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\nimport type { RequestVerificationResponse } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport type { ValidateVerificationCodeResponse } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport type { VerificationTypeEnum } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport class VerificationService {\n protected readonly log = $logger();\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly verificationRepository = $repository(verifications);\n\n public async findByEntry(\n entry: VerificationEntry,\n ): Promise<VerificationEntity> {\n this.log.trace(\"Finding verification by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n const results = await this.verificationRepository.findMany({\n limit: 1, // only need the most recent entry\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n },\n });\n\n if (results.length === 0) {\n this.log.debug(\"Verification entry not found\", {\n type: entry.type,\n target: entry.target,\n });\n throw new NotFoundError(\"Verification entry not found\");\n }\n\n this.log.debug(\"Verification entry found\", {\n id: results[0].id,\n type: entry.type,\n target: entry.target,\n });\n\n return results[0];\n }\n\n public findRecentsByEntry(entry: VerificationEntry) {\n this.log.trace(\"Finding recent verifications by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n return this.verificationRepository.findMany({\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n createdAt: {\n gte: this.dateTimeProvider.now().startOf(\"day\").toISOString(),\n },\n },\n });\n }\n\n /**\n * Creates a verification entry and returns the token.\n * The caller is responsible for sending notifications with the token.\n * This allows for context-specific notifications (e.g., password reset vs email verification).\n */\n public async createVerification(\n entry: VerificationEntry,\n ): Promise<RequestVerificationResponse> {\n this.log.trace(\"Creating verification\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const recents = await this.findRecentsByEntry(entry);\n if (recents.length >= settings.limitPerDay) {\n this.log.warn(\"Daily verification limit reached\", {\n type: entry.type,\n target: entry.target,\n limit: settings.limitPerDay,\n count: recents.length,\n });\n throw new BadRequestError(\n `Maximum number of verification requests per day reached (${settings.limitPerDay})`,\n );\n }\n\n const existingVerification = recents[0];\n if (existingVerification) {\n const nowSec = this.dateTimeProvider.now().unix();\n const createdAtSec = this.dateTimeProvider\n .of(existingVerification.createdAt)\n .unix();\n\n const diffSec = nowSec - createdAtSec;\n if (diffSec < settings.verificationCooldown) {\n const remainingCooldown = Math.floor(\n settings.verificationCooldown - diffSec,\n );\n this.log.debug(\"Verification on cooldown\", {\n type: entry.type,\n target: entry.target,\n remainingSeconds: remainingCooldown,\n });\n throw new BadRequestError(\n `Verification is on cooldown for ${remainingCooldown} seconds`,\n );\n }\n }\n\n const token = this.generateToken(entry.type);\n\n const verification = await this.verificationRepository.create({\n type: entry.type,\n target: entry.target,\n code: this.hashCode(token),\n });\n\n this.log.info(\"Verification created\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n expiresInSeconds: settings.codeExpiration,\n });\n\n return {\n token,\n codeExpiration: settings.codeExpiration,\n verificationCooldown: settings.verificationCooldown,\n maxVerificationAttempts: settings.maxAttempts,\n };\n }\n\n public async verifyCode(\n entry: VerificationEntry,\n code: string,\n ): Promise<ValidateVerificationCodeResponse> {\n this.log.trace(\"Verifying code\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const verification = await this.findByEntry(entry);\n if (verification.verifiedAt) {\n this.log.debug(\"Verification already verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n verifiedAt: verification.verifiedAt,\n });\n return { ok: true, alreadyVerified: true };\n }\n\n // DO NOT DELETE THE VERIFICATION WHEN IT IS REJECTED,\n // or we won't be able to cooldown the verification\n\n const now = this.dateTimeProvider.now();\n const expirationDate = this.dateTimeProvider\n .of(verification.createdAt)\n .add(settings.codeExpiration, \"seconds\");\n\n if (now > expirationDate) {\n this.log.warn(\"Verification code expired\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n createdAt: verification.createdAt,\n expiredAt: expirationDate.toISOString(),\n });\n throw new BadRequestError(\"Verification code has expired\");\n }\n\n if (verification.attempts >= settings.maxAttempts) {\n this.log.warn(\"Verification locked due to max attempts\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: verification.attempts,\n maxAttempts: settings.maxAttempts,\n });\n throw new BadRequestError(\n \"Maximum number of attempts reached - verification is locked\",\n );\n }\n\n if (verification.code !== this.hashCode(code)) {\n const newAttempts = verification.attempts + 1;\n this.log.warn(\"Invalid verification code\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: newAttempts,\n maxAttempts: settings.maxAttempts,\n });\n await this.verificationRepository.updateById(verification.id, {\n attempts: newAttempts,\n });\n throw new BadRequestError(\"Invalid verification code\");\n }\n\n await this.verificationRepository.updateById(verification.id, {\n verifiedAt: this.dateTimeProvider.nowISOString(),\n });\n\n this.log.info(\"Verification code verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n });\n\n return { ok: true };\n }\n\n public hashCode(code: string): string {\n return createHash(\"sha256\").update(code).digest(\"hex\");\n }\n\n public generateToken(type: VerificationTypeEnum): string {\n if (type === \"code\") {\n const settings = this.verificationParameters.get(\"code\");\n return randomInt(0, 1_000_000)\n .toString()\n .padStart(settings.codeLength, \"0\");\n } else if (type === \"link\") {\n return randomUUID();\n }\n\n throw new BadRequestError(`Invalid verification type: ${type}`);\n }\n}\n\nexport interface VerificationEntry {\n type: VerificationTypeEnum;\n target: string;\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { requestVerificationCodeResponseSchema } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport { validateVerificationCodeResponseSchema } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\nimport { VerificationService } from \"../services/VerificationService.ts\";\n\nexport class VerificationController {\n protected readonly verificationService = $inject(VerificationService);\n\n public readonly url = \"/verifications\";\n public readonly group = \"verifications\";\n\n public readonly requestVerificationCode = $action({\n path: `${this.url}/:type`,\n group: this.group,\n method: \"POST\",\n secure: false,\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n }),\n response: requestVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return await this.verificationService.createVerification({\n type: params.type,\n target: body.target,\n });\n },\n });\n\n public readonly validateVerificationCode = $action({\n path: `${this.url}/:type/validate`,\n group: this.group,\n method: \"POST\",\n secure: false,\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n token: t.text({\n description:\n \"The verification token (6-digit code for phone, UUID for email).\",\n }),\n }),\n response: validateVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return this.verificationService.verifyCode(\n {\n type: params.type,\n target: body.target,\n },\n body.token,\n );\n },\n });\n}\n","import { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $repository } from \"alepha/orm\";\nimport { $scheduler } from \"alepha/scheduler\";\nimport { verifications } from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\n\nexport class VerificationJobs {\n protected readonly verificationRepository = $repository(verifications);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n\n public readonly cleanExpired = $scheduler({\n cron: \"0 0 * * *\", // Every day at midnight\n description: \"Clean expired verifications\",\n handler: async () => {\n const purgeDays = this.verificationParameters.get(\"purgeDays\");\n if (purgeDays <= 0) {\n return; // Auto deletion is disabled\n }\n\n const dayMs = 24 * 60 * 60 * 1000;\n const purgeThreshold = Date.now() - purgeDays * dayMs;\n\n await this.verificationRepository.deleteMany({\n createdAt: {\n lt: this.dateTimeProvider.of(purgeThreshold).toISOString(),\n },\n });\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { VerificationController } from \"./controllers/VerificationController.ts\";\nimport { VerificationJobs } from \"./jobs/VerificationJobs.ts\";\nimport { VerificationParameters } from \"./parameters/VerificationParameters.ts\";\nimport { VerificationService } from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/VerificationController.ts\";\nexport * from \"./entities/verifications.ts\";\nexport * from \"./jobs/VerificationJobs.ts\";\nexport * from \"./parameters/VerificationParameters.ts\";\nexport * from \"./schemas/requestVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/validateVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/verificationTypeEnumSchema.ts\";\nexport * from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * | type | quality | stability |\n * |------|---------|-----------|\n * | backend | standard | stable |\n *\n * Email and phone verification workflows.\n *\n * **Features:**\n * - Verification token generation\n * - Verification code sending\n * - Verification completion tracking\n * - Resend functionality\n *\n * @module alepha.api.verifications\n */\nexport const AlephaApiVerification = $module({\n name: \"alepha.api.verifications\",\n services: [\n VerificationController,\n VerificationJobs,\n VerificationService,\n VerificationParameters,\n ],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/api/verifications/schemas/requestVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/validateVerificationCodeResponseSchema.ts","../../../src/api/verifications/schemas/verificationTypeEnumSchema.ts","../../../src/api/verifications/entities/verifications.ts","../../../src/api/verifications/schemas/verificationSettingsSchema.ts","../../../src/api/verifications/parameters/VerificationParameters.ts","../../../src/api/verifications/services/VerificationService.ts","../../../src/api/verifications/controllers/VerificationController.ts","../../../src/api/verifications/jobs/VerificationJobs.ts","../../../src/api/verifications/index.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const requestVerificationCodeResponseSchema = t.object({\n token: t.string({\n description:\n \"The verification token (6-digit code for phone, UUID for email). The caller should send this to the user via their preferred notification method.\",\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before your verification token expires.\",\n }),\n verificationCooldown: t.integer({\n description:\n \"Cooldown period in seconds before you can request another verification.\",\n }),\n maxVerificationAttempts: t.integer({\n description:\n \"Maximum number of verification attempts allowed before the token is locked.\",\n }),\n});\n\nexport type RequestVerificationResponse = Static<\n typeof requestVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const validateVerificationCodeResponseSchema = t.object({\n ok: t.boolean({\n description: \"Indicates whether the verification was successful.\",\n }),\n alreadyVerified: t.optional(\n t.boolean({\n description: \"Indicates whether the target was already verified.\",\n }),\n ),\n});\n\nexport type ValidateVerificationCodeResponse = Static<\n typeof validateVerificationCodeResponseSchema\n>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationTypeEnumSchema = t.enum([\"code\", \"link\"]);\nexport type VerificationTypeEnum = Static<typeof verificationTypeEnumSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport const verifications = $entity({\n name: \"verification\",\n schema: t.object({\n id: db.primaryKey(t.bigint()),\n\n createdAt: db.createdAt(),\n\n updatedAt: db.updatedAt(),\n\n version: db.version(),\n\n type: verificationTypeEnumSchema,\n\n target: t.text({\n description: \"Can be a phone (E.164 format) or email address\",\n }),\n\n code: t.text({\n description: \"Hashed verification token (n-digit code or UUID)\",\n }),\n\n verifiedAt: t.optional(\n t.datetime({\n description: \"When it was successfully verified\",\n }),\n ),\n\n attempts: db.default(\n t.integer({\n description: \"Number of failed attempts (to prevent brute-force)\",\n }),\n 0,\n ),\n }),\n indexes: [\n \"createdAt\",\n {\n columns: [\"target\", \"code\"],\n },\n ],\n});\n\nexport const verificationEntitySchema = verifications.schema;\nexport const verificationEntityInsertSchema = verifications.insertSchema;\nexport type VerificationEntity = Static<typeof verifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const verificationSettingsSchema = t.object({\n code: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeLength: t.integer({\n description: \"Length of the verification code.\",\n minimum: 4,\n maximum: 12,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification code expires.\",\n minimum: 60, // 1 minute\n maximum: 3600, // 1 hour\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to code verifications.\",\n },\n ),\n link: t.object(\n {\n maxAttempts: t.integer({\n description:\n \"Maximum number of attempts before locking the verification.\",\n minimum: 1,\n maximum: 10,\n }),\n codeExpiration: t.integer({\n description: \"Time in seconds before the verification token expires.\",\n minimum: 60, // 1 minute\n maximum: 7200, // 2 hours\n }),\n verificationCooldown: t.integer({\n description: \"Cooldown period in seconds after a request verification.\",\n minimum: 0,\n maximum: 3600, // 1 hour\n }),\n limitPerDay: t.integer({\n description:\n \"Maximum number of verification requests per day for one entry.\",\n minimum: 1,\n maximum: 100,\n }),\n },\n {\n description: \"Settings specific to link verifications.\",\n },\n ),\n purgeDays: t.integer({\n description:\n \"Number of days after which expired verifications are automatically deleted. Set to 0 to disable auto-deletion.\",\n minimum: 0,\n maximum: 365,\n }),\n});\n\nexport type VerificationSettings = Static<typeof verificationSettingsSchema>;\n","import { $atom, $use, type Static } from \"alepha\";\nimport {\n type VerificationSettings,\n verificationSettingsSchema,\n} from \"../schemas/verificationSettingsSchema.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Verification settings configuration atom\n */\nexport const verificationOptions = $atom({\n name: \"alepha.api.verifications.options\",\n schema: verificationSettingsSchema,\n default: {\n code: {\n maxAttempts: 5,\n codeLength: 6,\n codeExpiration: 300, // 5 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n link: {\n maxAttempts: 3, // Lower since UUIDs are harder to guess\n codeExpiration: 1800, // 30 minutes\n verificationCooldown: 90,\n limitPerDay: 10,\n },\n purgeDays: 1,\n },\n});\n\nexport type VerificationOptions = Static<typeof verificationOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [verificationOptions.key]: VerificationOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class VerificationParameters {\n protected readonly options = $use(verificationOptions);\n\n public get<K extends keyof VerificationSettings>(\n key: K,\n ): VerificationSettings[K] {\n return this.options[key];\n }\n}\n","import { createHash, randomInt, randomUUID } from \"node:crypto\";\nimport { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { BadRequestError, NotFoundError } from \"alepha/server\";\nimport {\n type VerificationEntity,\n verifications,\n} from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\nimport type { RequestVerificationResponse } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport type { ValidateVerificationCodeResponse } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport type { VerificationTypeEnum } from \"../schemas/verificationTypeEnumSchema.ts\";\n\nexport class VerificationService {\n protected readonly log = $logger();\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly verificationRepository = $repository(verifications);\n\n public async findByEntry(\n entry: VerificationEntry,\n ): Promise<VerificationEntity> {\n this.log.trace(\"Finding verification by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n const results = await this.verificationRepository.findMany({\n limit: 1, // only need the most recent entry\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n },\n });\n\n if (results.length === 0) {\n this.log.debug(\"Verification entry not found\", {\n type: entry.type,\n target: entry.target,\n });\n throw new NotFoundError(\"Verification entry not found\");\n }\n\n this.log.debug(\"Verification entry found\", {\n id: results[0].id,\n type: entry.type,\n target: entry.target,\n });\n\n return results[0];\n }\n\n public findRecentsByEntry(entry: VerificationEntry) {\n this.log.trace(\"Finding recent verifications by entry\", {\n type: entry.type,\n target: entry.target,\n });\n\n return this.verificationRepository.findMany({\n orderBy: {\n column: \"createdAt\",\n direction: \"desc\",\n },\n where: {\n type: { eq: entry.type },\n target: { eq: entry.target },\n createdAt: {\n gte: this.dateTimeProvider.now().startOf(\"day\").toISOString(),\n },\n },\n });\n }\n\n /**\n * Creates a verification entry and returns the token.\n * The caller is responsible for sending notifications with the token.\n * This allows for context-specific notifications (e.g., password reset vs email verification).\n */\n public async createVerification(\n entry: VerificationEntry,\n ): Promise<RequestVerificationResponse> {\n this.log.trace(\"Creating verification\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const recents = await this.findRecentsByEntry(entry);\n if (recents.length >= settings.limitPerDay) {\n this.log.warn(\"Daily verification limit reached\", {\n type: entry.type,\n target: entry.target,\n limit: settings.limitPerDay,\n count: recents.length,\n });\n throw new BadRequestError(\n `Maximum number of verification requests per day reached (${settings.limitPerDay})`,\n );\n }\n\n const existingVerification = recents[0];\n if (existingVerification) {\n const nowSec = this.dateTimeProvider.now().unix();\n const createdAtSec = this.dateTimeProvider\n .of(existingVerification.createdAt)\n .unix();\n\n const diffSec = nowSec - createdAtSec;\n if (diffSec < settings.verificationCooldown) {\n const remainingCooldown = Math.floor(\n settings.verificationCooldown - diffSec,\n );\n this.log.debug(\"Verification on cooldown\", {\n type: entry.type,\n target: entry.target,\n remainingSeconds: remainingCooldown,\n });\n throw new BadRequestError(\n `Verification is on cooldown for ${remainingCooldown} seconds`,\n );\n }\n }\n\n const token = this.generateToken(entry.type);\n\n const verification = await this.verificationRepository.create({\n type: entry.type,\n target: entry.target,\n code: this.hashCode(token),\n });\n\n this.log.info(\"Verification created\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n expiresInSeconds: settings.codeExpiration,\n });\n\n return {\n token,\n codeExpiration: settings.codeExpiration,\n verificationCooldown: settings.verificationCooldown,\n maxVerificationAttempts: settings.maxAttempts,\n };\n }\n\n public async verifyCode(\n entry: VerificationEntry,\n code: string,\n ): Promise<ValidateVerificationCodeResponse> {\n this.log.trace(\"Verifying code\", {\n type: entry.type,\n target: entry.target,\n });\n\n const settings = this.verificationParameters.get(entry.type);\n\n const verification = await this.findByEntry(entry);\n if (verification.verifiedAt) {\n this.log.debug(\"Verification already verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n verifiedAt: verification.verifiedAt,\n });\n return { ok: true, alreadyVerified: true };\n }\n\n // DO NOT DELETE THE VERIFICATION WHEN IT IS REJECTED,\n // or we won't be able to cooldown the verification\n\n const now = this.dateTimeProvider.now();\n const expirationDate = this.dateTimeProvider\n .of(verification.createdAt)\n .add(settings.codeExpiration, \"seconds\");\n\n if (now > expirationDate) {\n this.log.warn(\"Verification code expired\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n createdAt: verification.createdAt,\n expiredAt: expirationDate.toISOString(),\n });\n throw new BadRequestError(\"Verification code has expired\");\n }\n\n if (verification.attempts >= settings.maxAttempts) {\n this.log.warn(\"Verification locked due to max attempts\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: verification.attempts,\n maxAttempts: settings.maxAttempts,\n });\n throw new BadRequestError(\n \"Maximum number of attempts reached - verification is locked\",\n );\n }\n\n if (verification.code !== this.hashCode(code)) {\n const newAttempts = verification.attempts + 1;\n this.log.warn(\"Invalid verification code\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n attempts: newAttempts,\n maxAttempts: settings.maxAttempts,\n });\n await this.verificationRepository.updateById(verification.id, {\n attempts: newAttempts,\n });\n throw new BadRequestError(\"Invalid verification code\");\n }\n\n await this.verificationRepository.updateById(verification.id, {\n verifiedAt: this.dateTimeProvider.nowISOString(),\n });\n\n this.log.info(\"Verification code verified\", {\n id: verification.id,\n type: entry.type,\n target: entry.target,\n });\n\n return { ok: true };\n }\n\n public hashCode(code: string): string {\n return createHash(\"sha256\").update(code).digest(\"hex\");\n }\n\n public generateToken(type: VerificationTypeEnum): string {\n if (type === \"code\") {\n const settings = this.verificationParameters.get(\"code\");\n return randomInt(0, 1_000_000)\n .toString()\n .padStart(settings.codeLength, \"0\");\n } else if (type === \"link\") {\n return randomUUID();\n }\n\n throw new BadRequestError(`Invalid verification type: ${type}`);\n }\n}\n\nexport interface VerificationEntry {\n type: VerificationTypeEnum;\n target: string;\n}\n","import { $inject, t } from \"alepha\";\nimport { $action } from \"alepha/server\";\nimport { requestVerificationCodeResponseSchema } from \"../schemas/requestVerificationCodeResponseSchema.ts\";\nimport { validateVerificationCodeResponseSchema } from \"../schemas/validateVerificationCodeResponseSchema.ts\";\nimport { verificationTypeEnumSchema } from \"../schemas/verificationTypeEnumSchema.ts\";\nimport { VerificationService } from \"../services/VerificationService.ts\";\n\nexport class VerificationController {\n protected readonly verificationService = $inject(VerificationService);\n\n public readonly url = \"/verifications\";\n public readonly group = \"verifications\";\n\n public readonly requestVerificationCode = $action({\n path: `${this.url}/:type`,\n group: this.group,\n method: \"POST\",\n secure: false,\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n }),\n response: requestVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return await this.verificationService.createVerification({\n type: params.type,\n target: body.target,\n });\n },\n });\n\n public readonly validateVerificationCode = $action({\n path: `${this.url}/:type/validate`,\n group: this.group,\n method: \"POST\",\n secure: false,\n schema: {\n params: t.object({\n type: verificationTypeEnumSchema,\n }),\n body: t.object({\n target: t.text(),\n token: t.text({\n description:\n \"The verification token (6-digit code for phone, UUID for email).\",\n }),\n }),\n response: validateVerificationCodeResponseSchema,\n },\n handler: async ({ body, params }) => {\n return this.verificationService.verifyCode(\n {\n type: params.type,\n target: body.target,\n },\n body.token,\n );\n },\n });\n}\n","import { $inject } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $repository } from \"alepha/orm\";\nimport { $scheduler } from \"alepha/scheduler\";\nimport { verifications } from \"../entities/verifications.ts\";\nimport { VerificationParameters } from \"../parameters/VerificationParameters.ts\";\n\nexport class VerificationJobs {\n protected readonly verificationRepository = $repository(verifications);\n protected readonly verificationParameters = $inject(VerificationParameters);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n\n public readonly cleanExpired = $scheduler({\n cron: \"0 0 * * *\", // Every day at midnight\n description: \"Clean expired verifications\",\n handler: async () => {\n const purgeDays = this.verificationParameters.get(\"purgeDays\");\n if (purgeDays <= 0) {\n return; // Auto deletion is disabled\n }\n\n const dayMs = 24 * 60 * 60 * 1000;\n const purgeThreshold = Date.now() - purgeDays * dayMs;\n\n await this.verificationRepository.deleteMany({\n createdAt: {\n lt: this.dateTimeProvider.of(purgeThreshold).toISOString(),\n },\n });\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { VerificationController } from \"./controllers/VerificationController.ts\";\nimport { VerificationJobs } from \"./jobs/VerificationJobs.ts\";\nimport { VerificationParameters } from \"./parameters/VerificationParameters.ts\";\nimport { VerificationService } from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/VerificationController.ts\";\nexport * from \"./entities/verifications.ts\";\nexport * from \"./jobs/VerificationJobs.ts\";\nexport * from \"./parameters/VerificationParameters.ts\";\nexport * from \"./schemas/requestVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/validateVerificationCodeResponseSchema.ts\";\nexport * from \"./schemas/verificationTypeEnumSchema.ts\";\nexport * from \"./services/VerificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * | Stability | Since | Runtime |\n * |-----------|-------|---------|\n * | 3 - stable | 0.13.0 | node, bun, workerd|\n *\n * Email and phone verification workflows.\n *\n * **Features:**\n * - Verification token generation\n * - Verification code sending\n * - Verification completion tracking\n * - Resend functionality\n *\n * @module alepha.api.verifications\n */\nexport const AlephaApiVerification = $module({\n name: \"alepha.api.verifications\",\n services: [\n VerificationController,\n VerificationJobs,\n VerificationService,\n VerificationParameters,\n ],\n});\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import * as alepha1 from "alepha";
1
+ import * as alepha2 from "alepha";
2
2
  import { Alepha, KIND, Primitive, Static, TSchema } from "alepha";
3
3
  import { DateTimeProvider, DurationLike } from "alepha/datetime";
4
4
  import * as alepha_logger0 from "alepha/logger";
@@ -123,7 +123,7 @@ declare class BatchProvider {
123
123
  /**
124
124
  * Shutdown hook - flushes all batch contexts on application stop.
125
125
  */
126
- protected readonly onStop: alepha1.HookPrimitive<"stop">;
126
+ protected readonly onStop: alepha2.HookPrimitive<"stop">;
127
127
  /**
128
128
  * Get the effective maxSize for a context.
129
129
  */
@@ -285,14 +285,14 @@ declare class BatchPrimitive<TItem extends TSchema, TResponse = any> extends Pri
285
285
  * @returns The number of items cleared
286
286
  */
287
287
  clearCompleted(status?: "completed" | "failed"): number;
288
- protected readonly onReady: alepha1.HookPrimitive<"ready">;
288
+ protected readonly onReady: alepha2.HookPrimitive<"ready">;
289
289
  }
290
290
  //#endregion
291
291
  //#region ../../src/batch/index.d.ts
292
292
  /**
293
- * | type | quality | stability |
294
- * |------|---------|-----------|
295
- * | backend | standard | stable |
293
+ * | Stability | Since | Runtime |
294
+ * |-----------|-------|---------|
295
+ * | 3 - stable | 0.8.0 | node, bun|
296
296
  *
297
297
  * Batch accumulation and processing.
298
298
  *
@@ -304,7 +304,7 @@ declare class BatchPrimitive<TItem extends TSchema, TResponse = any> extends Pri
304
304
  *
305
305
  * @module alepha.batch
306
306
  */
307
- declare const AlephaBatch: alepha1.Service<alepha1.Module>;
307
+ declare const AlephaBatch: alepha2.Service<alepha2.Module>;
308
308
  //#endregion
309
309
  export { $batch, AlephaBatch, BatchContext, type BatchItemState, type BatchItemStatus, BatchOptions, BatchPrimitive, BatchPrimitiveOptions, BatchProvider, PartitionState };
310
310
  //# sourceMappingURL=index.d.ts.map
@@ -361,9 +361,9 @@ $batch[KIND] = BatchPrimitive;
361
361
  //#endregion
362
362
  //#region ../../src/batch/index.ts
363
363
  /**
364
- * | type | quality | stability |
365
- * |------|---------|-----------|
366
- * | backend | standard | stable |
364
+ * | Stability | Since | Runtime |
365
+ * |-----------|-------|---------|
366
+ * | 3 - stable | 0.8.0 | node, bun|
367
367
  *
368
368
  * Batch accumulation and processing.
369
369
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/batch/providers/BatchProvider.ts","../../src/batch/primitives/$batch.ts","../../src/batch/index.ts"],"sourcesContent":["import { $hook, $inject, type Alepha } from \"alepha\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { type RetryBackoffOptions, RetryProvider } from \"alepha/retry\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchOptions<TItem, TResponse = any> {\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: TItem[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n *\n * @default 10\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n *\n * @default [1, \"second\"]\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: TItem) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n *\n * @default 1\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: {\n /**\n * The maximum number of attempts.\n *\n * @default 3\n */\n max?: number;\n\n /**\n * The backoff strategy for delays between retries.\n * Can be a fixed number (in ms) or a configuration object for exponential backoff.\n *\n * @default { initial: 200, factor: 2, jitter: true }\n */\n backoff?: number | RetryBackoffOptions;\n\n /**\n * An overall time limit for all retry attempts combined.\n *\n * e.g., `[5, 'seconds']`\n */\n maxDuration?: DurationLike;\n\n /**\n * A function that determines if a retry should be attempted based on the error.\n *\n * @default (error) => true (retries on any error)\n */\n when?: (error: Error) => boolean;\n\n /**\n * A custom callback for when a retry attempt fails.\n * This is called before the delay.\n */\n onError?: (error: Error, attempt: number) => void;\n };\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type BatchItemStatus = \"pending\" | \"processing\" | \"completed\" | \"failed\";\n\nexport interface BatchItemState<TItem, TResponse> {\n id: string;\n item: TItem;\n partitionKey: string;\n status: BatchItemStatus;\n result?: TResponse;\n error?: Error;\n promise?: Promise<TResponse>;\n resolve?: (value: TResponse) => void;\n reject?: (error: Error) => void;\n}\n\nexport interface PartitionState {\n itemIds: string[];\n timeout?: { clear: () => void };\n flushing: boolean;\n}\n\n/**\n * Context object that holds all state for a batch processor instance.\n */\nexport interface BatchContext<TItem, TResponse> {\n options: BatchOptions<TItem, TResponse>;\n itemStates: Map<string, BatchItemState<TItem, TResponse>>;\n partitions: Map<string, PartitionState>;\n activeHandlers: PromiseWithResolvers<void>[];\n isShuttingDown: boolean;\n isReady: boolean;\n alepha: Alepha;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Service for batch processing operations.\n * Provides methods to manage batches of items with automatic flushing based on size or time.\n */\nexport class BatchProvider {\n protected readonly log = $logger();\n protected readonly dateTime = $inject(DateTimeProvider);\n protected readonly retryProvider = $inject(RetryProvider);\n\n /**\n * All active batch contexts managed by this provider.\n */\n protected readonly contexts = new Set<BatchContext<any, any>>();\n\n /**\n * Creates a new batch context with the given options.\n */\n createContext<TItem, TResponse>(\n alepha: Alepha,\n options: BatchOptions<TItem, TResponse>,\n ): BatchContext<TItem, TResponse> {\n const context: BatchContext<TItem, TResponse> = {\n options,\n itemStates: new Map(),\n partitions: new Map(),\n activeHandlers: [],\n isShuttingDown: false,\n isReady: false,\n alepha,\n };\n this.contexts.add(context);\n return context;\n }\n\n /**\n * Shutdown hook - flushes all batch contexts on application stop.\n */\n protected readonly onStop = $hook({\n on: \"stop\",\n priority: \"first\",\n handler: async () => {\n if (this.contexts.size === 0) {\n return;\n }\n this.log.debug(`Shutting down ${this.contexts.size} batch context(s)...`);\n const promises: Promise<void>[] = [];\n for (const context of this.contexts) {\n promises.push(this.shutdown(context));\n }\n await Promise.all(promises);\n this.log.debug(\"All batch contexts shut down\");\n },\n });\n\n /**\n * Get the effective maxSize for a context.\n */\n protected getMaxSize<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.maxSize ?? 10;\n }\n\n /**\n * Get the effective concurrency for a context.\n */\n protected getConcurrency<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.concurrency ?? 1;\n }\n\n /**\n * Get the effective maxDuration for a context.\n */\n protected getMaxDuration<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): DurationLike {\n return context.options.maxDuration ?? [1, \"second\"];\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n *\n * @throws Error if maxQueueSize is exceeded\n */\n push<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n item: TItem,\n ): string {\n // 1. Generate unique ID\n const id = crypto.randomUUID();\n\n // 2. Determine the partition key (with error handling)\n let partitionKey: string;\n try {\n partitionKey = context.options.partitionBy\n ? context.options.partitionBy(item)\n : \"default\";\n } catch (error) {\n this.log.warn(\n \"partitionBy function threw an error, using 'default' partition\",\n { error },\n );\n partitionKey = \"default\";\n }\n\n // 3. Create item state\n const itemState: BatchItemState<TItem, TResponse> = {\n id,\n item,\n partitionKey,\n status: \"pending\",\n };\n\n // CAUTION: Do not log.debug/info here as it may cause infinite loops if logging is batched\n\n context.itemStates.set(id, itemState);\n\n // 4. Get or create the partition state\n if (!context.partitions.has(partitionKey)) {\n context.partitions.set(partitionKey, {\n itemIds: [],\n flushing: false,\n });\n }\n const partition = context.partitions.get(partitionKey)!;\n\n // 5. Check maxQueueSize before adding\n if (\n context.options.maxQueueSize !== undefined &&\n partition.itemIds.length >= context.options.maxQueueSize\n ) {\n throw new Error(\n `Batch queue size exceeded for partition '${partitionKey}' (max: ${context.options.maxQueueSize})`,\n );\n }\n\n // 6. Add item ID to partition\n partition.itemIds.push(id);\n\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n // 7. Only start processing if the app is ready (after \"ready\" hook)\n // During startup, items are just buffered in memory\n if (context.isReady) {\n // Check if the batch is full\n if (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Batch partition '${partitionKey}' is full, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on max size`,\n error,\n ),\n );\n } else if (!partition.timeout && !partition.flushing) {\n // 8. Start the timeout if it's not already running for this partition and not currently flushing\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n } else {\n // Not ready yet - just buffer items, no size checks or timeouts\n this.log.trace(\n `Buffering item in partition '${partitionKey}' (app not ready yet, ${partition.itemIds.length} items buffered)`,\n );\n }\n\n // 9. Return ID immediately\n return id;\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n async wait<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ): Promise<TResponse> {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n throw new Error(`Item with id '${id}' not found`);\n }\n\n // If already completed or failed, return immediately\n if (itemState.status === \"completed\") {\n return itemState.result!;\n }\n if (itemState.status === \"failed\") {\n throw itemState.error!;\n }\n\n // Create promise on-demand if not already created\n if (!itemState.promise) {\n itemState.promise = new Promise<TResponse>((resolve, reject) => {\n itemState.resolve = resolve;\n itemState.reject = reject;\n });\n }\n\n return itemState.promise;\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n status<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n return undefined;\n }\n\n if (itemState.status === \"completed\") {\n return { status: \"completed\", result: itemState.result! };\n }\n if (itemState.status === \"failed\") {\n return { status: \"failed\", error: itemState.error! };\n }\n return { status: itemState.status };\n }\n\n /**\n * Clears completed and failed items from the context to free memory.\n * Returns the number of items cleared.\n *\n * @param context The batch context\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n clearCompleted<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n status?: \"completed\" | \"failed\",\n ): number {\n let count = 0;\n for (const [id, state] of context.itemStates) {\n if (status) {\n if (state.status === status) {\n context.itemStates.delete(id);\n count++;\n }\n } else if (state.status === \"completed\" || state.status === \"failed\") {\n context.itemStates.delete(id);\n count++;\n }\n }\n return count;\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n async flush<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey?: string,\n ): Promise<void> {\n const promises: Promise<void>[] = [];\n if (partitionKey) {\n if (context.partitions.has(partitionKey)) {\n promises.push(this.flushPartition(context, partitionKey));\n }\n } else {\n for (const key of context.partitions.keys()) {\n promises.push(this.flushPartition(context, key));\n }\n }\n await Promise.all(promises);\n }\n\n /**\n * Flush a specific partition.\n */\n protected async flushPartition<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey: string,\n limit?: number,\n ): Promise<void> {\n const partition = context.partitions.get(partitionKey);\n if (!partition || partition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n return;\n }\n\n // Clear the timeout and grab the item IDs (up to limit if specified)\n partition.timeout?.clear();\n partition.timeout = undefined;\n const itemsToTake =\n limit !== undefined\n ? Math.min(limit, partition.itemIds.length)\n : partition.itemIds.length;\n const itemIdsToProcess = partition.itemIds.splice(0, itemsToTake);\n\n // Mark partition as flushing to prevent race conditions\n partition.flushing = true;\n\n // Get the items and mark them as processing\n const itemsToProcess: TItem[] = [];\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"processing\";\n itemsToProcess.push(itemState.item);\n }\n }\n\n const concurrency = this.getConcurrency(context);\n const maxDuration = this.getMaxDuration(context);\n\n // Wait until there's a free slot (if at concurrency limit)\n while (context.activeHandlers.length >= concurrency) {\n this.log.trace(\n `Batch handler is at concurrency limit, waiting for a slot...`,\n );\n // Wait for any single handler to complete, not all of them\n await Promise.race(context.activeHandlers.map((it) => it.promise));\n }\n\n const promise = Promise.withResolvers<void>();\n context.activeHandlers.push(promise);\n let result: any;\n try {\n result = await context.alepha.context.run(() =>\n // during shutdown, call handler directly to avoid retry cancellation\n context.isShuttingDown\n ? context.options.handler(itemsToProcess)\n : this.retryProvider.retry(\n {\n ...context.options.retry,\n handler: context.options.handler,\n },\n itemsToProcess,\n ),\n );\n\n // Mark all items as completed and resolve their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"completed\";\n itemState.result = result;\n // Only resolve if someone is waiting\n itemState.resolve?.(result);\n }\n }\n } catch (error) {\n this.log.error(`Batch handler failed`, error);\n\n // Mark all items as failed and reject their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"failed\";\n itemState.error = error as Error;\n // Only reject if someone is waiting (promise was created)\n itemState.reject?.(error as Error);\n }\n }\n } finally {\n promise.resolve();\n context.activeHandlers = context.activeHandlers.filter(\n (it) => it !== promise,\n );\n\n // Only delete partition if no new items arrived during processing\n const currentPartition = context.partitions.get(partitionKey);\n if (currentPartition?.flushing && currentPartition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n } else if (currentPartition) {\n // Reset flushing flag if partition still exists with items\n currentPartition.flushing = false;\n\n // Restart timeout for items that arrived during flush\n if (currentPartition.itemIds.length > 0 && !currentPartition.timeout) {\n currentPartition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n }\n\n /**\n * Mark the context as ready and start processing buffered items.\n * Called after the \"ready\" hook.\n */\n async markReady<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\n \"Batch processor is now ready, starting to process buffered items...\",\n );\n context.isReady = true;\n await this.startProcessing(context);\n }\n\n /**\n * Mark the context as shutting down and flush all remaining items.\n */\n async shutdown<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\"Flushing all remaining batch partitions on shutdown...\");\n context.isShuttingDown = true;\n await this.flush(context);\n this.log.debug(\"All batch partitions flushed\");\n }\n\n /**\n * Called after the \"ready\" hook to start processing buffered items that were\n * pushed during startup. This checks all partitions and starts timeouts/flushes\n * for items that were accumulated before the app was ready.\n */\n protected async startProcessing<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n for (const [partitionKey, partition] of context.partitions.entries()) {\n if (partition.itemIds.length === 0) {\n continue;\n }\n\n this.log.trace(\n `Starting processing for partition '${partitionKey}' with ${partition.itemIds.length} buffered items`,\n );\n\n // Flush batches of maxSize while we have items >= maxSize\n while (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Partition '${partitionKey}' has ${partition.itemIds.length} items, flushing batch of ${maxSize}...`,\n );\n await this.flushPartition(context, partitionKey, maxSize);\n }\n\n // After flushing full batches, start timeout for any remaining items\n if (\n partition.itemIds.length > 0 &&\n !partition.timeout &&\n !partition.flushing\n ) {\n this.log.trace(\n `Starting timeout for partition '${partitionKey}' with ${partition.itemIds.length} remaining items`,\n );\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush partition '${partitionKey}' on timeout after startup`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n}\n","import {\n $hook,\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport type { RetryPrimitiveOptions } from \"alepha/retry\";\nimport {\n type BatchContext,\n type BatchItemState,\n type BatchItemStatus,\n BatchProvider,\n} from \"../providers/BatchProvider.ts\";\n\n/**\n * Creates a batch processing primitive for efficient grouping and processing of multiple operations.\n */\nexport const $batch = <TItem extends TSchema, TResponse>(\n options: BatchPrimitiveOptions<TItem, TResponse>,\n): BatchPrimitive<TItem, TResponse> =>\n createPrimitive(BatchPrimitive<TItem, TResponse>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchPrimitiveOptions<TItem extends TSchema, TResponse = any> {\n /**\n * TypeBox schema for validating each item added to the batch.\n */\n schema: TItem;\n\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: Static<TItem>[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: Static<TItem>) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: Omit<RetryPrimitiveOptions<() => Array<Static<TItem>>>, \"handler\">;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type { BatchItemState, BatchItemStatus };\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BatchPrimitive<\n TItem extends TSchema,\n TResponse = any,\n> extends Primitive<BatchPrimitiveOptions<TItem, TResponse>> {\n protected readonly batchProvider = $inject(BatchProvider);\n protected readonly context: BatchContext<Static<TItem>, TResponse>;\n\n constructor(\n ...args: ConstructorParameters<\n typeof Primitive<BatchPrimitiveOptions<TItem, TResponse>>\n >\n ) {\n super(...args);\n this.context = this.batchProvider.createContext(this.alepha, {\n handler: this.options.handler,\n maxSize: this.options.maxSize,\n maxQueueSize: this.options.maxQueueSize,\n maxDuration: this.options.maxDuration,\n partitionBy: this.options.partitionBy,\n concurrency: this.options.concurrency,\n retry: this.options.retry,\n });\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n */\n public async push(item: Static<TItem>): Promise<string> {\n // Validate the item against the schema\n const validatedItem = this.alepha.codec.validate(this.options.schema, item);\n return this.batchProvider.push(this.context, validatedItem);\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n public async wait(id: string): Promise<TResponse> {\n return this.batchProvider.wait(this.context, id);\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n public status(\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n return this.batchProvider.status(this.context, id);\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n public async flush(partitionKey?: string): Promise<void> {\n return this.batchProvider.flush(this.context, partitionKey);\n }\n\n /**\n * Clears completed and failed items from memory.\n * Call this periodically in long-running applications to prevent memory leaks.\n *\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n public clearCompleted(status?: \"completed\" | \"failed\"): number {\n return this.batchProvider.clearCompleted(this.context, status);\n }\n\n protected readonly onReady = $hook({\n on: \"ready\",\n handler: async () => {\n await this.batchProvider.markReady(this.context);\n },\n });\n}\n\n$batch[KIND] = BatchPrimitive;\n","import { $module } from \"alepha\";\nimport { $batch } from \"./primitives/$batch.ts\";\nimport { BatchProvider } from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$batch.ts\";\nexport * from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * | type | quality | stability |\n * |------|---------|-----------|\n * | backend | standard | stable |\n *\n * Batch accumulation and processing.\n *\n * **Features:**\n * - Batch accumulator with handler\n * - Configurable batch size\n * - Time-based triggers\n * - Status tracking\n *\n * @module alepha.batch\n */\nexport const AlephaBatch = $module({\n name: \"alepha.batch\",\n primitives: [$batch],\n services: [BatchProvider],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/batch/providers/BatchProvider.ts","../../src/batch/primitives/$batch.ts","../../src/batch/index.ts"],"sourcesContent":["import { $hook, $inject, type Alepha } from \"alepha\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { type RetryBackoffOptions, RetryProvider } from \"alepha/retry\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchOptions<TItem, TResponse = any> {\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: TItem[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n *\n * @default 10\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n *\n * @default [1, \"second\"]\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: TItem) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n *\n * @default 1\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: {\n /**\n * The maximum number of attempts.\n *\n * @default 3\n */\n max?: number;\n\n /**\n * The backoff strategy for delays between retries.\n * Can be a fixed number (in ms) or a configuration object for exponential backoff.\n *\n * @default { initial: 200, factor: 2, jitter: true }\n */\n backoff?: number | RetryBackoffOptions;\n\n /**\n * An overall time limit for all retry attempts combined.\n *\n * e.g., `[5, 'seconds']`\n */\n maxDuration?: DurationLike;\n\n /**\n * A function that determines if a retry should be attempted based on the error.\n *\n * @default (error) => true (retries on any error)\n */\n when?: (error: Error) => boolean;\n\n /**\n * A custom callback for when a retry attempt fails.\n * This is called before the delay.\n */\n onError?: (error: Error, attempt: number) => void;\n };\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type BatchItemStatus = \"pending\" | \"processing\" | \"completed\" | \"failed\";\n\nexport interface BatchItemState<TItem, TResponse> {\n id: string;\n item: TItem;\n partitionKey: string;\n status: BatchItemStatus;\n result?: TResponse;\n error?: Error;\n promise?: Promise<TResponse>;\n resolve?: (value: TResponse) => void;\n reject?: (error: Error) => void;\n}\n\nexport interface PartitionState {\n itemIds: string[];\n timeout?: { clear: () => void };\n flushing: boolean;\n}\n\n/**\n * Context object that holds all state for a batch processor instance.\n */\nexport interface BatchContext<TItem, TResponse> {\n options: BatchOptions<TItem, TResponse>;\n itemStates: Map<string, BatchItemState<TItem, TResponse>>;\n partitions: Map<string, PartitionState>;\n activeHandlers: PromiseWithResolvers<void>[];\n isShuttingDown: boolean;\n isReady: boolean;\n alepha: Alepha;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Service for batch processing operations.\n * Provides methods to manage batches of items with automatic flushing based on size or time.\n */\nexport class BatchProvider {\n protected readonly log = $logger();\n protected readonly dateTime = $inject(DateTimeProvider);\n protected readonly retryProvider = $inject(RetryProvider);\n\n /**\n * All active batch contexts managed by this provider.\n */\n protected readonly contexts = new Set<BatchContext<any, any>>();\n\n /**\n * Creates a new batch context with the given options.\n */\n createContext<TItem, TResponse>(\n alepha: Alepha,\n options: BatchOptions<TItem, TResponse>,\n ): BatchContext<TItem, TResponse> {\n const context: BatchContext<TItem, TResponse> = {\n options,\n itemStates: new Map(),\n partitions: new Map(),\n activeHandlers: [],\n isShuttingDown: false,\n isReady: false,\n alepha,\n };\n this.contexts.add(context);\n return context;\n }\n\n /**\n * Shutdown hook - flushes all batch contexts on application stop.\n */\n protected readonly onStop = $hook({\n on: \"stop\",\n priority: \"first\",\n handler: async () => {\n if (this.contexts.size === 0) {\n return;\n }\n this.log.debug(`Shutting down ${this.contexts.size} batch context(s)...`);\n const promises: Promise<void>[] = [];\n for (const context of this.contexts) {\n promises.push(this.shutdown(context));\n }\n await Promise.all(promises);\n this.log.debug(\"All batch contexts shut down\");\n },\n });\n\n /**\n * Get the effective maxSize for a context.\n */\n protected getMaxSize<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.maxSize ?? 10;\n }\n\n /**\n * Get the effective concurrency for a context.\n */\n protected getConcurrency<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): number {\n return context.options.concurrency ?? 1;\n }\n\n /**\n * Get the effective maxDuration for a context.\n */\n protected getMaxDuration<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): DurationLike {\n return context.options.maxDuration ?? [1, \"second\"];\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n *\n * @throws Error if maxQueueSize is exceeded\n */\n push<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n item: TItem,\n ): string {\n // 1. Generate unique ID\n const id = crypto.randomUUID();\n\n // 2. Determine the partition key (with error handling)\n let partitionKey: string;\n try {\n partitionKey = context.options.partitionBy\n ? context.options.partitionBy(item)\n : \"default\";\n } catch (error) {\n this.log.warn(\n \"partitionBy function threw an error, using 'default' partition\",\n { error },\n );\n partitionKey = \"default\";\n }\n\n // 3. Create item state\n const itemState: BatchItemState<TItem, TResponse> = {\n id,\n item,\n partitionKey,\n status: \"pending\",\n };\n\n // CAUTION: Do not log.debug/info here as it may cause infinite loops if logging is batched\n\n context.itemStates.set(id, itemState);\n\n // 4. Get or create the partition state\n if (!context.partitions.has(partitionKey)) {\n context.partitions.set(partitionKey, {\n itemIds: [],\n flushing: false,\n });\n }\n const partition = context.partitions.get(partitionKey)!;\n\n // 5. Check maxQueueSize before adding\n if (\n context.options.maxQueueSize !== undefined &&\n partition.itemIds.length >= context.options.maxQueueSize\n ) {\n throw new Error(\n `Batch queue size exceeded for partition '${partitionKey}' (max: ${context.options.maxQueueSize})`,\n );\n }\n\n // 6. Add item ID to partition\n partition.itemIds.push(id);\n\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n // 7. Only start processing if the app is ready (after \"ready\" hook)\n // During startup, items are just buffered in memory\n if (context.isReady) {\n // Check if the batch is full\n if (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Batch partition '${partitionKey}' is full, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on max size`,\n error,\n ),\n );\n } else if (!partition.timeout && !partition.flushing) {\n // 8. Start the timeout if it's not already running for this partition and not currently flushing\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n } else {\n // Not ready yet - just buffer items, no size checks or timeouts\n this.log.trace(\n `Buffering item in partition '${partitionKey}' (app not ready yet, ${partition.itemIds.length} items buffered)`,\n );\n }\n\n // 9. Return ID immediately\n return id;\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n async wait<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ): Promise<TResponse> {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n throw new Error(`Item with id '${id}' not found`);\n }\n\n // If already completed or failed, return immediately\n if (itemState.status === \"completed\") {\n return itemState.result!;\n }\n if (itemState.status === \"failed\") {\n throw itemState.error!;\n }\n\n // Create promise on-demand if not already created\n if (!itemState.promise) {\n itemState.promise = new Promise<TResponse>((resolve, reject) => {\n itemState.resolve = resolve;\n itemState.reject = reject;\n });\n }\n\n return itemState.promise;\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n status<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n const itemState = context.itemStates.get(id);\n if (!itemState) {\n return undefined;\n }\n\n if (itemState.status === \"completed\") {\n return { status: \"completed\", result: itemState.result! };\n }\n if (itemState.status === \"failed\") {\n return { status: \"failed\", error: itemState.error! };\n }\n return { status: itemState.status };\n }\n\n /**\n * Clears completed and failed items from the context to free memory.\n * Returns the number of items cleared.\n *\n * @param context The batch context\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n clearCompleted<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n status?: \"completed\" | \"failed\",\n ): number {\n let count = 0;\n for (const [id, state] of context.itemStates) {\n if (status) {\n if (state.status === status) {\n context.itemStates.delete(id);\n count++;\n }\n } else if (state.status === \"completed\" || state.status === \"failed\") {\n context.itemStates.delete(id);\n count++;\n }\n }\n return count;\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n async flush<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey?: string,\n ): Promise<void> {\n const promises: Promise<void>[] = [];\n if (partitionKey) {\n if (context.partitions.has(partitionKey)) {\n promises.push(this.flushPartition(context, partitionKey));\n }\n } else {\n for (const key of context.partitions.keys()) {\n promises.push(this.flushPartition(context, key));\n }\n }\n await Promise.all(promises);\n }\n\n /**\n * Flush a specific partition.\n */\n protected async flushPartition<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n partitionKey: string,\n limit?: number,\n ): Promise<void> {\n const partition = context.partitions.get(partitionKey);\n if (!partition || partition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n return;\n }\n\n // Clear the timeout and grab the item IDs (up to limit if specified)\n partition.timeout?.clear();\n partition.timeout = undefined;\n const itemsToTake =\n limit !== undefined\n ? Math.min(limit, partition.itemIds.length)\n : partition.itemIds.length;\n const itemIdsToProcess = partition.itemIds.splice(0, itemsToTake);\n\n // Mark partition as flushing to prevent race conditions\n partition.flushing = true;\n\n // Get the items and mark them as processing\n const itemsToProcess: TItem[] = [];\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"processing\";\n itemsToProcess.push(itemState.item);\n }\n }\n\n const concurrency = this.getConcurrency(context);\n const maxDuration = this.getMaxDuration(context);\n\n // Wait until there's a free slot (if at concurrency limit)\n while (context.activeHandlers.length >= concurrency) {\n this.log.trace(\n `Batch handler is at concurrency limit, waiting for a slot...`,\n );\n // Wait for any single handler to complete, not all of them\n await Promise.race(context.activeHandlers.map((it) => it.promise));\n }\n\n const promise = Promise.withResolvers<void>();\n context.activeHandlers.push(promise);\n let result: any;\n try {\n result = await context.alepha.context.run(() =>\n // during shutdown, call handler directly to avoid retry cancellation\n context.isShuttingDown\n ? context.options.handler(itemsToProcess)\n : this.retryProvider.retry(\n {\n ...context.options.retry,\n handler: context.options.handler,\n },\n itemsToProcess,\n ),\n );\n\n // Mark all items as completed and resolve their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"completed\";\n itemState.result = result;\n // Only resolve if someone is waiting\n itemState.resolve?.(result);\n }\n }\n } catch (error) {\n this.log.error(`Batch handler failed`, error);\n\n // Mark all items as failed and reject their promises\n for (const id of itemIdsToProcess) {\n const itemState = context.itemStates.get(id);\n if (itemState) {\n itemState.status = \"failed\";\n itemState.error = error as Error;\n // Only reject if someone is waiting (promise was created)\n itemState.reject?.(error as Error);\n }\n }\n } finally {\n promise.resolve();\n context.activeHandlers = context.activeHandlers.filter(\n (it) => it !== promise,\n );\n\n // Only delete partition if no new items arrived during processing\n const currentPartition = context.partitions.get(partitionKey);\n if (currentPartition?.flushing && currentPartition.itemIds.length === 0) {\n context.partitions.delete(partitionKey);\n } else if (currentPartition) {\n // Reset flushing flag if partition still exists with items\n currentPartition.flushing = false;\n\n // Restart timeout for items that arrived during flush\n if (currentPartition.itemIds.length > 0 && !currentPartition.timeout) {\n currentPartition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush batch partition '${partitionKey}' on timeout`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n }\n\n /**\n * Mark the context as ready and start processing buffered items.\n * Called after the \"ready\" hook.\n */\n async markReady<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\n \"Batch processor is now ready, starting to process buffered items...\",\n );\n context.isReady = true;\n await this.startProcessing(context);\n }\n\n /**\n * Mark the context as shutting down and flush all remaining items.\n */\n async shutdown<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n this.log.debug(\"Flushing all remaining batch partitions on shutdown...\");\n context.isShuttingDown = true;\n await this.flush(context);\n this.log.debug(\"All batch partitions flushed\");\n }\n\n /**\n * Called after the \"ready\" hook to start processing buffered items that were\n * pushed during startup. This checks all partitions and starts timeouts/flushes\n * for items that were accumulated before the app was ready.\n */\n protected async startProcessing<TItem, TResponse>(\n context: BatchContext<TItem, TResponse>,\n ): Promise<void> {\n const maxSize = this.getMaxSize(context);\n const maxDuration = this.getMaxDuration(context);\n\n for (const [partitionKey, partition] of context.partitions.entries()) {\n if (partition.itemIds.length === 0) {\n continue;\n }\n\n this.log.trace(\n `Starting processing for partition '${partitionKey}' with ${partition.itemIds.length} buffered items`,\n );\n\n // Flush batches of maxSize while we have items >= maxSize\n while (partition.itemIds.length >= maxSize) {\n this.log.trace(\n `Partition '${partitionKey}' has ${partition.itemIds.length} items, flushing batch of ${maxSize}...`,\n );\n await this.flushPartition(context, partitionKey, maxSize);\n }\n\n // After flushing full batches, start timeout for any remaining items\n if (\n partition.itemIds.length > 0 &&\n !partition.timeout &&\n !partition.flushing\n ) {\n this.log.trace(\n `Starting timeout for partition '${partitionKey}' with ${partition.itemIds.length} remaining items`,\n );\n partition.timeout = this.dateTime.createTimeout(() => {\n this.log.trace(\n `Batch partition '${partitionKey}' timed out, flushing...`,\n );\n this.flushPartition(context, partitionKey).catch((error) =>\n this.log.error(\n `Failed to flush partition '${partitionKey}' on timeout after startup`,\n error,\n ),\n );\n }, maxDuration);\n }\n }\n }\n}\n","import {\n $hook,\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport type { RetryPrimitiveOptions } from \"alepha/retry\";\nimport {\n type BatchContext,\n type BatchItemState,\n type BatchItemStatus,\n BatchProvider,\n} from \"../providers/BatchProvider.ts\";\n\n/**\n * Creates a batch processing primitive for efficient grouping and processing of multiple operations.\n */\nexport const $batch = <TItem extends TSchema, TResponse>(\n options: BatchPrimitiveOptions<TItem, TResponse>,\n): BatchPrimitive<TItem, TResponse> =>\n createPrimitive(BatchPrimitive<TItem, TResponse>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface BatchPrimitiveOptions<TItem extends TSchema, TResponse = any> {\n /**\n * TypeBox schema for validating each item added to the batch.\n */\n schema: TItem;\n\n /**\n * The batch processing handler function that processes arrays of validated items.\n */\n handler: (items: Static<TItem>[]) => TResponse;\n\n /**\n * Maximum number of items to collect before automatically flushing the batch.\n */\n maxSize?: number;\n\n /**\n * Maximum number of items that can be queued in a single partition.\n * If exceeded, push() will throw an error.\n */\n maxQueueSize?: number;\n\n /**\n * Maximum time to wait before flushing a batch, even if it hasn't reached maxSize.\n */\n maxDuration?: DurationLike;\n\n /**\n * Function to determine partition keys for grouping items into separate batches.\n */\n partitionBy?: (item: Static<TItem>) => string;\n\n /**\n * Maximum number of batch handlers that can execute simultaneously.\n */\n concurrency?: number;\n\n /**\n * Retry configuration for failed batch processing operations.\n */\n retry?: Omit<RetryPrimitiveOptions<() => Array<Static<TItem>>>, \"handler\">;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type { BatchItemState, BatchItemStatus };\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BatchPrimitive<\n TItem extends TSchema,\n TResponse = any,\n> extends Primitive<BatchPrimitiveOptions<TItem, TResponse>> {\n protected readonly batchProvider = $inject(BatchProvider);\n protected readonly context: BatchContext<Static<TItem>, TResponse>;\n\n constructor(\n ...args: ConstructorParameters<\n typeof Primitive<BatchPrimitiveOptions<TItem, TResponse>>\n >\n ) {\n super(...args);\n this.context = this.batchProvider.createContext(this.alepha, {\n handler: this.options.handler,\n maxSize: this.options.maxSize,\n maxQueueSize: this.options.maxQueueSize,\n maxDuration: this.options.maxDuration,\n partitionBy: this.options.partitionBy,\n concurrency: this.options.concurrency,\n retry: this.options.retry,\n });\n }\n\n /**\n * Pushes an item into the batch and returns immediately with a unique ID.\n * The item will be processed asynchronously with other items when the batch is flushed.\n * Use wait(id) to get the processing result.\n */\n public async push(item: Static<TItem>): Promise<string> {\n // Validate the item against the schema\n const validatedItem = this.alepha.codec.validate(this.options.schema, item);\n return this.batchProvider.push(this.context, validatedItem);\n }\n\n /**\n * Wait for a specific item to be processed and get its result.\n * @param id The item ID returned from push()\n * @returns The processing result\n * @throws If the item doesn't exist or processing failed\n */\n public async wait(id: string): Promise<TResponse> {\n return this.batchProvider.wait(this.context, id);\n }\n\n /**\n * Get the current status of an item.\n * @param id The item ID returned from push()\n * @returns Status information or undefined if item doesn't exist\n */\n public status(\n id: string,\n ):\n | { status: \"pending\" | \"processing\" }\n | { status: \"completed\"; result: TResponse }\n | { status: \"failed\"; error: Error }\n | undefined {\n return this.batchProvider.status(this.context, id);\n }\n\n /**\n * Flush all partitions or a specific partition.\n */\n public async flush(partitionKey?: string): Promise<void> {\n return this.batchProvider.flush(this.context, partitionKey);\n }\n\n /**\n * Clears completed and failed items from memory.\n * Call this periodically in long-running applications to prevent memory leaks.\n *\n * @param status Optional: only clear items with this specific status ('completed' or 'failed')\n * @returns The number of items cleared\n */\n public clearCompleted(status?: \"completed\" | \"failed\"): number {\n return this.batchProvider.clearCompleted(this.context, status);\n }\n\n protected readonly onReady = $hook({\n on: \"ready\",\n handler: async () => {\n await this.batchProvider.markReady(this.context);\n },\n });\n}\n\n$batch[KIND] = BatchPrimitive;\n","import { $module } from \"alepha\";\nimport { $batch } from \"./primitives/$batch.ts\";\nimport { BatchProvider } from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$batch.ts\";\nexport * from \"./providers/BatchProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * | Stability | Since | Runtime |\n * |-----------|-------|---------|\n * | 3 - stable | 0.8.0 | node, bun|\n *\n * Batch accumulation and processing.\n *\n * **Features:**\n * - Batch accumulator with handler\n * - Configurable batch size\n * - Time-based triggers\n * - Status tracking\n *\n * @module alepha.batch\n */\nexport const AlephaBatch = $module({\n name: \"alepha.batch\",\n primitives: [$batch],\n services: [BatchProvider],\n});\n"],"mappings":"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"}
@@ -764,9 +764,9 @@ declare module "alepha" {
764
764
  }
765
765
  }
766
766
  /**
767
- * | type | quality | stability |
768
- * |------|---------|-----------|
769
- * | backend | rare | stable |
767
+ * | Stability | Since | Runtime |
768
+ * |-----------|-------|---------|
769
+ * | 3 - stable | 0.9.0 | node, bun, workerd|
770
770
  *
771
771
  * Unified file storage abstraction across multiple backends.
772
772
  *
@@ -1811,9 +1811,9 @@ var ShellProvider = class {};
1811
1811
  //#endregion
1812
1812
  //#region ../../src/system/index.ts
1813
1813
  /**
1814
- * | type | quality | stability |
1815
- * |------|---------|-----------|
1816
- * | tooling | standard | stable |
1814
+ * | Stability | Since | Runtime |
1815
+ * |-----------|-------|---------|
1816
+ * | 3 - stable | 0.14.0 | node, bun, browser|
1817
1817
  *
1818
1818
  * System-level abstractions for portable code across runtimes.
1819
1819
  *
@@ -2108,9 +2108,9 @@ var LocalFileStorageProvider = class {
2108
2108
  //#endregion
2109
2109
  //#region ../../src/bucket/index.ts
2110
2110
  /**
2111
- * | type | quality | stability |
2112
- * |------|---------|-----------|
2113
- * | backend | rare | stable |
2111
+ * | Stability | Since | Runtime |
2112
+ * |-----------|-------|---------|
2113
+ * | 3 - stable | 0.9.0 | node, bun, workerd|
2114
2114
  *
2115
2115
  * Unified file storage abstraction across multiple backends.
2116
2116
  *