@rebasepro/server 0.14.0 → 0.14.1
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/dist/api/rest/query-parser.d.ts +37 -1
- package/dist/api/rest/write-validation.d.ts +26 -0
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/interfaces.d.ts +14 -1
- package/dist/auth/jwks-routes.d.ts +17 -0
- package/dist/auth/jwt-keys.d.ts +108 -0
- package/dist/auth/jwt.d.ts +62 -2
- package/dist/{auth-CYoPVf-E.js → auth-BobZVd0j.js} +142 -167
- package/dist/auth-BobZVd0j.js.map +1 -0
- package/dist/boot/boot.d.ts +36 -50
- package/dist/boot/ddl-bootstrap.d.ts +15 -0
- package/dist/boot/env.d.ts +20 -0
- package/dist/boot/provision.d.ts +182 -0
- package/dist/boot/role.d.ts +88 -0
- package/dist/{cron-store-Dvr4Y1sZ.js → cron-store-CB1x-Ken.js} +3 -3
- package/dist/{cron-store-Dvr4Y1sZ.js.map → cron-store-CB1x-Ken.js.map} +1 -1
- package/dist/{ddl-bootstrap-BhXbTnBl.js → ddl-bootstrap-Cywoj8Ta.js} +40 -2
- package/dist/ddl-bootstrap-Cywoj8Ta.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/functions/proxy.d.ts +41 -0
- package/dist/functions/selection.d.ts +45 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.es.js +1113 -661
- package/dist/index.es.js.map +1 -1
- package/dist/init/shutdown.d.ts +4 -0
- package/dist/init/surfaces.d.ts +79 -0
- package/dist/init.d.ts +121 -1
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/job-queue.d.ts +14 -0
- package/dist/jobs/job-store.d.ts +22 -0
- package/dist/jobs/types.d.ts +125 -0
- package/dist/jobs-DR4SjGrD.js +326 -0
- package/dist/jobs-DR4SjGrD.js.map +1 -0
- package/dist/{jwt-_IFqfTOg.js → jwt-VJyXTdQQ.js} +447 -11
- package/dist/jwt-VJyXTdQQ.js.map +1 -0
- package/dist/{openapi-generator-DPKtUC9X.js → openapi-generator-DQeQ_q2f.js} +68 -3
- package/dist/openapi-generator-DQeQ_q2f.js.map +1 -0
- package/dist/proxy-Bj5DVllb.js +139 -0
- package/dist/proxy-Bj5DVllb.js.map +1 -0
- package/dist/{request-timeout-RivJsME0.js → request-timeout-BuFoEKwT.js} +6 -3
- package/dist/request-timeout-BuFoEKwT.js.map +1 -0
- package/dist/selection-_z6TM1DB.js +64 -0
- package/dist/selection-_z6TM1DB.js.map +1 -0
- package/dist/services/webhook-service.d.ts +43 -5
- package/dist/{src-C7rkDGxA.js → src-8XDWyDfR.js} +84 -13
- package/dist/src-8XDWyDfR.js.map +1 -0
- package/dist/src-Cz9nMgUR.js.map +1 -1
- package/dist/storage/keys.d.ts +17 -0
- package/dist/storage/routes.d.ts +1 -1
- package/dist/storage/storage-registry.d.ts +46 -4
- package/dist/storage/tus-handler.d.ts +1 -1
- package/package.json +5 -5
- package/dist/auth-CYoPVf-E.js.map +0 -1
- package/dist/ddl-bootstrap-BhXbTnBl.js.map +0 -1
- package/dist/jwt-_IFqfTOg.js.map +0 -1
- package/dist/openapi-generator-DPKtUC9X.js.map +0 -1
- package/dist/request-timeout-RivJsME0.js.map +0 -1
- package/dist/src-C7rkDGxA.js.map +0 -1
package/dist/init/shutdown.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ interface ShutdownConfig {
|
|
|
5
5
|
cronScheduler?: {
|
|
6
6
|
stop(): void;
|
|
7
7
|
};
|
|
8
|
+
/** Structural, for the same no-circular-imports reason as the backend below. */
|
|
9
|
+
jobQueue?: {
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
};
|
|
8
12
|
realtimeServices: Record<string, RealtimeProvider>;
|
|
9
13
|
}
|
|
10
14
|
/**
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which HTTP surfaces a runtime process mounts.
|
|
3
|
+
*
|
|
4
|
+
* One process serving everything is the only shape this server had, and it is
|
|
5
|
+
* still the default. This exists so a deployment can boot the *same* image and
|
|
6
|
+
* the *same* bundle several times over and have each process serve a different
|
|
7
|
+
* part of the project — a custom function that pins the event loop then does so
|
|
8
|
+
* in a process that is not also answering `/api/data`.
|
|
9
|
+
*
|
|
10
|
+
* A surface is a mount point, not a feature. Switching `data` off does not stop
|
|
11
|
+
* the collection registry from being built (functions read through it); it stops
|
|
12
|
+
* `${basePath}/data` from answering. What a process *owns* — the cron scheduler,
|
|
13
|
+
* the job workers, schema DDL — is a separate axis, because "which URLs answer"
|
|
14
|
+
* and "which timers fire" are genuinely independent questions. See
|
|
15
|
+
* {@link RuntimeOwnership}.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A mountable HTTP surface.
|
|
19
|
+
*
|
|
20
|
+
* These are coarse on purpose: the set of processes anyone wants to run is
|
|
21
|
+
* small, and a per-route toggle is a combinatorial space nobody tests.
|
|
22
|
+
*
|
|
23
|
+
* - `auth` — `${basePath}/auth`, plus `/.well-known/jwks.json`, which is only
|
|
24
|
+
* meaningful for a process that mints tokens.
|
|
25
|
+
* - `data` — `${basePath}/data`, the collection REST API.
|
|
26
|
+
* - `storage` — `${basePath}/storage`, including the stub mounted when storage
|
|
27
|
+
* is unconfigured.
|
|
28
|
+
* - `admin` — every admin-gated surface: `${basePath}/admin` and its children
|
|
29
|
+
* (users, roles, api-keys, backups), `${basePath}/logs`, and the schema
|
|
30
|
+
* editor. One name because they share one gate and one audience.
|
|
31
|
+
* - `functions` — `${basePath}/functions`, the custom function router.
|
|
32
|
+
* - `cron` — `${basePath}/cron`, the cron *admin* surface. Whether jobs actually
|
|
33
|
+
* fire is {@link RuntimeOwnership.cronScheduler}, not this.
|
|
34
|
+
* - `meta` — `${basePath}/meta`, the contract endpoint a generated SDK reads.
|
|
35
|
+
*/
|
|
36
|
+
export type RuntimeSurface = "auth" | "data" | "storage" | "admin" | "functions" | "cron" | "meta";
|
|
37
|
+
/** Every surface, in a stable order. */
|
|
38
|
+
export declare const ALL_RUNTIME_SURFACES: readonly RuntimeSurface[];
|
|
39
|
+
/** A fully-resolved answer for every surface. */
|
|
40
|
+
export type ResolvedSurfaces = Record<RuntimeSurface, boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* What a process owns, as opposed to what it serves.
|
|
43
|
+
*
|
|
44
|
+
* Both of these are already safe to run in more than one process — the cron
|
|
45
|
+
* scheduler claims each `(job, slot)` pair in `rebase.cron_claims`, and the job
|
|
46
|
+
* store claims rows `FOR UPDATE SKIP LOCKED`. So this is not a correctness
|
|
47
|
+
* control; it is about not giving scheduled work to a process whose replica
|
|
48
|
+
* count is a scaling decision.
|
|
49
|
+
*/
|
|
50
|
+
export interface RuntimeOwnership {
|
|
51
|
+
/** Start the cron scheduler's timers. The `cron` surface is separate. */
|
|
52
|
+
cronScheduler: boolean;
|
|
53
|
+
/** Start the durable job queue's workers. Only relevant when `jobs.enabled`. */
|
|
54
|
+
jobWorkers: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** A fully-resolved answer for every owned singleton. */
|
|
57
|
+
export type ResolvedOwnership = RuntimeOwnership;
|
|
58
|
+
/**
|
|
59
|
+
* Surfaces as a caller may express them: name only what differs.
|
|
60
|
+
*
|
|
61
|
+
* `undefined` means "every surface", which is what every existing caller passes
|
|
62
|
+
* by passing nothing.
|
|
63
|
+
*/
|
|
64
|
+
export type RuntimeSurfaceOptions = Partial<ResolvedSurfaces>;
|
|
65
|
+
/** Ownership as a caller may express it: name only what differs. */
|
|
66
|
+
export type RuntimeOwnershipOptions = Partial<ResolvedOwnership>;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a partial surface set against the default, which is everything on.
|
|
69
|
+
*
|
|
70
|
+
* Defaulting to on rather than off is the whole compatibility story: a caller
|
|
71
|
+
* that says nothing gets the process this server has always booted, and a
|
|
72
|
+
* surface added later is mounted by every existing deployment without anyone
|
|
73
|
+
* editing a list.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveSurfaces(options?: RuntimeSurfaceOptions): ResolvedSurfaces;
|
|
76
|
+
/** Resolve partial ownership against the default, which is owning everything. */
|
|
77
|
+
export declare function resolveOwnership(options?: RuntimeOwnershipOptions): ResolvedOwnership;
|
|
78
|
+
/** The surfaces a process is not serving, for a boot log line. */
|
|
79
|
+
export declare function disabledSurfaces(resolved: ResolvedSurfaces): RuntimeSurface[];
|
package/dist/init.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { AuthAdapter, BackendBootstrapper, BootstrappedAuth, DatabaseAdapter, DataDriver, DataSourceDefinition, CollectionCallbacks, AnyCollectionConfig, HealthCheckResult, HistoryConfig, RealtimeProvider } from "@rebasepro/types";
|
|
1
|
+
import { AuthAdapter, BackendBootstrapper, BootstrappedAuth, DatabaseAdapter, DataDriver, DataSourceDefinition, CollectionCallbacks, AnyCollectionConfig, HealthCheckResult, HistoryConfig, InitializedDriver, RealtimeProvider } from "@rebasepro/types";
|
|
2
2
|
import { BackendCollectionRegistry } from "./collections/BackendCollectionRegistry";
|
|
3
3
|
import { DriverRegistry } from "./services/driver-registry";
|
|
4
4
|
import { Server } from "http";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { HonoEnv } from "./api/types";
|
|
7
|
+
import { type RuntimeOwnershipOptions, type RuntimeSurfaceOptions } from "./init/surfaces";
|
|
8
|
+
import type { JwtSigningKeyConfig } from "./auth/jwt-keys";
|
|
9
|
+
import type { JobQueueOptions } from "./jobs/types";
|
|
7
10
|
import { BackendStorageConfig, StorageController, StorageRegistry } from "./storage";
|
|
8
11
|
import { type DataRateLimitConfig } from "./auth/rate-limiter";
|
|
9
12
|
import { EmailConfig } from "./email";
|
|
@@ -32,6 +35,22 @@ export interface RebaseAuthConfig {
|
|
|
32
35
|
*/
|
|
33
36
|
collection?: AnyCollectionConfig;
|
|
34
37
|
jwtSecret?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Asymmetric keys for signing access tokens, newest first. Additive —
|
|
40
|
+
* without them tokens stay HS256, exactly as before. With them, anyone can
|
|
41
|
+
* verify a session from `/.well-known/jwks.json` without holding the key
|
|
42
|
+
* that mints one, and a key can be rotated without signing users out.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* auth: {
|
|
46
|
+
* jwtSecret: process.env.JWT_SECRET,
|
|
47
|
+
* signingKeys: [{ kid: "2026-08", privateKey: process.env.JWT_PRIVATE_KEY! }]
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
signingKeys?: JwtSigningKeyConfig[];
|
|
52
|
+
/** Which of {@link signingKeys} mints new tokens. Defaults to the first. */
|
|
53
|
+
activeKid?: string;
|
|
35
54
|
accessExpiresIn?: string;
|
|
36
55
|
refreshExpiresIn?: string;
|
|
37
56
|
requireAuth?: boolean;
|
|
@@ -261,6 +280,21 @@ export interface RebaseBackendConfig {
|
|
|
261
280
|
schemaEditor?: boolean;
|
|
262
281
|
/** Options that only apply when collections are derived from the database. */
|
|
263
282
|
baas?: BaasOptions;
|
|
283
|
+
/**
|
|
284
|
+
* The pre-init handle to hand the boot-time schema hooks, when the caller
|
|
285
|
+
* has one.
|
|
286
|
+
*
|
|
287
|
+
* Those hooks run before any driver is initialized, so there is no driver
|
|
288
|
+
* result to pass — the bundle boot path can still supply a stand-in built
|
|
289
|
+
* from the connection its coordinator opened, and does, because a driver
|
|
290
|
+
* written before this was optional dereferences `driverResult.internals`
|
|
291
|
+
* unconditionally. An application that constructed its own adapter has no
|
|
292
|
+
* such handle to give (it gave the connection to the adapter instead), so it
|
|
293
|
+
* leaves this unset and the adapter falls back to its own connection.
|
|
294
|
+
*
|
|
295
|
+
* Not part of the ordinary configuration surface: set by `bootFromBundle`.
|
|
296
|
+
*/
|
|
297
|
+
provisioningDriverResult?: InitializedDriver;
|
|
264
298
|
/**
|
|
265
299
|
* Declared data sources, shared with the frontend `<Rebase dataSources>`.
|
|
266
300
|
*
|
|
@@ -370,6 +404,43 @@ export interface RebaseBackendConfig {
|
|
|
370
404
|
*/
|
|
371
405
|
history?: HistoryConfig;
|
|
372
406
|
enableSwagger?: boolean;
|
|
407
|
+
/**
|
|
408
|
+
* Which HTTP surfaces this process mounts. Every one, unless named.
|
|
409
|
+
*
|
|
410
|
+
* The point of naming a subset is to run the same bundle as several
|
|
411
|
+
* cooperating processes — one answering `/api/data`, another `/api/functions`
|
|
412
|
+
* — so a custom function that pins the event loop does not do it in the
|
|
413
|
+
* process serving the data API. `bootFromBundle` derives this from
|
|
414
|
+
* `REBASE_ROLE`; passing it directly is for tests and for embedders.
|
|
415
|
+
*
|
|
416
|
+
* Omitting it, or naming only some surfaces, leaves the rest mounted. That
|
|
417
|
+
* default is deliberate: a surface added in a later version is served by
|
|
418
|
+
* every existing deployment without anyone editing a list.
|
|
419
|
+
*/
|
|
420
|
+
/**
|
|
421
|
+
* Whether this process provisions the collection schema and its RLS
|
|
422
|
+
* policies at boot. Default `true` — what every deployment does today.
|
|
423
|
+
*
|
|
424
|
+
* `false` on every process but one in a split deployment: `CREATE … IF NOT
|
|
425
|
+
* EXISTS` reads the catalog and then writes to it, so N processes racing to
|
|
426
|
+
* provision the same schema is a state nobody designed. `bootFromBundle`
|
|
427
|
+
* derives it from `REBASE_ROLE`.
|
|
428
|
+
*/
|
|
429
|
+
provisionSchema?: boolean;
|
|
430
|
+
surfaces?: RuntimeSurfaceOptions;
|
|
431
|
+
/**
|
|
432
|
+
* Which background singletons this process runs. All of them, unless named.
|
|
433
|
+
*
|
|
434
|
+
* Separate from {@link surfaces} because "which URLs answer" and "which
|
|
435
|
+
* timers fire" are independent: a process can serve the cron *admin* surface
|
|
436
|
+
* without being the one that fires the jobs, and vice versa.
|
|
437
|
+
*
|
|
438
|
+
* Neither is a correctness control — the cron scheduler claims each
|
|
439
|
+
* `(job, slot)` pair and the job store claims rows `FOR UPDATE SKIP LOCKED`,
|
|
440
|
+
* so running several of each is already safe. It is about not handing
|
|
441
|
+
* scheduled work to a process whose replica count is a scaling decision.
|
|
442
|
+
*/
|
|
443
|
+
ownership?: RuntimeOwnershipOptions;
|
|
373
444
|
functionsDir?: string;
|
|
374
445
|
/**
|
|
375
446
|
* Per-request ceiling for `/api/functions/*`, in milliseconds.
|
|
@@ -382,6 +453,24 @@ export interface RebaseBackendConfig {
|
|
|
382
453
|
* cancelled, so give outbound calls an `AbortSignal`.
|
|
383
454
|
*/
|
|
384
455
|
functionsTimeoutMs?: number;
|
|
456
|
+
/**
|
|
457
|
+
* Serve only some of the bundle's functions.
|
|
458
|
+
*
|
|
459
|
+
* How one expensive function gets its own replica count and its own blast
|
|
460
|
+
* radius without its code moving anywhere. `bootFromBundle` fills this from
|
|
461
|
+
* `REBASE_FUNCTIONS_ONLY` / `REBASE_FUNCTIONS_EXCLUDE`.
|
|
462
|
+
*
|
|
463
|
+
* A name that is not in the bundle fails the boot — see `selectFunctions`.
|
|
464
|
+
*/
|
|
465
|
+
functionsSelection?: import("./functions/selection").FunctionSelection;
|
|
466
|
+
/**
|
|
467
|
+
* Forward `/api/functions/*` to another process instead of serving it here.
|
|
468
|
+
*
|
|
469
|
+
* Only consulted when the `functions` surface is off — a process that serves
|
|
470
|
+
* them has nothing to forward. `bootFromBundle` fills this from
|
|
471
|
+
* `REBASE_FUNCTIONS_UPSTREAM` on the `api` role.
|
|
472
|
+
*/
|
|
473
|
+
functionsUpstream?: string;
|
|
385
474
|
cronsDir?: string;
|
|
386
475
|
/**
|
|
387
476
|
* Enable/disable database persistence for cron job execution logs.
|
|
@@ -389,6 +478,24 @@ export interface RebaseBackendConfig {
|
|
|
389
478
|
* Default: true.
|
|
390
479
|
*/
|
|
391
480
|
cronPersistence?: boolean;
|
|
481
|
+
/**
|
|
482
|
+
* The durable job queue: background work that survives a restart.
|
|
483
|
+
*
|
|
484
|
+
* Off unless `enabled` is set, because a worker polls the database forever
|
|
485
|
+
* and that is not a default anyone chose. Requires a driver that can run
|
|
486
|
+
* SQL; on one that cannot, the queue is unavailable and callers are told
|
|
487
|
+
* so at boot rather than at the first enqueue.
|
|
488
|
+
*
|
|
489
|
+
* ```ts
|
|
490
|
+
* jobs: {
|
|
491
|
+
* enabled: true,
|
|
492
|
+
* tasks: {
|
|
493
|
+
* "send-welcome": async ({ payload }) => { await sendEmail(payload.email); }
|
|
494
|
+
* }
|
|
495
|
+
* }
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
jobs?: JobQueueOptions;
|
|
392
499
|
/**
|
|
393
500
|
* Maximum request body size in bytes for API routes (default: 10MB).
|
|
394
501
|
* Set to 0 to disable the global limit entirely.
|
|
@@ -484,6 +591,19 @@ export interface RebaseBackendInstance {
|
|
|
484
591
|
storageController?: StorageController;
|
|
485
592
|
collectionRegistry: BackendCollectionRegistry;
|
|
486
593
|
cronScheduler?: import("./cron").CronScheduler;
|
|
594
|
+
/**
|
|
595
|
+
* The durable job queue, when `jobs.enabled` is set and the driver can run
|
|
596
|
+
* SQL. Present on the instance because the *producers* live in application
|
|
597
|
+
* code — a `WebhookDispatcher` is constructed by the app, so this is what
|
|
598
|
+
* it gets handed:
|
|
599
|
+
*
|
|
600
|
+
* ```ts
|
|
601
|
+
* const { jobQueue } = await initializeRebaseBackend({ jobs: { enabled: true } });
|
|
602
|
+
* const dispatcher = new WebhookDispatcher({ jobQueue });
|
|
603
|
+
* jobQueue?.register(WEBHOOK_DELIVERY_TASK, ctx => dispatcher.deliverQueuedJob(ctx.payload));
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
jobQueue?: import("./jobs").JobQueue;
|
|
487
607
|
/**
|
|
488
608
|
* Attach collection callbacks AFTER initialization.
|
|
489
609
|
*
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createJobStore } from "./job-store";
|
|
2
|
+
export type { JobStore } from "./job-store";
|
|
3
|
+
export { createJobQueue, defaultBackoff } from "./job-queue";
|
|
4
|
+
export type { JobQueue } from "./job-queue";
|
|
5
|
+
export type { EnqueueOptions, JobContext, JobHandler, JobQueueClient, JobQueueOptions, JobRecord, JobStatus } from "./types";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JobStore } from "./job-store.js";
|
|
2
|
+
import type { JobHandler, JobQueueClient, JobQueueOptions } from "./types.js";
|
|
3
|
+
/** 1s, 5s, 25s, 125s … capped at an hour. */
|
|
4
|
+
export declare function defaultBackoff(attempt: number): number;
|
|
5
|
+
export interface JobQueue extends JobQueueClient {
|
|
6
|
+
start(): void;
|
|
7
|
+
stop(): Promise<void>;
|
|
8
|
+
/** Run one poll's worth of work and return how many jobs ran. For tests and for `/jobs/drain`. */
|
|
9
|
+
runOnce(): Promise<number>;
|
|
10
|
+
/** Registered after construction — how `tasks` from config and internal producers meet. */
|
|
11
|
+
register<P = unknown>(task: string, handler: JobHandler<P>): void;
|
|
12
|
+
isRunning(): boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function createJobQueue(store: JobStore, options?: JobQueueOptions): JobQueue;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DataDriver } from "@rebasepro/types";
|
|
2
|
+
import type { JobRecord } from "./types.js";
|
|
3
|
+
export interface JobStore {
|
|
4
|
+
ensureTable(): Promise<void>;
|
|
5
|
+
/** Returns the new job's id, or `null` if an idempotency key matched unfinished work. */
|
|
6
|
+
insert(job: {
|
|
7
|
+
task: string;
|
|
8
|
+
payload: unknown;
|
|
9
|
+
runAt: Date;
|
|
10
|
+
maxAttempts: number;
|
|
11
|
+
idempotencyKey?: string;
|
|
12
|
+
}): Promise<string | null>;
|
|
13
|
+
/** Atomically take up to `limit` runnable jobs for this worker. */
|
|
14
|
+
claim(limit: number, workerId: string): Promise<JobRecord[]>;
|
|
15
|
+
complete(id: string): Promise<void>;
|
|
16
|
+
/** Back to `pending` with a later `runAt`, or `failed` when out of attempts. */
|
|
17
|
+
fail(id: string, error: string, retryAt: Date | null): Promise<void>;
|
|
18
|
+
/** Return jobs stranded by a worker that died holding them. Resolves with how many. */
|
|
19
|
+
reapExpired(visibilityTimeoutMs: number): Promise<number>;
|
|
20
|
+
fetch(id: string): Promise<JobRecord | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createJobStore(driver: DataDriver): JobStore | undefined;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background work that survives a restart.
|
|
3
|
+
*
|
|
4
|
+
* Rebase already had two ways to run something later, and neither is this one.
|
|
5
|
+
* Cron runs work on a *schedule*, coordinated across instances by a claims
|
|
6
|
+
* table. The webhook dispatcher runs work *soon*, from an array in memory — and
|
|
7
|
+
* says so in its own docblock: "the queue is in-process and in-memory. A
|
|
8
|
+
* crash…". Everything queued that way is gone on deploy, on a crash, and on the
|
|
9
|
+
* pod being rescheduled, with no record that it existed.
|
|
10
|
+
*
|
|
11
|
+
* A job is a row. It is claimed by exactly one worker, retried on failure with
|
|
12
|
+
* a widening delay, and left in the table when it finally gives up so somebody
|
|
13
|
+
* can look at it. The table is the queue: there is nothing to install, nothing
|
|
14
|
+
* to run alongside Postgres, and a job enqueued in a transaction that rolls
|
|
15
|
+
* back was never enqueued.
|
|
16
|
+
*/
|
|
17
|
+
/** Where a job is in its life. */
|
|
18
|
+
export type JobStatus =
|
|
19
|
+
/** Waiting for `runAt` to arrive and a worker to claim it. */
|
|
20
|
+
"pending"
|
|
21
|
+
/** Claimed. `lockedAt` is when, which is what makes a dead worker recoverable. */
|
|
22
|
+
| "running"
|
|
23
|
+
/** Handler returned. Kept briefly so a caller can observe the outcome. */
|
|
24
|
+
| "succeeded"
|
|
25
|
+
/**
|
|
26
|
+
* Out of attempts. Deliberately *kept* rather than deleted: a queue that
|
|
27
|
+
* silently drops what it could not deliver is indistinguishable from one
|
|
28
|
+
* with nothing to do.
|
|
29
|
+
*/
|
|
30
|
+
| "failed";
|
|
31
|
+
/** A job as stored. */
|
|
32
|
+
export interface JobRecord<P = unknown> {
|
|
33
|
+
id: string;
|
|
34
|
+
/** Which handler runs it. */
|
|
35
|
+
task: string;
|
|
36
|
+
payload: P;
|
|
37
|
+
status: JobStatus;
|
|
38
|
+
/** Not before this instant. Bumped on each retry to implement backoff. */
|
|
39
|
+
runAt: string;
|
|
40
|
+
attempts: number;
|
|
41
|
+
maxAttempts: number;
|
|
42
|
+
lastError: string | null;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
}
|
|
46
|
+
/** What a handler is given. */
|
|
47
|
+
export interface JobContext<P = unknown> {
|
|
48
|
+
id: string;
|
|
49
|
+
task: string;
|
|
50
|
+
payload: P;
|
|
51
|
+
/**
|
|
52
|
+
* Which attempt this is, counting from 1.
|
|
53
|
+
*
|
|
54
|
+
* Worth branching on: the first attempt of a webhook delivery and its
|
|
55
|
+
* fourth are the same call, but the fourth is the one where it is worth
|
|
56
|
+
* logging loudly, or falling back to a different route.
|
|
57
|
+
*/
|
|
58
|
+
attempt: number;
|
|
59
|
+
maxAttempts: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A handler. Throwing is how a job fails — there is no `return false`, because
|
|
63
|
+
* a boolean would be silently ignored by every handler that forgot to return
|
|
64
|
+
* one, and the failure path has to be the one you get by default.
|
|
65
|
+
*/
|
|
66
|
+
export type JobHandler<P = unknown> = (ctx: JobContext<P>) => Promise<void> | void;
|
|
67
|
+
/** Options for a single enqueue. */
|
|
68
|
+
export interface EnqueueOptions {
|
|
69
|
+
/** Run no earlier than this many milliseconds from now. */
|
|
70
|
+
delayMs?: number;
|
|
71
|
+
/** Overrides the queue default (3). */
|
|
72
|
+
maxAttempts?: number;
|
|
73
|
+
/**
|
|
74
|
+
* At most one *unfinished* job may hold a given key.
|
|
75
|
+
*
|
|
76
|
+
* The narrow scope is the point. Keying on the whole lifetime would mean a
|
|
77
|
+
* key could never be reused — "send the nightly digest to user 7" would
|
|
78
|
+
* work once, ever. Keying on unfinished work collapses a double-click, a
|
|
79
|
+
* retried request, and two instances reacting to one event into a single
|
|
80
|
+
* job, and still lets tomorrow's digest through.
|
|
81
|
+
*/
|
|
82
|
+
idempotencyKey?: string;
|
|
83
|
+
}
|
|
84
|
+
/** How the worker behaves. */
|
|
85
|
+
export interface JobQueueOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Off unless asked for. A worker polls the database forever, and turning
|
|
88
|
+
* that on for every backend that happens to run on Postgres is not a
|
|
89
|
+
* default anyone chose.
|
|
90
|
+
*/
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
/** Named handlers. A job whose `task` is not here is left alone, not failed — see the worker. */
|
|
93
|
+
tasks?: Record<string, JobHandler<never>>;
|
|
94
|
+
/** How many jobs one instance runs at once. Default 5. */
|
|
95
|
+
concurrency?: number;
|
|
96
|
+
/** How often to look for work when the last look found none. Default 2000ms. */
|
|
97
|
+
pollIntervalMs?: number;
|
|
98
|
+
/**
|
|
99
|
+
* How long a claimed job may stay claimed before another worker may take
|
|
100
|
+
* it. Default 5 minutes.
|
|
101
|
+
*
|
|
102
|
+
* This is the only thing that recovers work from a worker that died holding
|
|
103
|
+
* it — a `SIGKILL`ed pod cannot release its own claim. It is therefore also
|
|
104
|
+
* the interval after which a job that legitimately runs longer than this
|
|
105
|
+
* gets a *second* worker running it concurrently, so it must exceed the
|
|
106
|
+
* slowest handler.
|
|
107
|
+
*/
|
|
108
|
+
visibilityTimeoutMs?: number;
|
|
109
|
+
/** Attempts before a job is left `failed`. Default 3. */
|
|
110
|
+
maxAttempts?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Delay before attempt N+1, in milliseconds. Default: 1s, 5s, 25s, …
|
|
113
|
+
* capped at an hour.
|
|
114
|
+
*/
|
|
115
|
+
backoff?: (attempt: number) => number;
|
|
116
|
+
}
|
|
117
|
+
/** The enqueue side, as application code sees it. */
|
|
118
|
+
export interface JobQueueClient {
|
|
119
|
+
/**
|
|
120
|
+
* Add a job. Resolves with its id, or with `null` when an
|
|
121
|
+
* `idempotencyKey` matched an unfinished job — the caller usually does not
|
|
122
|
+
* care which, and the ones that do can tell.
|
|
123
|
+
*/
|
|
124
|
+
enqueue<P = unknown>(task: string, payload?: P, options?: EnqueueOptions): Promise<string | null>;
|
|
125
|
+
}
|