bunderstack 0.17.0-beta.2 → 0.17.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/CHANGELOG.md +233 -0
  2. package/README.md +11 -1
  3. package/dist/access.d.ts +116 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +325 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/api/builder.d.ts +145 -0
  8. package/dist/api/builder.d.ts.map +1 -0
  9. package/dist/api/builder.js +28 -0
  10. package/dist/api/builder.js.map +1 -0
  11. package/dist/api/context.d.ts +34 -0
  12. package/dist/api/context.d.ts.map +1 -0
  13. package/dist/api/context.js +32 -0
  14. package/dist/api/context.js.map +1 -0
  15. package/dist/api/crud-router.d.ts +290 -0
  16. package/dist/api/crud-router.d.ts.map +1 -0
  17. package/dist/api/crud-router.js +249 -0
  18. package/dist/api/crud-router.js.map +1 -0
  19. package/dist/api/openapi.d.ts +6 -0
  20. package/dist/api/openapi.d.ts.map +1 -0
  21. package/dist/api/openapi.js +146 -0
  22. package/dist/api/openapi.js.map +1 -0
  23. package/dist/api/realtime-router.d.ts +76 -0
  24. package/dist/api/realtime-router.d.ts.map +1 -0
  25. package/dist/api/realtime-router.js +41 -0
  26. package/dist/api/realtime-router.js.map +1 -0
  27. package/dist/api/registry.d.ts +30 -0
  28. package/dist/api/registry.d.ts.map +1 -0
  29. package/dist/api/registry.js +239 -0
  30. package/dist/api/registry.js.map +1 -0
  31. package/dist/api/router.d.ts +9 -0
  32. package/dist/api/router.d.ts.map +1 -0
  33. package/dist/api/router.js +19 -0
  34. package/dist/api/router.js.map +1 -0
  35. package/dist/api/storage-router.d.ts +279 -0
  36. package/dist/api/storage-router.d.ts.map +1 -0
  37. package/dist/api/storage-router.js +155 -0
  38. package/dist/api/storage-router.js.map +1 -0
  39. package/dist/api/types.d.ts +55 -0
  40. package/dist/api/types.d.ts.map +1 -0
  41. package/dist/api/types.js +2 -0
  42. package/dist/api/types.js.map +1 -0
  43. package/dist/auth.d.ts +31 -0
  44. package/dist/auth.d.ts.map +1 -0
  45. package/dist/auth.js +102 -0
  46. package/dist/auth.js.map +1 -0
  47. package/dist/blueprint-generator.d.ts +18 -0
  48. package/dist/blueprint-generator.d.ts.map +1 -0
  49. package/dist/blueprint-generator.js +125 -0
  50. package/dist/blueprint-generator.js.map +1 -0
  51. package/dist/blueprint.d.ts +46 -0
  52. package/dist/blueprint.d.ts.map +1 -0
  53. package/dist/blueprint.js +162 -0
  54. package/dist/blueprint.js.map +1 -0
  55. package/dist/cli.d.ts +8 -0
  56. package/dist/cli.d.ts.map +1 -0
  57. package/dist/cli.js +68 -0
  58. package/dist/cli.js.map +1 -0
  59. package/dist/config.d.ts +108 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +90 -0
  62. package/dist/config.js.map +1 -0
  63. package/dist/cron.d.ts +4 -0
  64. package/dist/cron.d.ts.map +1 -0
  65. package/dist/cron.js +3 -0
  66. package/dist/cron.js.map +1 -0
  67. package/dist/crud-operations.d.ts +43 -0
  68. package/dist/crud-operations.d.ts.map +1 -0
  69. package/dist/crud-operations.js +238 -0
  70. package/dist/crud-operations.js.map +1 -0
  71. package/dist/database/adapter.d.ts +20 -0
  72. package/dist/database/adapter.d.ts.map +1 -0
  73. package/dist/database/adapter.js +2 -0
  74. package/dist/database/adapter.js.map +1 -0
  75. package/dist/database/bun-sql.d.ts +3 -0
  76. package/dist/database/bun-sql.d.ts.map +1 -0
  77. package/dist/database/bun-sql.js +21 -0
  78. package/dist/database/bun-sql.js.map +1 -0
  79. package/dist/database/libsql.d.ts +3 -0
  80. package/dist/database/libsql.d.ts.map +1 -0
  81. package/dist/database/libsql.js +22 -0
  82. package/dist/database/libsql.js.map +1 -0
  83. package/dist/database/pglite.d.ts +3 -0
  84. package/dist/database/pglite.d.ts.map +1 -0
  85. package/dist/database/pglite.js +27 -0
  86. package/dist/database/pglite.js.map +1 -0
  87. package/dist/database/postgres-js.d.ts +3 -0
  88. package/dist/database/postgres-js.d.ts.map +1 -0
  89. package/dist/database/postgres-js.js +21 -0
  90. package/dist/database/postgres-js.js.map +1 -0
  91. package/dist/db.d.ts +18 -0
  92. package/dist/db.d.ts.map +1 -0
  93. package/dist/db.js +26 -0
  94. package/dist/db.js.map +1 -0
  95. package/dist/dialect.d.ts +17 -0
  96. package/dist/dialect.d.ts.map +1 -0
  97. package/dist/dialect.js +22 -0
  98. package/dist/dialect.js.map +1 -0
  99. package/dist/email/smtp.d.ts +14 -0
  100. package/dist/email/smtp.d.ts.map +1 -0
  101. package/dist/email/smtp.js +27 -0
  102. package/dist/email/smtp.js.map +1 -0
  103. package/dist/email.d.ts +39 -0
  104. package/dist/email.d.ts.map +1 -0
  105. package/dist/email.js +97 -0
  106. package/dist/email.js.map +1 -0
  107. package/dist/env.d.ts +47 -0
  108. package/dist/env.d.ts.map +1 -0
  109. package/dist/env.js +99 -0
  110. package/dist/env.js.map +1 -0
  111. package/dist/errors.d.ts +125 -0
  112. package/dist/errors.d.ts.map +1 -0
  113. package/dist/errors.js +92 -0
  114. package/dist/errors.js.map +1 -0
  115. package/dist/handler.d.ts +9 -0
  116. package/dist/handler.d.ts.map +1 -0
  117. package/dist/handler.js +22 -0
  118. package/dist/handler.js.map +1 -0
  119. package/dist/idempotency.d.ts +17 -0
  120. package/dist/idempotency.d.ts.map +1 -0
  121. package/dist/idempotency.js +65 -0
  122. package/dist/idempotency.js.map +1 -0
  123. package/dist/index.d.ts +134 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +480 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/internal-tables-pg.d.ts +481 -0
  128. package/dist/internal-tables-pg.d.ts.map +1 -0
  129. package/{src/internal-tables-pg.ts → dist/internal-tables-pg.js} +10 -34
  130. package/dist/internal-tables-pg.js.map +1 -0
  131. package/dist/internal-tables.d.ts +2509 -0
  132. package/dist/internal-tables.d.ts.map +1 -0
  133. package/dist/internal-tables.js +108 -0
  134. package/dist/internal-tables.js.map +1 -0
  135. package/dist/jobs/cron.d.ts +15 -0
  136. package/dist/jobs/cron.d.ts.map +1 -0
  137. package/dist/jobs/cron.js +74 -0
  138. package/dist/jobs/cron.js.map +1 -0
  139. package/dist/jobs/define.d.ts +133 -0
  140. package/dist/jobs/define.d.ts.map +1 -0
  141. package/dist/jobs/define.js +74 -0
  142. package/dist/jobs/define.js.map +1 -0
  143. package/dist/jobs/index.d.ts +11 -0
  144. package/dist/jobs/index.d.ts.map +1 -0
  145. package/dist/jobs/index.js +8 -0
  146. package/dist/jobs/index.js.map +1 -0
  147. package/dist/jobs/queue.d.ts +6 -0
  148. package/dist/jobs/queue.d.ts.map +1 -0
  149. package/dist/jobs/queue.js +55 -0
  150. package/dist/jobs/queue.js.map +1 -0
  151. package/dist/jobs/runtime.d.ts +13 -0
  152. package/dist/jobs/runtime.d.ts.map +1 -0
  153. package/dist/jobs/runtime.js +46 -0
  154. package/dist/jobs/runtime.js.map +1 -0
  155. package/dist/jobs/slots.d.ts +25 -0
  156. package/dist/jobs/slots.d.ts.map +1 -0
  157. package/dist/jobs/slots.js +41 -0
  158. package/dist/jobs/slots.js.map +1 -0
  159. package/dist/jobs/worker.d.ts +12 -0
  160. package/dist/jobs/worker.d.ts.map +1 -0
  161. package/dist/jobs/worker.js +311 -0
  162. package/dist/jobs/worker.js.map +1 -0
  163. package/dist/lifecycle.d.ts +10 -0
  164. package/dist/lifecycle.d.ts.map +1 -0
  165. package/dist/lifecycle.js +37 -0
  166. package/dist/lifecycle.js.map +1 -0
  167. package/dist/list-query.d.ts +62 -0
  168. package/dist/list-query.d.ts.map +1 -0
  169. package/dist/list-query.js +194 -0
  170. package/dist/list-query.js.map +1 -0
  171. package/dist/manifest.d.ts +59 -0
  172. package/dist/manifest.d.ts.map +1 -0
  173. package/dist/manifest.js +167 -0
  174. package/dist/manifest.js.map +1 -0
  175. package/dist/provision-internals.d.ts +24 -0
  176. package/dist/provision-internals.d.ts.map +1 -0
  177. package/dist/provision-internals.js +7 -0
  178. package/dist/provision-internals.js.map +1 -0
  179. package/dist/provision.d.ts +23 -0
  180. package/dist/provision.d.ts.map +1 -0
  181. package/dist/provision.js +96 -0
  182. package/dist/provision.js.map +1 -0
  183. package/dist/rate-limit.d.ts +7 -0
  184. package/dist/rate-limit.d.ts.map +1 -0
  185. package/dist/rate-limit.js +49 -0
  186. package/dist/rate-limit.js.map +1 -0
  187. package/dist/realtime/facade.d.ts +18 -0
  188. package/dist/realtime/facade.d.ts.map +1 -0
  189. package/dist/realtime/facade.js +36 -0
  190. package/dist/realtime/facade.js.map +1 -0
  191. package/dist/realtime/filter.d.ts +13 -0
  192. package/dist/realtime/filter.d.ts.map +1 -0
  193. package/dist/realtime/filter.js +48 -0
  194. package/dist/realtime/filter.js.map +1 -0
  195. package/dist/realtime/heartbeat.d.ts +14 -0
  196. package/dist/realtime/heartbeat.d.ts.map +1 -0
  197. package/dist/realtime/heartbeat.js +64 -0
  198. package/dist/realtime/heartbeat.js.map +1 -0
  199. package/dist/realtime/publisher.d.ts +21 -0
  200. package/dist/realtime/publisher.d.ts.map +1 -0
  201. package/dist/realtime/publisher.js +17 -0
  202. package/dist/realtime/publisher.js.map +1 -0
  203. package/dist/schema-export-pg.d.ts +2 -0
  204. package/dist/schema-export-pg.d.ts.map +1 -0
  205. package/dist/schema-export-pg.js +4 -0
  206. package/dist/schema-export-pg.js.map +1 -0
  207. package/dist/schema-export.d.ts +2 -0
  208. package/dist/schema-export.d.ts.map +1 -0
  209. package/dist/schema-export.js +2 -0
  210. package/dist/schema-export.js.map +1 -0
  211. package/dist/scope.d.ts +4 -0
  212. package/dist/scope.d.ts.map +1 -0
  213. package/dist/scope.js +13 -0
  214. package/dist/scope.js.map +1 -0
  215. package/dist/standard-schema.d.ts +13 -0
  216. package/dist/standard-schema.d.ts.map +1 -0
  217. package/dist/standard-schema.js +34 -0
  218. package/dist/standard-schema.js.map +1 -0
  219. package/dist/storage/buckets.d.ts +80 -0
  220. package/dist/storage/buckets.d.ts.map +1 -0
  221. package/dist/storage/buckets.js +178 -0
  222. package/dist/storage/buckets.js.map +1 -0
  223. package/dist/storage/delete.d.ts +11 -0
  224. package/dist/storage/delete.d.ts.map +1 -0
  225. package/dist/storage/delete.js +18 -0
  226. package/dist/storage/delete.js.map +1 -0
  227. package/dist/storage/file-meta.d.ts +37 -0
  228. package/dist/storage/file-meta.d.ts.map +1 -0
  229. package/dist/storage/file-meta.js +166 -0
  230. package/dist/storage/file-meta.js.map +1 -0
  231. package/dist/storage/index.d.ts +28 -0
  232. package/dist/storage/index.d.ts.map +1 -0
  233. package/dist/storage/index.js +5 -0
  234. package/dist/storage/index.js.map +1 -0
  235. package/dist/storage/local.d.ts +15 -0
  236. package/dist/storage/local.d.ts.map +1 -0
  237. package/dist/storage/local.js +56 -0
  238. package/dist/storage/local.js.map +1 -0
  239. package/dist/storage/operations.d.ts +54 -0
  240. package/dist/storage/operations.d.ts.map +1 -0
  241. package/dist/storage/operations.js +250 -0
  242. package/dist/storage/operations.js.map +1 -0
  243. package/dist/storage/registry.d.ts +10 -0
  244. package/dist/storage/registry.d.ts.map +1 -0
  245. package/dist/storage/registry.js +23 -0
  246. package/dist/storage/registry.js.map +1 -0
  247. package/dist/storage/s3.d.ts +28 -0
  248. package/dist/storage/s3.d.ts.map +1 -0
  249. package/dist/storage/s3.js +71 -0
  250. package/dist/storage/s3.js.map +1 -0
  251. package/dist/storage/sweep.d.ts +9 -0
  252. package/dist/storage/sweep.d.ts.map +1 -0
  253. package/dist/storage/sweep.js +17 -0
  254. package/dist/storage/sweep.js.map +1 -0
  255. package/dist/storage/thumbnails.d.ts +11 -0
  256. package/dist/storage/thumbnails.d.ts.map +1 -0
  257. package/dist/storage/thumbnails.js +60 -0
  258. package/dist/storage/thumbnails.js.map +1 -0
  259. package/dist/testing.d.ts +27 -0
  260. package/dist/testing.d.ts.map +1 -0
  261. package/dist/testing.js +11 -0
  262. package/dist/testing.js.map +1 -0
  263. package/dist/typeid-pg.d.ts +19 -0
  264. package/dist/typeid-pg.d.ts.map +1 -0
  265. package/{src/typeid-pg.ts → dist/typeid-pg.js} +10 -13
  266. package/dist/typeid-pg.js.map +1 -0
  267. package/dist/typeid.d.ts +45 -0
  268. package/dist/typeid.d.ts.map +1 -0
  269. package/dist/typeid.js +100 -0
  270. package/dist/typeid.js.map +1 -0
  271. package/package.json +77 -28
  272. package/src/access.ts +0 -532
  273. package/src/api/builder.ts +0 -52
  274. package/src/api/context.ts +0 -83
  275. package/src/api/crud-router.ts +0 -386
  276. package/src/api/openapi.ts +0 -184
  277. package/src/api/realtime-router.ts +0 -75
  278. package/src/api/registry.ts +0 -338
  279. package/src/api/router.ts +0 -34
  280. package/src/api/storage-router.ts +0 -224
  281. package/src/api/types.ts +0 -105
  282. package/src/auth.ts +0 -117
  283. package/src/blueprint-generator.ts +0 -203
  284. package/src/blueprint.ts +0 -264
  285. package/src/cli.ts +0 -90
  286. package/src/config.ts +0 -215
  287. package/src/cron.ts +0 -3
  288. package/src/crud-operations.ts +0 -478
  289. package/src/database/adapter.ts +0 -26
  290. package/src/database/bun-sql.ts +0 -23
  291. package/src/database/libsql.ts +0 -28
  292. package/src/database/pglite.ts +0 -33
  293. package/src/database/postgres-js.ts +0 -33
  294. package/src/db.ts +0 -61
  295. package/src/dialect.ts +0 -38
  296. package/src/email/smtp.ts +0 -45
  297. package/src/email.ts +0 -154
  298. package/src/env.ts +0 -180
  299. package/src/errors.ts +0 -123
  300. package/src/handler.ts +0 -30
  301. package/src/idempotency.ts +0 -97
  302. package/src/index.ts +0 -834
  303. package/src/internal-tables.ts +0 -149
  304. package/src/jobs/cron.ts +0 -87
  305. package/src/jobs/define.ts +0 -283
  306. package/src/jobs/index.ts +0 -37
  307. package/src/jobs/queue.ts +0 -67
  308. package/src/jobs/runtime.ts +0 -58
  309. package/src/jobs/slots.ts +0 -52
  310. package/src/jobs/worker.ts +0 -369
  311. package/src/lifecycle.ts +0 -49
  312. package/src/list-query.ts +0 -333
  313. package/src/manifest.ts +0 -297
  314. package/src/provision-internals.ts +0 -29
  315. package/src/provision.ts +0 -128
  316. package/src/rate-limit.ts +0 -71
  317. package/src/realtime/facade.ts +0 -74
  318. package/src/realtime/filter.ts +0 -81
  319. package/src/realtime/heartbeat.ts +0 -80
  320. package/src/realtime/publisher.ts +0 -46
  321. package/src/schema-export-pg.ts +0 -7
  322. package/src/schema-export.ts +0 -5
  323. package/src/scope.ts +0 -17
  324. package/src/standard-schema.ts +0 -59
  325. package/src/storage/buckets.ts +0 -308
  326. package/src/storage/delete.ts +0 -26
  327. package/src/storage/file-meta.ts +0 -222
  328. package/src/storage/index.ts +0 -39
  329. package/src/storage/local.ts +0 -69
  330. package/src/storage/operations.ts +0 -398
  331. package/src/storage/registry.ts +0 -40
  332. package/src/storage/s3.ts +0 -99
  333. package/src/storage/sweep.ts +0 -25
  334. package/src/storage/thumbnails.ts +0 -65
  335. package/src/testing.ts +0 -31
  336. package/src/typeid.ts +0 -116
package/src/jobs/slots.ts DELETED
@@ -1,52 +0,0 @@
1
- // src/jobs/slots.ts — cron slot enumeration. Pure; no db, no clock reads.
2
- import { cronMatches, type ParsedCron } from './cron'
3
-
4
- /** Slot granularity. Every slot timestamp satisfies `slot % SLOT_MS === 0`. */
5
- export const SLOT_MS = 60_000
6
-
7
- /** Reserved job-type prefix for cron occurrences. */
8
- export const CRON_PREFIX = 'cron:'
9
-
10
- /** How far back either catch-up mode will look. */
11
- export const DEFAULT_CATCH_UP_WINDOW_MS = 60 * SLOT_MS
12
-
13
- export type CatchUp = 'latest' | 'all'
14
-
15
- /** Aligns `ms` down to its containing slot. */
16
- export function floorSlot(ms: number): number {
17
- return Math.floor(ms / SLOT_MS) * SLOT_MS
18
- }
19
-
20
- /**
21
- * Slots matching `cron` in the half-open range `(from, to]`, oldest first.
22
- *
23
- * `from` is exclusive so a stored watermark is never re-emitted. Both modes are
24
- * clamped to `catchUpWindowMs` — without it a watermark far in the past would
25
- * make this iterate unbounded minutes.
26
- */
27
- export function slotsDue(args: {
28
- cron: ParsedCron
29
- from: number
30
- to: number
31
- catchUp?: CatchUp
32
- catchUpWindowMs?: number
33
- }): number[] {
34
- const catchUp = args.catchUp ?? 'latest'
35
- const windowMs = args.catchUpWindowMs ?? DEFAULT_CATCH_UP_WINDOW_MS
36
- const to = floorSlot(args.to)
37
- const from = Math.max(floorSlot(args.from), to - windowMs)
38
- if (to <= from) return []
39
-
40
- if (catchUp === 'latest') {
41
- for (let s = to; s > from; s -= SLOT_MS) {
42
- if (cronMatches(args.cron, s)) return [s]
43
- }
44
- return []
45
- }
46
-
47
- const slots: number[] = []
48
- for (let s = from + SLOT_MS; s <= to; s += SLOT_MS) {
49
- if (cronMatches(args.cron, s)) slots.push(s)
50
- }
51
- return slots
52
- }
@@ -1,369 +0,0 @@
1
- // src/jobs/worker.ts — the queue worker. One `tick()` is a full cycle:
2
- // recover expired leases → reap old succeeded rows → claim and run queue jobs.
3
- import { and, eq, inArray, is, isNotNull, lt, lte, max, sql } from 'drizzle-orm'
4
- import { PgDatabase } from 'drizzle-orm/pg-core'
5
-
6
- import type { AnyDb } from '../dialect'
7
- import type {
8
- AnyBackgroundDefinition,
9
- JobsDefs,
10
- JobsRuntimeFacade,
11
- TickResult,
12
- } from './define'
13
-
14
- import { jobsTableFor } from '../internal-tables'
15
- import { validateStandardSchema } from '../standard-schema'
16
- import { parseCron } from './cron'
17
- import { backoffMs, DEFAULT_RETRIES, DEFAULT_TIMEOUT_MS } from './define'
18
- import { enqueueJob } from './queue'
19
- import { CRON_PREFIX, floorSlot, slotsDue, SLOT_MS } from './slots'
20
-
21
- const CLAIM_BATCH = 10
22
- const SUCCEEDED_RETENTION_MS = 24 * 60 * 60 * 1000
23
- const REAP_INTERVAL_MS = 60 * 60_000
24
-
25
- type JobRow = {
26
- id: string
27
- type: string
28
- payloadJson: string
29
- attempts: number
30
- runAt: number
31
- }
32
-
33
- function toError(err: unknown): Error {
34
- return err instanceof Error ? err : new Error(String(err))
35
- }
36
-
37
- function maxAttempts(def: AnyBackgroundDefinition): number {
38
- return 1 + (def.retries ?? DEFAULT_RETRIES)
39
- }
40
-
41
- /** Resolve a stored row type back to its definition. Cron rows carry the
42
- * reserved prefix; queue rows use the definition key verbatim. */
43
- function definitionFor(
44
- defs: JobsDefs,
45
- type: string,
46
- ): AnyBackgroundDefinition | undefined {
47
- if (type.startsWith(CRON_PREFIX)) {
48
- const def = defs[type.slice(CRON_PREFIX.length)]
49
- return def?.kind === 'cron' ? def : undefined
50
- }
51
- const def = defs[type]
52
- return def?.kind === 'job' ? def : undefined
53
- }
54
-
55
- /** Terminal queue rows release their dedupe key. */
56
- function terminalPatch() {
57
- return { dedupeKey: null }
58
- }
59
-
60
- export function createJobRunner(deps: {
61
- db: AnyDb
62
- defs: JobsDefs
63
- /** Handler ctx WITHOUT `jobs`; the facade is injected via setJobsFacade. */
64
- ctx: Record<string, unknown>
65
- }) {
66
- const { db, defs } = deps
67
- const t = jobsTableFor(db)
68
- const ctx = { ...deps.ctx } as Record<string, unknown>
69
- let lastReapAt = 0
70
-
71
- /** Cron rows carry no payload — their handler input is the slot itself. */
72
- function resolveInput(def: AnyBackgroundDefinition, row: JobRow): unknown {
73
- if (def.kind === 'cron') {
74
- return { scheduledFor: new Date(Number(row.runAt)) }
75
- }
76
- const raw = JSON.parse(row.payloadJson)
77
- return def.input
78
- ? validateStandardSchema(def.input, raw, 'job payload')
79
- : undefined
80
- }
81
-
82
- /**
83
- * The watermark is the newest slot we already stored for this cron. When no
84
- * rows exist — a newly declared cron, or one whose rows were reaped — anchor
85
- * one slot before now so the current minute is eligible and nothing older is.
86
- */
87
- async function cronWatermark(type: string, now: number): Promise<number> {
88
- const rows = await db
89
- .select({ latest: max(t.runAt) })
90
- .from(t)
91
- .where(eq(t.type, type))
92
- const latest = rows[0]?.latest
93
- return latest == null ? floorSlot(now) - SLOT_MS : Number(latest)
94
- }
95
-
96
- /** Enqueue a row per due slot. The unique(type, dedupeKey) constraint makes
97
- * this safe to run concurrently in any number of processes. */
98
- async function materializeCronSlots(now: number) {
99
- for (const [name, def] of Object.entries(defs)) {
100
- if (def.kind !== 'cron') continue
101
- const type = `${CRON_PREFIX}${name}`
102
- const from = await cronWatermark(type, now)
103
- const slots = slotsDue({
104
- cron: parseCron(def.schedule),
105
- from,
106
- to: now,
107
- catchUp: def.catchUp,
108
- catchUpWindowMs: def.catchUpWindow,
109
- })
110
- for (const slot of slots) {
111
- await enqueueJob(db, defs, name, null, {
112
- runAt: slot,
113
- dedupeKey: String(slot),
114
- })
115
- }
116
- }
117
- }
118
-
119
- async function fireOnFailed(
120
- def: AnyBackgroundDefinition,
121
- input: unknown,
122
- error: Error,
123
- ) {
124
- if (!def.onFailed) return
125
- try {
126
- await (def.onFailed as (i: unknown, e: Error, c: unknown) => unknown)(
127
- input,
128
- error,
129
- ctx,
130
- )
131
- } catch (hookErr) {
132
- console.error('[bunderstack] onFailed hook threw:', hookErr)
133
- }
134
- }
135
-
136
- /** running rows whose lease expired → pending (or failed when exhausted). */
137
- async function recoverExpiredLeases(now: number) {
138
- const expired: (JobRow & { lastError: string | null })[] = await db
139
- .select({
140
- id: t.id,
141
- type: t.type,
142
- payloadJson: t.payloadJson,
143
- attempts: t.attempts,
144
- lastError: t.lastError,
145
- runAt: t.runAt,
146
- })
147
- .from(t)
148
- .where(
149
- and(
150
- eq(t.status, 'running'),
151
- isNotNull(t.lockedUntil),
152
- lt(t.lockedUntil, now),
153
- ),
154
- )
155
- for (const row of expired) {
156
- const def = definitionFor(defs, row.type)
157
- const error = new Error('lease expired (worker crashed or timed out)')
158
- if (!def) {
159
- await db
160
- .update(t)
161
- .set({
162
- status: 'failed',
163
- finishedAt: now,
164
- lockedUntil: null,
165
- lastError: `unknown job type "${row.type}"`,
166
- dedupeKey: null,
167
- })
168
- .where(eq(t.id, row.id))
169
- continue
170
- }
171
- if (Number(row.attempts) >= maxAttempts(def)) {
172
- await db
173
- .update(t)
174
- .set({
175
- status: 'failed',
176
- finishedAt: now,
177
- lockedUntil: null,
178
- lastError: error.message,
179
- ...terminalPatch(),
180
- })
181
- .where(eq(t.id, row.id))
182
- await fireOnFailed(def, resolveInput(def, row), error)
183
- } else {
184
- await db
185
- .update(t)
186
- .set({
187
- status: 'pending',
188
- lockedUntil: null,
189
- runAt: now + backoffMs(def, Number(row.attempts)),
190
- lastError: error.message,
191
- })
192
- .where(eq(t.id, row.id))
193
- }
194
- }
195
- }
196
-
197
- async function reapSucceeded(now: number) {
198
- await db
199
- .delete(t)
200
- .where(
201
- and(
202
- eq(t.status, 'succeeded'),
203
- lt(t.finishedAt, now - SUCCEEDED_RETENTION_MS),
204
- ),
205
- )
206
- }
207
-
208
- /** Atomically claim up to `limit` runnable jobs of one type. */
209
- async function claim(
210
- type: string,
211
- limit: number,
212
- now: number,
213
- leaseUntil: number,
214
- ): Promise<JobRow[]> {
215
- const pendingIds = db
216
- .select({ id: t.id })
217
- .from(t)
218
- .where(and(eq(t.type, type), eq(t.status, 'pending'), lte(t.runAt, now)))
219
- .orderBy(t.runAt)
220
- .limit(limit)
221
- // PG: lock the selected rows so concurrent replicas skip them. SQLite's
222
- // single-writer model makes the one-statement UPDATE atomic on its own.
223
- const sub = is(db, PgDatabase)
224
- ? (
225
- pendingIds as unknown as {
226
- for: (m: string, o: object) => typeof pendingIds
227
- }
228
- ).for('update', { skipLocked: true })
229
- : pendingIds
230
- const rows: JobRow[] = await db
231
- .update(t)
232
- .set({
233
- status: 'running',
234
- lockedUntil: leaseUntil,
235
- attempts: sql`${t.attempts} + 1`,
236
- })
237
- .where(and(inArray(t.id, sub), eq(t.status, 'pending')))
238
- .returning({
239
- id: t.id,
240
- type: t.type,
241
- payloadJson: t.payloadJson,
242
- attempts: t.attempts,
243
- runAt: t.runAt,
244
- })
245
- return rows
246
- }
247
-
248
- // `now` is the tick's injected clock: retry runAt math uses it so tests can
249
- // drive backoff deterministically. finishedAt uses the real clock (a handler
250
- // may run long past the tick's start).
251
- async function runJob(
252
- row: JobRow,
253
- def: AnyBackgroundDefinition,
254
- now: number,
255
- leaseUntil: number,
256
- ): Promise<'ran' | 'failed' | 'lost'> {
257
- let input: unknown
258
- try {
259
- input = resolveInput(def, row)
260
- } catch (err) {
261
- // Stored payload no longer parses (schema drift): retrying can't help.
262
- const e = toError(err)
263
- const updated = await db
264
- .update(t)
265
- .set({
266
- status: 'failed',
267
- finishedAt: Date.now(),
268
- lockedUntil: null,
269
- lastError: e.message,
270
- ...terminalPatch(),
271
- })
272
- .where(and(eq(t.id, row.id), eq(t.lockedUntil, leaseUntil)))
273
- .returning({ id: t.id })
274
- if (!updated[0]) return 'lost'
275
- await fireOnFailed(def, undefined, e)
276
- return 'failed'
277
- }
278
- try {
279
- await (def.handler as (i: unknown, c: unknown) => unknown)(input, ctx)
280
- const updated = await db
281
- .update(t)
282
- .set({
283
- status: 'succeeded',
284
- finishedAt: Date.now(),
285
- lockedUntil: null,
286
- ...terminalPatch(),
287
- })
288
- .where(and(eq(t.id, row.id), eq(t.lockedUntil, leaseUntil)))
289
- .returning({ id: t.id })
290
- if (!updated[0]) return 'lost'
291
- return 'ran'
292
- } catch (err) {
293
- const e = toError(err)
294
- if (Number(row.attempts) < maxAttempts(def)) {
295
- const updated = await db
296
- .update(t)
297
- .set({
298
- status: 'pending',
299
- lockedUntil: null,
300
- runAt: now + backoffMs(def, Number(row.attempts)),
301
- lastError: e.message,
302
- })
303
- .where(and(eq(t.id, row.id), eq(t.lockedUntil, leaseUntil)))
304
- .returning({ id: t.id })
305
- if (!updated[0]) return 'lost'
306
- } else {
307
- const updated = await db
308
- .update(t)
309
- .set({
310
- status: 'failed',
311
- finishedAt: Date.now(),
312
- lockedUntil: null,
313
- lastError: e.message,
314
- ...terminalPatch(),
315
- })
316
- .where(and(eq(t.id, row.id), eq(t.lockedUntil, leaseUntil)))
317
- .returning({ id: t.id })
318
- if (!updated[0]) return 'lost'
319
- await fireOnFailed(def, input, e)
320
- }
321
- return 'failed'
322
- }
323
- }
324
-
325
- async function runClaimable(now: number): Promise<TickResult> {
326
- const work: Promise<'ran' | 'failed' | 'lost'>[] = []
327
- let totalClaimed = 0
328
- for (const [name, def] of Object.entries(defs)) {
329
- const type = def.kind === 'cron' ? `${CRON_PREFIX}${name}` : name
330
- let limit = CLAIM_BATCH
331
- if (def.kind === 'job' && def.concurrency !== undefined) {
332
- const runningRows = await db
333
- .select({ id: t.id })
334
- .from(t)
335
- .where(and(eq(t.type, type), eq(t.status, 'running')))
336
- const capacity = def.concurrency - runningRows.length
337
- if (capacity <= 0) continue
338
- limit = Math.min(limit, capacity)
339
- }
340
- const leaseUntil = now + (def.timeout ?? DEFAULT_TIMEOUT_MS)
341
- const claimed = await claim(type, limit, now, leaseUntil)
342
- totalClaimed += claimed.length
343
- for (const row of claimed) work.push(runJob(row, def, now, leaseUntil))
344
- }
345
- const outcomes = await Promise.all(work)
346
- let ran = 0
347
- let failed = 0
348
- for (const outcome of outcomes) {
349
- if (outcome === 'ran') ran++
350
- else if (outcome === 'failed') failed++
351
- }
352
- return { claimed: totalClaimed, ran, failed }
353
- }
354
-
355
- return {
356
- async tick(now: number = Date.now()): Promise<TickResult> {
357
- await materializeCronSlots(now)
358
- await recoverExpiredLeases(now)
359
- if (now - lastReapAt >= REAP_INTERVAL_MS) {
360
- lastReapAt = now
361
- await reapSucceeded(now)
362
- }
363
- return runClaimable(now)
364
- },
365
- setJobsFacade(f: JobsRuntimeFacade) {
366
- ctx.jobs = f
367
- },
368
- }
369
- }
package/src/lifecycle.ts DELETED
@@ -1,49 +0,0 @@
1
- export type Cleanup = () => void | Promise<void>
2
- export type LifecycleStatus = 'ready' | 'closing' | 'closed'
3
-
4
- export class Lifecycle {
5
- #controller = new AbortController()
6
- #status: LifecycleStatus = 'ready'
7
- #cleanups = new Set<Cleanup>()
8
- #closePromise: Promise<void> | undefined
9
-
10
- get signal(): AbortSignal {
11
- return this.#controller.signal
12
- }
13
-
14
- get status(): LifecycleStatus {
15
- return this.#status
16
- }
17
-
18
- add(cleanup: Cleanup): () => void {
19
- if (this.#status !== 'ready') {
20
- throw new Error('[bunderstack] application lifecycle is closed')
21
- }
22
- this.#cleanups.add(cleanup)
23
- return () => this.#cleanups.delete(cleanup)
24
- }
25
-
26
- close(): Promise<void> {
27
- if (this.#closePromise) return this.#closePromise
28
- this.#status = 'closing'
29
- this.#controller.abort()
30
- this.#closePromise = (async () => {
31
- const cleanups = [...this.#cleanups].reverse()
32
- this.#cleanups.clear()
33
- const results = await Promise.allSettled(
34
- cleanups.map(async (cleanup) => cleanup()),
35
- )
36
- this.#status = 'closed'
37
- const errors = results.flatMap((result) =>
38
- result.status === 'rejected' ? [result.reason] : [],
39
- )
40
- if (errors.length > 0) {
41
- throw new AggregateError(
42
- errors,
43
- '[bunderstack] lifecycle cleanup failed',
44
- )
45
- }
46
- })()
47
- return this.#closePromise
48
- }
49
- }