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.
- package/CHANGELOG.md +233 -0
- package/README.md +11 -1
- package/dist/access.d.ts +116 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +325 -0
- package/dist/access.js.map +1 -0
- package/dist/api/builder.d.ts +145 -0
- package/dist/api/builder.d.ts.map +1 -0
- package/dist/api/builder.js +28 -0
- package/dist/api/builder.js.map +1 -0
- package/dist/api/context.d.ts +34 -0
- package/dist/api/context.d.ts.map +1 -0
- package/dist/api/context.js +32 -0
- package/dist/api/context.js.map +1 -0
- package/dist/api/crud-router.d.ts +290 -0
- package/dist/api/crud-router.d.ts.map +1 -0
- package/dist/api/crud-router.js +249 -0
- package/dist/api/crud-router.js.map +1 -0
- package/dist/api/openapi.d.ts +6 -0
- package/dist/api/openapi.d.ts.map +1 -0
- package/dist/api/openapi.js +146 -0
- package/dist/api/openapi.js.map +1 -0
- package/dist/api/realtime-router.d.ts +76 -0
- package/dist/api/realtime-router.d.ts.map +1 -0
- package/dist/api/realtime-router.js +41 -0
- package/dist/api/realtime-router.js.map +1 -0
- package/dist/api/registry.d.ts +30 -0
- package/dist/api/registry.d.ts.map +1 -0
- package/dist/api/registry.js +239 -0
- package/dist/api/registry.js.map +1 -0
- package/dist/api/router.d.ts +9 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +19 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/storage-router.d.ts +279 -0
- package/dist/api/storage-router.d.ts.map +1 -0
- package/dist/api/storage-router.js +155 -0
- package/dist/api/storage-router.js.map +1 -0
- package/dist/api/types.d.ts +55 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/auth.d.ts +31 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/auth.js.map +1 -0
- package/dist/blueprint-generator.d.ts +18 -0
- package/dist/blueprint-generator.d.ts.map +1 -0
- package/dist/blueprint-generator.js +125 -0
- package/dist/blueprint-generator.js.map +1 -0
- package/dist/blueprint.d.ts +46 -0
- package/dist/blueprint.d.ts.map +1 -0
- package/dist/blueprint.js +162 -0
- package/dist/blueprint.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +68 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +108 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +90 -0
- package/dist/config.js.map +1 -0
- package/dist/cron.d.ts +4 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +3 -0
- package/dist/cron.js.map +1 -0
- package/dist/crud-operations.d.ts +43 -0
- package/dist/crud-operations.d.ts.map +1 -0
- package/dist/crud-operations.js +238 -0
- package/dist/crud-operations.js.map +1 -0
- package/dist/database/adapter.d.ts +20 -0
- package/dist/database/adapter.d.ts.map +1 -0
- package/dist/database/adapter.js +2 -0
- package/dist/database/adapter.js.map +1 -0
- package/dist/database/bun-sql.d.ts +3 -0
- package/dist/database/bun-sql.d.ts.map +1 -0
- package/dist/database/bun-sql.js +21 -0
- package/dist/database/bun-sql.js.map +1 -0
- package/dist/database/libsql.d.ts +3 -0
- package/dist/database/libsql.d.ts.map +1 -0
- package/dist/database/libsql.js +22 -0
- package/dist/database/libsql.js.map +1 -0
- package/dist/database/pglite.d.ts +3 -0
- package/dist/database/pglite.d.ts.map +1 -0
- package/dist/database/pglite.js +27 -0
- package/dist/database/pglite.js.map +1 -0
- package/dist/database/postgres-js.d.ts +3 -0
- package/dist/database/postgres-js.d.ts.map +1 -0
- package/dist/database/postgres-js.js +21 -0
- package/dist/database/postgres-js.js.map +1 -0
- package/dist/db.d.ts +18 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +26 -0
- package/dist/db.js.map +1 -0
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.d.ts.map +1 -0
- package/dist/dialect.js +22 -0
- package/dist/dialect.js.map +1 -0
- package/dist/email/smtp.d.ts +14 -0
- package/dist/email/smtp.d.ts.map +1 -0
- package/dist/email/smtp.js +27 -0
- package/dist/email/smtp.js.map +1 -0
- package/dist/email.d.ts +39 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +97 -0
- package/dist/email.js.map +1 -0
- package/dist/env.d.ts +47 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +99 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +125 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +92 -0
- package/dist/errors.js.map +1 -0
- package/dist/handler.d.ts +9 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +22 -0
- package/dist/handler.js.map +1 -0
- package/dist/idempotency.d.ts +17 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +65 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +480 -0
- package/dist/index.js.map +1 -0
- package/dist/internal-tables-pg.d.ts +481 -0
- package/dist/internal-tables-pg.d.ts.map +1 -0
- package/{src/internal-tables-pg.ts → dist/internal-tables-pg.js} +10 -34
- package/dist/internal-tables-pg.js.map +1 -0
- package/dist/internal-tables.d.ts +2509 -0
- package/dist/internal-tables.d.ts.map +1 -0
- package/dist/internal-tables.js +108 -0
- package/dist/internal-tables.js.map +1 -0
- package/dist/jobs/cron.d.ts +15 -0
- package/dist/jobs/cron.d.ts.map +1 -0
- package/dist/jobs/cron.js +74 -0
- package/dist/jobs/cron.js.map +1 -0
- package/dist/jobs/define.d.ts +133 -0
- package/dist/jobs/define.d.ts.map +1 -0
- package/dist/jobs/define.js +74 -0
- package/dist/jobs/define.js.map +1 -0
- package/dist/jobs/index.d.ts +11 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +8 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/queue.d.ts +6 -0
- package/dist/jobs/queue.d.ts.map +1 -0
- package/dist/jobs/queue.js +55 -0
- package/dist/jobs/queue.js.map +1 -0
- package/dist/jobs/runtime.d.ts +13 -0
- package/dist/jobs/runtime.d.ts.map +1 -0
- package/dist/jobs/runtime.js +46 -0
- package/dist/jobs/runtime.js.map +1 -0
- package/dist/jobs/slots.d.ts +25 -0
- package/dist/jobs/slots.d.ts.map +1 -0
- package/dist/jobs/slots.js +41 -0
- package/dist/jobs/slots.js.map +1 -0
- package/dist/jobs/worker.d.ts +12 -0
- package/dist/jobs/worker.d.ts.map +1 -0
- package/dist/jobs/worker.js +311 -0
- package/dist/jobs/worker.js.map +1 -0
- package/dist/lifecycle.d.ts +10 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +37 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/list-query.d.ts +62 -0
- package/dist/list-query.d.ts.map +1 -0
- package/dist/list-query.js +194 -0
- package/dist/list-query.js.map +1 -0
- package/dist/manifest.d.ts +59 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +167 -0
- package/dist/manifest.js.map +1 -0
- package/dist/provision-internals.d.ts +24 -0
- package/dist/provision-internals.d.ts.map +1 -0
- package/dist/provision-internals.js +7 -0
- package/dist/provision-internals.js.map +1 -0
- package/dist/provision.d.ts +23 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +96 -0
- package/dist/provision.js.map +1 -0
- package/dist/rate-limit.d.ts +7 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +49 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/realtime/facade.d.ts +18 -0
- package/dist/realtime/facade.d.ts.map +1 -0
- package/dist/realtime/facade.js +36 -0
- package/dist/realtime/facade.js.map +1 -0
- package/dist/realtime/filter.d.ts +13 -0
- package/dist/realtime/filter.d.ts.map +1 -0
- package/dist/realtime/filter.js +48 -0
- package/dist/realtime/filter.js.map +1 -0
- package/dist/realtime/heartbeat.d.ts +14 -0
- package/dist/realtime/heartbeat.d.ts.map +1 -0
- package/dist/realtime/heartbeat.js +64 -0
- package/dist/realtime/heartbeat.js.map +1 -0
- package/dist/realtime/publisher.d.ts +21 -0
- package/dist/realtime/publisher.d.ts.map +1 -0
- package/dist/realtime/publisher.js +17 -0
- package/dist/realtime/publisher.js.map +1 -0
- package/dist/schema-export-pg.d.ts +2 -0
- package/dist/schema-export-pg.d.ts.map +1 -0
- package/dist/schema-export-pg.js +4 -0
- package/dist/schema-export-pg.js.map +1 -0
- package/dist/schema-export.d.ts +2 -0
- package/dist/schema-export.d.ts.map +1 -0
- package/dist/schema-export.js +2 -0
- package/dist/schema-export.js.map +1 -0
- package/dist/scope.d.ts +4 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +13 -0
- package/dist/scope.js.map +1 -0
- package/dist/standard-schema.d.ts +13 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +34 -0
- package/dist/standard-schema.js.map +1 -0
- package/dist/storage/buckets.d.ts +80 -0
- package/dist/storage/buckets.d.ts.map +1 -0
- package/dist/storage/buckets.js +178 -0
- package/dist/storage/buckets.js.map +1 -0
- package/dist/storage/delete.d.ts +11 -0
- package/dist/storage/delete.d.ts.map +1 -0
- package/dist/storage/delete.js +18 -0
- package/dist/storage/delete.js.map +1 -0
- package/dist/storage/file-meta.d.ts +37 -0
- package/dist/storage/file-meta.d.ts.map +1 -0
- package/dist/storage/file-meta.js +166 -0
- package/dist/storage/file-meta.js.map +1 -0
- package/dist/storage/index.d.ts +28 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local.d.ts +15 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +56 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/storage/operations.d.ts +54 -0
- package/dist/storage/operations.d.ts.map +1 -0
- package/dist/storage/operations.js +250 -0
- package/dist/storage/operations.js.map +1 -0
- package/dist/storage/registry.d.ts +10 -0
- package/dist/storage/registry.d.ts.map +1 -0
- package/dist/storage/registry.js +23 -0
- package/dist/storage/registry.js.map +1 -0
- package/dist/storage/s3.d.ts +28 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +71 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/sweep.d.ts +9 -0
- package/dist/storage/sweep.d.ts.map +1 -0
- package/dist/storage/sweep.js +17 -0
- package/dist/storage/sweep.js.map +1 -0
- package/dist/storage/thumbnails.d.ts +11 -0
- package/dist/storage/thumbnails.d.ts.map +1 -0
- package/dist/storage/thumbnails.js +60 -0
- package/dist/storage/thumbnails.js.map +1 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +11 -0
- package/dist/testing.js.map +1 -0
- package/dist/typeid-pg.d.ts +19 -0
- package/dist/typeid-pg.d.ts.map +1 -0
- package/{src/typeid-pg.ts → dist/typeid-pg.js} +10 -13
- package/dist/typeid-pg.js.map +1 -0
- package/dist/typeid.d.ts +45 -0
- package/dist/typeid.d.ts.map +1 -0
- package/dist/typeid.js +100 -0
- package/dist/typeid.js.map +1 -0
- package/package.json +77 -28
- package/src/access.ts +0 -532
- package/src/api/builder.ts +0 -52
- package/src/api/context.ts +0 -83
- package/src/api/crud-router.ts +0 -386
- package/src/api/openapi.ts +0 -184
- package/src/api/realtime-router.ts +0 -75
- package/src/api/registry.ts +0 -338
- package/src/api/router.ts +0 -34
- package/src/api/storage-router.ts +0 -224
- package/src/api/types.ts +0 -105
- package/src/auth.ts +0 -117
- package/src/blueprint-generator.ts +0 -203
- package/src/blueprint.ts +0 -264
- package/src/cli.ts +0 -90
- package/src/config.ts +0 -215
- package/src/cron.ts +0 -3
- package/src/crud-operations.ts +0 -478
- package/src/database/adapter.ts +0 -26
- package/src/database/bun-sql.ts +0 -23
- package/src/database/libsql.ts +0 -28
- package/src/database/pglite.ts +0 -33
- package/src/database/postgres-js.ts +0 -33
- package/src/db.ts +0 -61
- package/src/dialect.ts +0 -38
- package/src/email/smtp.ts +0 -45
- package/src/email.ts +0 -154
- package/src/env.ts +0 -180
- package/src/errors.ts +0 -123
- package/src/handler.ts +0 -30
- package/src/idempotency.ts +0 -97
- package/src/index.ts +0 -834
- package/src/internal-tables.ts +0 -149
- package/src/jobs/cron.ts +0 -87
- package/src/jobs/define.ts +0 -283
- package/src/jobs/index.ts +0 -37
- package/src/jobs/queue.ts +0 -67
- package/src/jobs/runtime.ts +0 -58
- package/src/jobs/slots.ts +0 -52
- package/src/jobs/worker.ts +0 -369
- package/src/lifecycle.ts +0 -49
- package/src/list-query.ts +0 -333
- package/src/manifest.ts +0 -297
- package/src/provision-internals.ts +0 -29
- package/src/provision.ts +0 -128
- package/src/rate-limit.ts +0 -71
- package/src/realtime/facade.ts +0 -74
- package/src/realtime/filter.ts +0 -81
- package/src/realtime/heartbeat.ts +0 -80
- package/src/realtime/publisher.ts +0 -46
- package/src/schema-export-pg.ts +0 -7
- package/src/schema-export.ts +0 -5
- package/src/scope.ts +0 -17
- package/src/standard-schema.ts +0 -59
- package/src/storage/buckets.ts +0 -308
- package/src/storage/delete.ts +0 -26
- package/src/storage/file-meta.ts +0 -222
- package/src/storage/index.ts +0 -39
- package/src/storage/local.ts +0 -69
- package/src/storage/operations.ts +0 -398
- package/src/storage/registry.ts +0 -40
- package/src/storage/s3.ts +0 -99
- package/src/storage/sweep.ts +0 -25
- package/src/storage/thumbnails.ts +0 -65
- package/src/testing.ts +0 -31
- package/src/typeid.ts +0 -116
package/src/internal-tables.ts
DELETED
|
@@ -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
|
-
}
|
package/src/jobs/define.ts
DELETED
|
@@ -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
|
-
}
|
package/src/jobs/runtime.ts
DELETED
|
@@ -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
|
-
}
|