bunderstack 0.17.0-beta.2 → 0.17.0-beta.3

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 +227 -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 +76 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +81 -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 +477 -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
@@ -1,149 +0,0 @@
1
- import { getTableName, is, isTable } from 'drizzle-orm'
2
- import { PgDatabase } from 'drizzle-orm/pg-core'
3
- import {
4
- index,
5
- integer,
6
- primaryKey,
7
- sqliteTable,
8
- text,
9
- uniqueIndex,
10
- } from 'drizzle-orm/sqlite-core'
11
-
12
- import { detectDialect } from './dialect'
13
- import {
14
- bunderstackFilesPg,
15
- bunderstackIdempotencyPg,
16
- bunderstackJobsPg,
17
- } from './internal-tables-pg'
18
-
19
- export const bunderstackFiles = sqliteTable(
20
- 'bunderstack_file_meta',
21
- {
22
- fileId: text('file_id').primaryKey(),
23
- bucket: text('bucket').notNull(),
24
- ownerId: text('owner_id'),
25
- scopeJson: text('scope_json'),
26
- status: text('status').notNull(),
27
- filename: text('filename'),
28
- contentType: text('content_type'),
29
- size: integer('size'),
30
- createdAt: integer('created_at').notNull(),
31
- confirmedAt: integer('confirmed_at'),
32
- },
33
- (t) => [
34
- index('bfm_owner').on(t.ownerId),
35
- index('bfm_scope').on(t.bucket, t.scopeJson),
36
- index('bfm_sweep').on(t.status, t.createdAt),
37
- ],
38
- )
39
-
40
- export const bunderstackIdempotency = sqliteTable(
41
- '_bunderstack_idempotency',
42
- {
43
- key: text('key').notNull(),
44
- tableName: text('table_name').notNull(),
45
- bodyHash: text('body_hash').notNull(),
46
- status: integer('status').notNull(),
47
- response: text('response').notNull(),
48
- expiresAt: integer('expires_at').notNull(),
49
- },
50
- (t) => [primaryKey({ columns: [t.key, t.tableName] })],
51
- )
52
-
53
- export const bunderstackJobs = sqliteTable(
54
- '_bunderstack_jobs',
55
- {
56
- id: text('id').primaryKey(),
57
- type: text('type').notNull(),
58
- payloadJson: text('payload_json').notNull(),
59
- status: text('status').notNull(), // pending | running | succeeded | failed
60
- attempts: integer('attempts').notNull().default(0),
61
- runAt: integer('run_at').notNull(),
62
- lockedUntil: integer('locked_until'),
63
- dedupeKey: text('dedupe_key'),
64
- lastError: text('last_error'),
65
- createdAt: integer('created_at').notNull(),
66
- finishedAt: integer('finished_at'),
67
- },
68
- (t) => [
69
- index('bjq_claim').on(t.status, t.runAt),
70
- index('bjq_type_status').on(t.type, t.status),
71
- // NULL dedupe keys are distinct in both dialects, so keyless jobs never collide.
72
- uniqueIndex('bjq_dedupe').on(t.type, t.dedupeKey),
73
- ],
74
- )
75
-
76
- export const INTERNAL_TABLES = {
77
- bunderstackFiles,
78
- bunderstackIdempotency,
79
- bunderstackJobs,
80
- } as const
81
-
82
- export const INTERNAL_TABLE_NAMES: ReadonlySet<string> = new Set([
83
- 'bunderstack_file_meta',
84
- '_bunderstack_idempotency',
85
- '_bunderstack_jobs',
86
- ])
87
-
88
- export const INTERNAL_TABLES_PG = {
89
- bunderstackFiles: bunderstackFilesPg,
90
- bunderstackIdempotency: bunderstackIdempotencyPg,
91
- bunderstackJobs: bunderstackJobsPg,
92
- } as const
93
-
94
- // Both dialect twins count as "ours" for the re-export identity check.
95
- const INTERNAL_TABLE_CANDIDATES = new Map<string, readonly unknown[]>([
96
- [getTableName(bunderstackFiles), [bunderstackFiles, bunderstackFilesPg]],
97
- [
98
- getTableName(bunderstackIdempotency),
99
- [bunderstackIdempotency, bunderstackIdempotencyPg],
100
- ],
101
- [getTableName(bunderstackJobs), [bunderstackJobs, bunderstackJobsPg]],
102
- ])
103
-
104
- /** Internal file-meta table matching the db's dialect. */
105
- export function filesTableFor(db: unknown) {
106
- return is(db, PgDatabase) ? bunderstackFilesPg : bunderstackFiles
107
- }
108
-
109
- /** Internal idempotency table matching the db's dialect. */
110
- export function idempotencyTableFor(db: unknown) {
111
- return is(db, PgDatabase) ? bunderstackIdempotencyPg : bunderstackIdempotency
112
- }
113
-
114
- /** Internal jobs table matching the db's dialect. */
115
- export function jobsTableFor(db: unknown) {
116
- return is(db, PgDatabase) ? bunderstackJobsPg : bunderstackJobs
117
- }
118
-
119
- export function withInternalTables<TSchema extends Record<string, unknown>>(
120
- schema: TSchema,
121
- ): TSchema & typeof INTERNAL_TABLES {
122
- const merged = { ...schema } as TSchema & typeof INTERNAL_TABLES
123
-
124
- for (const value of Object.values(schema)) {
125
- if (!isTable(value)) continue
126
- const name = getTableName(value)
127
- if (!INTERNAL_TABLE_NAMES.has(name)) continue
128
-
129
- const candidates = INTERNAL_TABLE_CANDIDATES.get(name)
130
- if (candidates?.includes(value)) {
131
- // Re-exported from bunderstack/schema(-pg) — already in user schema.
132
- continue
133
- }
134
-
135
- throw new Error(
136
- `[bunderstack] table name "${name}" is reserved by bunderstack`,
137
- )
138
- }
139
-
140
- const internal =
141
- detectDialect(schema) === 'pg' ? INTERNAL_TABLES_PG : INTERNAL_TABLES
142
- for (const [key, table] of Object.entries(internal)) {
143
- if (!(key in merged)) {
144
- ;(merged as Record<string, unknown>)[key] = table
145
- }
146
- }
147
-
148
- return merged
149
- }
package/src/jobs/cron.ts DELETED
@@ -1,87 +0,0 @@
1
- // src/jobs/cron.ts — minimal 5-field cron parser, UTC, minute granularity.
2
- // Supports: * , lists (a,b) , ranges (a-b) , steps (*/n, a-b/n, a/n). No
3
- // month/day names, no seconds field, no @-shortcuts — YAGNI for v1.
4
-
5
- export type CronField = { any: boolean; values: ReadonlySet<number> }
6
-
7
- export type ParsedCron = {
8
- minute: CronField
9
- hour: CronField
10
- dayOfMonth: CronField
11
- month: CronField
12
- dayOfWeek: CronField
13
- }
14
-
15
- const PART_RE = /^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/
16
-
17
- function parseField(
18
- spec: string,
19
- min: number,
20
- max: number,
21
- expr: string,
22
- ): CronField {
23
- if (spec === '*') return { any: true, values: new Set() }
24
- const values = new Set<number>()
25
- for (const part of spec.split(',')) {
26
- const m = PART_RE.exec(part)
27
- if (!m) throw new Error(`[bunderstack] invalid cron "${expr}": "${part}"`)
28
- const step = m[2] !== undefined ? Number(m[2]) : 1
29
- let lo: number
30
- let hi: number
31
- if (m[1] === '*') {
32
- lo = min
33
- hi = max
34
- } else if (m[1]!.includes('-')) {
35
- const [a, b] = m[1]!.split('-')
36
- lo = Number(a)
37
- hi = Number(b)
38
- } else {
39
- lo = Number(m[1])
40
- // "5/15" means "starting at 5, every 15" per cron convention.
41
- hi = step > 1 ? max : lo
42
- }
43
- if (step < 1 || lo < min || hi > max || lo > hi) {
44
- throw new Error(`[bunderstack] invalid cron "${expr}": "${part}"`)
45
- }
46
- for (let v = lo; v <= hi; v += step) values.add(v)
47
- }
48
- return { any: false, values }
49
- }
50
-
51
- export function parseCron(expr: string): ParsedCron {
52
- const parts = expr.trim().split(/\s+/)
53
- if (parts.length !== 5) {
54
- throw new Error(
55
- `[bunderstack] invalid cron "${expr}": expected 5 fields (minute hour day-of-month month day-of-week)`,
56
- )
57
- }
58
- const dow = parseField(parts[4]!, 0, 7, expr)
59
- return {
60
- minute: parseField(parts[0]!, 0, 59, expr),
61
- hour: parseField(parts[1]!, 0, 23, expr),
62
- dayOfMonth: parseField(parts[2]!, 1, 31, expr),
63
- month: parseField(parts[3]!, 1, 12, expr),
64
- // 7 is an alias for Sunday (0).
65
- dayOfWeek: dow.any
66
- ? dow
67
- : { any: false, values: new Set([...dow.values].map((v) => v % 7)) },
68
- }
69
- }
70
-
71
- function inField(field: CronField, value: number): boolean {
72
- return field.any || field.values.has(value)
73
- }
74
-
75
- /** Whether the minute containing `epochMs` matches, evaluated in UTC. */
76
- export function cronMatches(cron: ParsedCron, epochMs: number): boolean {
77
- const d = new Date(epochMs)
78
- if (!inField(cron.minute, d.getUTCMinutes())) return false
79
- if (!inField(cron.hour, d.getUTCHours())) return false
80
- if (!inField(cron.month, d.getUTCMonth() + 1)) return false
81
- const domOk = inField(cron.dayOfMonth, d.getUTCDate())
82
- const dowOk = inField(cron.dayOfWeek, d.getUTCDay())
83
- // Standard cron rule: when BOTH day fields are restricted, either may match.
84
- return !cron.dayOfMonth.any && !cron.dayOfWeek.any
85
- ? domOk || dowOk
86
- : domOk && dowOk
87
- }
@@ -1,283 +0,0 @@
1
- // src/jobs/define.ts — job definition types and the typed builder.
2
- // `createJobsBuilder` exists purely to carry
3
- // TSchema/TEnvResult typing into inline callbacks and extracted files.
4
- import type { StandardSchemaV1 } from '@standard-schema/spec'
5
-
6
- import type { DbFor } from '../db'
7
- import type { EmailFacade } from '../email'
8
- import type { StorageFacade } from '../index'
9
-
10
- import { parseCron } from './cron'
11
- import { CRON_PREFIX, type CatchUp } from './slots'
12
-
13
- export const DEFAULT_RETRIES = 3
14
- export const DEFAULT_TIMEOUT_MS = 60_000
15
-
16
- export type EnqueueOptions = {
17
- /** Collapse duplicate enqueues while the queue row is non-terminal. */
18
- dedupeKey?: string
19
- /** Milliseconds from now until the job becomes claimable. */
20
- delay?: number
21
- /** Absolute time the job becomes claimable; wins over `delay`. */
22
- runAt?: Date | number
23
- }
24
-
25
- export type TickResult = {
26
- /** Rows moved from pending to running this tick. */
27
- claimed: number
28
- /** Handlers that completed successfully. */
29
- ran: number
30
- /** Handlers that threw, whether or not they will be retried. */
31
- failed: number
32
- }
33
-
34
- /**
35
- * The untyped runtime facade. Job handlers and API context expose this shape;
36
- * `app.jobs` narrows `enqueue` to the declared job names/payloads.
37
- */
38
- export type JobsRuntimeFacade = {
39
- enqueue(
40
- name: string,
41
- input?: unknown,
42
- opts?: EnqueueOptions,
43
- ): Promise<{ id: string }>
44
- /** Run one poll cycle deterministically (tests). `now` defaults to Date.now(). */
45
- tick(now?: number): Promise<TickResult>
46
- }
47
-
48
- import type { RealtimeFacade } from '../realtime/facade'
49
-
50
- export type JobContext<
51
- TSchema extends Record<string, unknown> = Record<string, unknown>,
52
- TEnvResult = Record<string, unknown>,
53
- > = {
54
- db: DbFor<TSchema>
55
- env: TEnvResult
56
- email: EmailFacade
57
- storage: StorageFacade
58
- jobs: JobsRuntimeFacade
59
- realtime: RealtimeFacade<TSchema>
60
- }
61
-
62
- export type BunderstackJobContext<
63
- TSchema extends Record<string, unknown> = Record<string, unknown>,
64
- TEnvResult = Record<string, unknown>,
65
- > = JobContext<TSchema, TEnvResult>
66
-
67
- export type QueueJobDefinition<
68
- TInput,
69
- TSchema extends Record<string, unknown> = Record<string, unknown>,
70
- TEnvResult = Record<string, unknown>,
71
- > = {
72
- kind: 'job'
73
- /** Standard Schema payload; parsed at enqueue AND before the handler runs. */
74
- input?: StandardSchemaV1<unknown, TInput>
75
- /** Attempts after the first failure. Default 3 (so 4 total attempts). */
76
- retries?: number
77
- /** Delay before retry N (1-based). Default exponential: 1s, 2s, 4s, … */
78
- backoff?: ((attempt: number) => number) | { baseMs?: number; factor?: number }
79
- /** Max simultaneous `running` rows of this type, enforced per worker. */
80
- concurrency?: number
81
- /** Lease duration in ms; an expired lease sends the job back to pending. */
82
- timeout?: number
83
- handler: (
84
- input: TInput,
85
- ctx: JobContext<TSchema, TEnvResult>,
86
- ) => Promise<void> | void
87
- /** Fires once, after the final attempt fails. Errors here are logged, never retried. */
88
- onFailed?: (
89
- input: TInput,
90
- error: Error,
91
- ctx: JobContext<TSchema, TEnvResult>,
92
- ) => Promise<void> | void
93
- }
94
-
95
- export type CronInvocation = { scheduledFor: Date }
96
-
97
- export type CronDefinition<
98
- TSchema extends Record<string, unknown> = Record<string, unknown>,
99
- TEnvResult = Record<string, unknown>,
100
- TSchedule extends string = string,
101
- > = {
102
- kind: 'cron'
103
- schedule: TSchedule
104
- /** Attempts after the first failure. Default 3 (so 4 total attempts). */
105
- retries?: number
106
- /** Delay before retry N (1-based). Default exponential: 1s, 2s, 4s, … */
107
- backoff?: ((attempt: number) => number) | { baseMs?: number; factor?: number }
108
- /** Lease duration in ms; an expired lease sends the slot back to pending. */
109
- timeout?: number
110
- /** How missed slots are handled on wake. Default 'latest'. */
111
- catchUp?: CatchUp
112
- /** How far back catch-up looks, in ms. Default 1 hour. */
113
- catchUpWindow?: number
114
- handler: (
115
- invocation: CronInvocation,
116
- ctx: JobContext<TSchema, TEnvResult>,
117
- ) => Promise<void> | void
118
- /** Fires once, after the final attempt fails. Errors here are logged, never retried. */
119
- onFailed?: (
120
- invocation: CronInvocation,
121
- error: Error,
122
- ctx: JobContext<TSchema, TEnvResult>,
123
- ) => Promise<void> | void
124
- }
125
-
126
- export type BackgroundDefinition =
127
- | QueueJobDefinition<any, any, any>
128
- | CronDefinition<any, any>
129
- export type BackgroundDefs = Record<string, BackgroundDefinition>
130
-
131
- /** @deprecated Use QueueJobDefinition. */
132
- export type JobDefinition<
133
- TInput,
134
- TSchema extends Record<string, unknown> = Record<string, unknown>,
135
- TEnvResult = Record<string, unknown>,
136
- > = QueueJobDefinition<TInput, TSchema, TEnvResult>
137
-
138
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
- export type AnyJobDefinition = QueueJobDefinition<any, any, any>
140
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
- export type AnyBackgroundDefinition =
142
- | QueueJobDefinition<any, any, any>
143
- | CronDefinition<any, any, any>
144
- export type JobsDefs = BackgroundDefs
145
-
146
- export type QueueJobKeys<TDefs extends BackgroundDefs> = {
147
- [K in keyof TDefs & string]: TDefs[K] extends QueueJobDefinition<
148
- any,
149
- any,
150
- any
151
- >
152
- ? K
153
- : never
154
- }[keyof TDefs & string]
155
-
156
- /** Throws when a definition is unusable. Safe to call more than once. */
157
- export function validateBackgroundDefs(defs: BackgroundDefs): void {
158
- for (const [name, def] of Object.entries(defs)) {
159
- if (typeof def.handler !== 'function') {
160
- throw new Error(`[bunderstack] background task "${name}" has no handler`)
161
- }
162
- if (def.kind === 'job' && name.startsWith(CRON_PREFIX)) {
163
- throw new Error(
164
- `[bunderstack] job "${name}": the "${CRON_PREFIX}" prefix is reserved for cron tasks`,
165
- )
166
- }
167
- if (
168
- def.retries !== undefined &&
169
- (def.retries < 0 || !Number.isInteger(def.retries))
170
- ) {
171
- throw new Error(
172
- `[bunderstack] background task "${name}": retries must be a non-negative integer`,
173
- )
174
- }
175
- if (def.timeout !== undefined && def.timeout <= 0) {
176
- throw new Error(
177
- `[bunderstack] background task "${name}": timeout must be positive`,
178
- )
179
- }
180
- if (def.kind === 'cron') {
181
- parseCron(def.schedule)
182
- if ((def as { concurrency?: number }).concurrency !== undefined) {
183
- throw new Error(
184
- `[bunderstack] cron "${name}": concurrency is not supported for cron tasks — slots are already unique`,
185
- )
186
- }
187
- if (def.catchUpWindow !== undefined && def.catchUpWindow <= 0) {
188
- throw new Error(
189
- `[bunderstack] cron "${name}": catchUpWindow must be positive`,
190
- )
191
- }
192
- continue
193
- }
194
- if (
195
- def.concurrency !== undefined &&
196
- (def.concurrency < 1 || !Number.isInteger(def.concurrency))
197
- ) {
198
- throw new Error(
199
- `[bunderstack] job "${name}": concurrency must be a positive integer`,
200
- )
201
- }
202
- }
203
- }
204
-
205
- /** @deprecated Use validateBackgroundDefs. */
206
- export const validateJobsDefs = validateBackgroundDefs
207
-
208
- /**
209
- * Delay in ms before retry `attempt` (1-based = the attempt that just failed).
210
- * Jittered by ±20% so a shared outage does not retry every job in lockstep.
211
- * A caller-supplied backoff function is returned verbatim — the caller owns it.
212
- */
213
- export function backoffMs(
214
- def: AnyBackgroundDefinition,
215
- attempt: number,
216
- ): number {
217
- const b = def.backoff
218
- if (typeof b === 'function') return b(attempt)
219
- const baseMs = b?.baseMs ?? 1000
220
- const factor = b?.factor ?? 2
221
- const flat = baseMs * factor ** (attempt - 1)
222
- return Math.round(flat * (0.8 + Math.random() * 0.4))
223
- }
224
-
225
- /**
226
- * Build the `j` instance bunderstack hands to the config's `jobs` builder
227
- * callback (and exports for multi-file job setups).
228
- */
229
- export function createJobsBuilder<
230
- TSchema extends Record<string, unknown>,
231
- TEnvResult = Record<string, unknown>,
232
- >() {
233
- return {
234
- /** Identity with inference: pins TInput from the schema output. */
235
- job<TInput = undefined>(
236
- def: Omit<QueueJobDefinition<TInput, TSchema, TEnvResult>, 'kind'>,
237
- ): QueueJobDefinition<TInput, TSchema, TEnvResult> {
238
- return { kind: 'job', ...def }
239
- },
240
- cron<const TSchedule extends string>(
241
- def: Omit<CronDefinition<TSchema, TEnvResult, TSchedule>, 'kind'>,
242
- ): CronDefinition<TSchema, TEnvResult, TSchedule> {
243
- parseCron(def.schedule)
244
- return { kind: 'cron', ...def }
245
- },
246
- /** Identity with validation: returns the defs map, typed. */
247
- define<TDefs extends BackgroundDefs>(defs: TDefs): TDefs {
248
- validateBackgroundDefs(defs)
249
- return defs
250
- },
251
- }
252
- }
253
-
254
- /** Type of the `j` instance — for builder callbacks declared in separate files. */
255
- export type BunderstackJobsBuilder<
256
- TSchema extends Record<string, unknown>,
257
- TEnvResult = Record<string, unknown>,
258
- > = ReturnType<typeof createJobsBuilder<TSchema, TEnvResult>>
259
-
260
- // Infers TInput from the JobDefinition's own type argument rather than
261
- // pattern-matching the (optional, so union-with-undefined) `input` property —
262
- // A required-property pattern fails structurally because `input` is optional,
263
- // so infer from the definition's own type argument instead.
264
- type JobInputOf<TDef> =
265
- TDef extends QueueJobDefinition<infer TInput, any, any> ? TInput : undefined
266
-
267
- /**
268
- * `app.jobs`: `enqueue` narrowed to declared names + payloads. `Omit`s the
269
- * runtime facade's loose `enqueue` first — intersecting two same-named
270
- * methods instead would make TS treat them as overloaded, so the loose
271
- * `(name: string, ...)` signature would still accept any name.
272
- */
273
- export type JobsFacade<TDefs extends JobsDefs> = Omit<
274
- JobsRuntimeFacade,
275
- 'enqueue'
276
- > & {
277
- enqueue<K extends QueueJobKeys<TDefs>>(
278
- name: K,
279
- ...rest: JobInputOf<TDefs[K]> extends undefined
280
- ? [input?: undefined, opts?: EnqueueOptions]
281
- : [input: JobInputOf<TDefs[K]>, opts?: EnqueueOptions]
282
- ): Promise<{ id: string }>
283
- }
package/src/jobs/index.ts DELETED
@@ -1,37 +0,0 @@
1
- // src/jobs/index.ts — module surface consumed by createBunderstack.
2
- export {
3
- createJobsBuilder,
4
- validateBackgroundDefs,
5
- validateJobsDefs,
6
- DEFAULT_RETRIES,
7
- DEFAULT_TIMEOUT_MS,
8
- } from './define'
9
- export type {
10
- AnyJobDefinition,
11
- BackgroundDefinition,
12
- BackgroundDefs,
13
- BunderstackJobContext,
14
- BunderstackJobsBuilder,
15
- EnqueueOptions,
16
- JobContext,
17
- JobDefinition,
18
- QueueJobDefinition,
19
- CronDefinition,
20
- CronInvocation,
21
- QueueJobKeys,
22
- JobsDefs,
23
- JobsFacade,
24
- JobsRuntimeFacade,
25
- } from './define'
26
- export { enqueueJob } from './queue'
27
- export { createJobRunner } from './worker'
28
- export { startJobWorker } from './runtime'
29
- export type {
30
- StartWorkerOptions,
31
- RunWorkerOptions,
32
- WorkerHandle,
33
- } from './runtime'
34
- export { parseCron, cronMatches } from './cron'
35
- export { slotsDue, floorSlot, CRON_PREFIX, SLOT_MS } from './slots'
36
- export type { CatchUp } from './slots'
37
- export type { TickResult } from './define'
package/src/jobs/queue.ts DELETED
@@ -1,67 +0,0 @@
1
- // src/jobs/queue.ts — durable enqueue with constraint-backed dedupe.
2
- import { and, eq } from 'drizzle-orm'
3
-
4
- import type { AnyDb } from '../dialect'
5
- import type { EnqueueOptions, JobsDefs } from './define'
6
-
7
- import { jobsTableFor } from '../internal-tables'
8
- import { validateStandardSchema } from '../standard-schema'
9
- import { generate } from '../typeid'
10
- import { CRON_PREFIX } from './slots'
11
-
12
- export async function enqueueJob(
13
- db: AnyDb,
14
- defs: JobsDefs,
15
- name: string,
16
- input: unknown,
17
- opts: EnqueueOptions = {},
18
- ): Promise<{ id: string }> {
19
- const def = defs[name]
20
- if (!def) {
21
- throw new Error(`[bunderstack] unknown background task "${name}"`)
22
- }
23
- const isCron = def.kind === 'cron'
24
- const type = isCron ? `${CRON_PREFIX}${name}` : name
25
- // Cron slots carry no payload; queue jobs validate theirs at the call site.
26
- const parsed = isCron
27
- ? null
28
- : def.input
29
- ? validateStandardSchema(def.input, input, `job "${name}" input`)
30
- : null
31
- const t = jobsTableFor(db)
32
- const now = Date.now()
33
- const runAt =
34
- opts.runAt !== undefined
35
- ? new Date(opts.runAt).getTime()
36
- : now + (opts.delay ?? 0)
37
-
38
- // Two rounds cover the race where the deduping row reaches a terminal state
39
- // (clearing its key) between our failed insert and our read.
40
- for (let round = 0; round < 2; round++) {
41
- const id = generate('job')
42
- const insertedRows = await db
43
- .insert(t)
44
- .values({
45
- id,
46
- type,
47
- payloadJson: JSON.stringify(parsed),
48
- status: 'pending',
49
- attempts: 0,
50
- runAt,
51
- dedupeKey: opts.dedupeKey ?? null,
52
- createdAt: now,
53
- })
54
- .onConflictDoNothing({ target: [t.type, t.dedupeKey] })
55
- .returning({ id: t.id })
56
- if (insertedRows[0]) return { id: String(insertedRows[0].id) }
57
- const existing = await db
58
- .select({ id: t.id })
59
- .from(t)
60
- .where(and(eq(t.type, type), eq(t.dedupeKey, opts.dedupeKey ?? '')))
61
- .limit(1)
62
- if (existing[0]) return { id: String(existing[0].id) }
63
- }
64
- throw new Error(
65
- `[bunderstack] enqueue of "${name}" lost a dedupe race twice — please retry`,
66
- )
67
- }
@@ -1,58 +0,0 @@
1
- export type WorkerHandle = {
2
- readonly closed: Promise<void>
3
- close(): Promise<void>
4
- }
5
-
6
- export type StartWorkerOptions = {
7
- signal?: AbortSignal
8
- pollIntervalMs?: number
9
- tick: (now: number) => Promise<void>
10
- onError?: (error: Error) => void
11
- }
12
-
13
- export type RunWorkerOptions = StartWorkerOptions
14
-
15
- function toError(error: unknown): Error {
16
- return error instanceof Error ? error : new Error(String(error))
17
- }
18
-
19
- function wait(ms: number, signal: AbortSignal): Promise<void> {
20
- return new Promise((resolve) => {
21
- if (signal.aborted) return resolve()
22
- const timer = setTimeout(done, ms)
23
- function done() {
24
- clearTimeout(timer)
25
- signal.removeEventListener('abort', done)
26
- resolve()
27
- }
28
- signal.addEventListener('abort', done, { once: true })
29
- })
30
- }
31
-
32
- export function startJobWorker(options: StartWorkerOptions): WorkerHandle {
33
- const controller = new AbortController()
34
- const pollIntervalMs = options.pollIntervalMs ?? 1_000
35
- const abort = () => controller.abort()
36
- options.signal?.addEventListener('abort', abort, { once: true })
37
- if (options.signal?.aborted) abort()
38
-
39
- const closed = (async () => {
40
- try {
41
- while (!controller.signal.aborted) {
42
- try {
43
- await options.tick(Date.now())
44
- } catch (error) {
45
- options.onError?.(toError(error))
46
- }
47
- await wait(pollIntervalMs, controller.signal)
48
- }
49
- } finally {
50
- options.signal?.removeEventListener('abort', abort)
51
- }
52
- })()
53
- const close = () => {
54
- controller.abort()
55
- return closed
56
- }
57
- return { closed, close }
58
- }