@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.
Files changed (58) hide show
  1. package/dist/api/rest/query-parser.d.ts +37 -1
  2. package/dist/api/rest/write-validation.d.ts +26 -0
  3. package/dist/auth/index.d.ts +3 -1
  4. package/dist/auth/interfaces.d.ts +14 -1
  5. package/dist/auth/jwks-routes.d.ts +17 -0
  6. package/dist/auth/jwt-keys.d.ts +108 -0
  7. package/dist/auth/jwt.d.ts +62 -2
  8. package/dist/{auth-CYoPVf-E.js → auth-BobZVd0j.js} +142 -167
  9. package/dist/auth-BobZVd0j.js.map +1 -0
  10. package/dist/boot/boot.d.ts +36 -50
  11. package/dist/boot/ddl-bootstrap.d.ts +15 -0
  12. package/dist/boot/env.d.ts +20 -0
  13. package/dist/boot/provision.d.ts +182 -0
  14. package/dist/boot/role.d.ts +88 -0
  15. package/dist/{cron-store-Dvr4Y1sZ.js → cron-store-CB1x-Ken.js} +3 -3
  16. package/dist/{cron-store-Dvr4Y1sZ.js.map → cron-store-CB1x-Ken.js.map} +1 -1
  17. package/dist/{ddl-bootstrap-BhXbTnBl.js → ddl-bootstrap-Cywoj8Ta.js} +40 -2
  18. package/dist/ddl-bootstrap-Cywoj8Ta.js.map +1 -0
  19. package/dist/env.d.ts +2 -0
  20. package/dist/functions/proxy.d.ts +41 -0
  21. package/dist/functions/selection.d.ts +45 -0
  22. package/dist/index.d.ts +8 -3
  23. package/dist/index.es.js +1113 -661
  24. package/dist/index.es.js.map +1 -1
  25. package/dist/init/shutdown.d.ts +4 -0
  26. package/dist/init/surfaces.d.ts +79 -0
  27. package/dist/init.d.ts +121 -1
  28. package/dist/jobs/index.d.ts +5 -0
  29. package/dist/jobs/job-queue.d.ts +14 -0
  30. package/dist/jobs/job-store.d.ts +22 -0
  31. package/dist/jobs/types.d.ts +125 -0
  32. package/dist/jobs-DR4SjGrD.js +326 -0
  33. package/dist/jobs-DR4SjGrD.js.map +1 -0
  34. package/dist/{jwt-_IFqfTOg.js → jwt-VJyXTdQQ.js} +447 -11
  35. package/dist/jwt-VJyXTdQQ.js.map +1 -0
  36. package/dist/{openapi-generator-DPKtUC9X.js → openapi-generator-DQeQ_q2f.js} +68 -3
  37. package/dist/openapi-generator-DQeQ_q2f.js.map +1 -0
  38. package/dist/proxy-Bj5DVllb.js +139 -0
  39. package/dist/proxy-Bj5DVllb.js.map +1 -0
  40. package/dist/{request-timeout-RivJsME0.js → request-timeout-BuFoEKwT.js} +6 -3
  41. package/dist/request-timeout-BuFoEKwT.js.map +1 -0
  42. package/dist/selection-_z6TM1DB.js +64 -0
  43. package/dist/selection-_z6TM1DB.js.map +1 -0
  44. package/dist/services/webhook-service.d.ts +43 -5
  45. package/dist/{src-C7rkDGxA.js → src-8XDWyDfR.js} +84 -13
  46. package/dist/src-8XDWyDfR.js.map +1 -0
  47. package/dist/src-Cz9nMgUR.js.map +1 -1
  48. package/dist/storage/keys.d.ts +17 -0
  49. package/dist/storage/routes.d.ts +1 -1
  50. package/dist/storage/storage-registry.d.ts +46 -4
  51. package/dist/storage/tus-handler.d.ts +1 -1
  52. package/package.json +5 -5
  53. package/dist/auth-CYoPVf-E.js.map +0 -1
  54. package/dist/ddl-bootstrap-BhXbTnBl.js.map +0 -1
  55. package/dist/jwt-_IFqfTOg.js.map +0 -1
  56. package/dist/openapi-generator-DPKtUC9X.js.map +0 -1
  57. package/dist/request-timeout-RivJsME0.js.map +0 -1
  58. package/dist/src-C7rkDGxA.js.map +0 -1
@@ -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
+ }