@protobase/server 0.8.1 → 0.9.0

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 (63) hide show
  1. package/dist/batch/run-batch.js +4 -2
  2. package/dist/changes/actor.js +7 -1
  3. package/dist/changes/capture-schema.js +1 -1
  4. package/dist/changes/capture-sql.js +13 -2
  5. package/dist/changes/coalesce.d.ts +4 -0
  6. package/dist/changes/coalesce.js +2 -1
  7. package/dist/changes/outbox.d.ts +14 -0
  8. package/dist/changes/outbox.js +51 -0
  9. package/dist/changes/resource-change.d.ts +2 -0
  10. package/dist/changes/resource-change.js +2 -1
  11. package/dist/create-admin.d.ts +11 -3
  12. package/dist/create-admin.js +21 -5
  13. package/dist/functions/require-env.d.ts +1 -1
  14. package/dist/index.d.ts +15 -3
  15. package/dist/index.js +15 -5
  16. package/dist/jobs/current.d.ts +10 -0
  17. package/dist/jobs/current.js +20 -0
  18. package/dist/jobs/database.js +36 -0
  19. package/dist/jobs/define-job.d.ts +8 -0
  20. package/dist/jobs/define-job.js +38 -0
  21. package/dist/jobs/drain-route.d.ts +21 -0
  22. package/dist/jobs/drain-route.js +28 -0
  23. package/dist/jobs/failed-jobs.js +67 -0
  24. package/dist/jobs/functions-sql.js +126 -0
  25. package/dist/jobs/job-types.d.ts +73 -0
  26. package/dist/jobs/options.js +44 -0
  27. package/dist/jobs/run-job.d.ts +9 -0
  28. package/dist/jobs/run-job.js +78 -0
  29. package/dist/jobs/runtime.d.ts +46 -0
  30. package/dist/jobs/runtime.js +158 -0
  31. package/dist/jobs/schedule.js +25 -0
  32. package/dist/jobs/schedules.js +41 -0
  33. package/dist/jobs/schema.d.ts +15 -0
  34. package/dist/jobs/schema.js +63 -0
  35. package/dist/project-config.d.ts +7 -2
  36. package/dist/project-config.js +24 -3
  37. package/dist/resource-source.d.ts +8 -3
  38. package/dist/resource-source.js +6 -3
  39. package/dist/types.d.ts +5 -1
  40. package/dist/webhooks/attempt.js +91 -0
  41. package/dist/webhooks/retry-schedule.js +40 -0
  42. package/dist/webhooks/route-events.js +61 -0
  43. package/dist/webhooks/standard-webhooks.d.ts +4 -0
  44. package/dist/webhooks/standard-webhooks.js +36 -0
  45. package/dist/webhooks/subscriber-view.js +76 -0
  46. package/dist/webhooks/subscription.d.ts +92 -0
  47. package/dist/webhooks/subscription.js +28 -0
  48. package/dist/webhooks/table/address-check.js +28 -0
  49. package/dist/webhooks/table/options.d.ts +23 -0
  50. package/dist/webhooks/table/options.js +14 -0
  51. package/dist/webhooks/table/owner-roles.js +23 -0
  52. package/dist/webhooks/table/plugin.d.ts +10 -0
  53. package/dist/webhooks/table/plugin.js +18 -0
  54. package/dist/webhooks/table/schema-sql.js +116 -0
  55. package/dist/webhooks/table/schema.d.ts +13 -0
  56. package/dist/webhooks/table/schema.js +44 -0
  57. package/dist/webhooks/table/table-subscriptions.js +77 -0
  58. package/dist/webhooks/table/validate-hook.js +32 -0
  59. package/dist/webhooks/table/webhook-table.js +23 -0
  60. package/dist/webhooks/test-event.js +48 -0
  61. package/dist/webhooks/webhooks.js +233 -0
  62. package/dist/write-pipeline.js +4 -2
  63. package/package.json +6 -4
@@ -2,7 +2,7 @@ import { HttpProblem, badRequest, notFound } from "../problem.js";
2
2
  import { problemOf } from "../error-handler.js";
3
3
  import { requestAccess } from "../request-access.js";
4
4
  import { writeTransaction } from "../transactions.js";
5
- import { actorSettings } from "../changes/actor.js";
5
+ import { actorSettings, wakeDispatch } from "../changes/actor.js";
6
6
  import { keyOf } from "../write-shared.js";
7
7
  import { writeIn } from "../write-pipeline.js";
8
8
  import { resolveData, resolveKey } from "./refs.js";
@@ -38,7 +38,7 @@ const runBatch = async (deps, session, operations) => {
38
38
  accesses.set(entry.name, access);
39
39
  return access;
40
40
  };
41
- return writeTransaction(deps.db, deps.statementTimeoutMs, async (trx) => {
41
+ const written = await writeTransaction(deps.db, deps.statementTimeoutMs, async (trx) => {
42
42
  const created = /* @__PURE__ */ new Map();
43
43
  const results = [];
44
44
  for (const [index, operation] of operations.entries()) try {
@@ -101,6 +101,8 @@ const runBatch = async (deps, session, operations) => {
101
101
  }
102
102
  return results;
103
103
  }, actorSettings(deps, session));
104
+ wakeDispatch(deps);
105
+ return written;
104
106
  };
105
107
  //#endregion
106
108
  export { runBatch };
@@ -1,3 +1,5 @@
1
+ import { runningJobs } from "../jobs/current.js";
2
+ import { dispatchJobName } from "./capture-sql.js";
1
3
  //#region src/changes/actor.ts
2
4
  /** The setting a write transaction sets so the change capture records who wrote, when a resource has events. */
3
5
  const actorSettings = (deps, session) => {
@@ -8,5 +10,9 @@ const actorSettings = (deps, session) => {
8
10
  };
9
11
  return { [deps.actorSetting]: JSON.stringify(actor) };
10
12
  };
13
+ /** After a write committed: has this process's dispatcher deliver its events now, rather than at its next poll. */
14
+ const wakeDispatch = (deps) => {
15
+ if (deps.actorSetting) runningJobs()?.wake(dispatchJobName);
16
+ };
11
17
  //#endregion
12
- export { actorSettings };
18
+ export { actorSettings, wakeDispatch };
@@ -1,5 +1,5 @@
1
- import { changesSchema } from "./events-options.js";
2
1
  import { quoteIdent, tableSql } from "./capture-sql.js";
2
+ import { changesSchema } from "./events-options.js";
3
3
  import { capturePlan, planSql, uncaptured } from "./capture-plan.js";
4
4
  import { sql } from "kysely";
5
5
  //#region src/changes/capture-schema.ts
@@ -1,3 +1,4 @@
1
+ import { jobsSchema } from "../jobs/schema.js";
1
2
  //#region src/changes/capture-sql.ts
2
3
  const quoteIdent = (name) => `"${name.replaceAll("\"", "\"\"")}"`;
3
4
  const quoteLiteral = (text) => `'${text.replaceAll("'", "''")}'`;
@@ -31,10 +32,16 @@ const outboxSql = (schema) => `create table if not exists ${quoteIdent(schema)}.
31
32
  "dispatch_after" timestamptz not null default now(),
32
33
  "created_at" timestamptz not null default now()
33
34
  )`;
35
+ /** The job that turns captured changes into events and deliveries: one per writing transaction, run one at a time. */
36
+ const dispatchJobName = "changes.dispatch";
37
+ /** Every dispatch job's group in its ordered queue, so only one runs at a time, across processes too. */
38
+ const dispatchGroup = "changes";
34
39
  /**
35
40
  * The body of `capture()`: its arguments are the resource, the columns to keep and the sensitive columns, whose names
36
41
  * it lists when they change but whose values it never keeps. It skips a transaction that set `<schema>.capture` to
37
- * `off`, and an update that changed none of those columns.
42
+ * `off`, and an update that changed none of those columns. The first row a transaction captures enqueues a dispatch
43
+ * job in the same transaction; `<schema>.dispatch` remembers that for the rest of it, and a rolled-back savepoint
44
+ * forgets it with the job.
38
45
  */
39
46
  const captureBody = (schema) => `
40
47
  declare
@@ -71,6 +78,10 @@ begin
71
78
  changed,
72
79
  nullif(current_setting('${schema}.actor', true), '')::jsonb
73
80
  );
81
+ if current_setting('${schema}.dispatch', true) is distinct from 'queued' then
82
+ perform ${jobsSchema}.enqueue('${dispatchJobName}', null, null, null, '${dispatchGroup}');
83
+ perform set_config('${schema}.dispatch', 'queued', true);
84
+ end if;
74
85
  return null;
75
86
  end
76
87
  `;
@@ -105,4 +116,4 @@ const createTriggerSql = (schema, trigger, table) => {
105
116
  return `create trigger ${quoteIdent(trigger.name)} ${timing} on ${table} for each row${when} execute function ${quoteIdent(schema)}."capture"(${args})`;
106
117
  };
107
118
  //#endregion
108
- export { captureBody, captureFunctionSql, createTriggerSql, dropTriggerSql, outboxSql, quoteIdent, resourceTriggers, tableSql };
119
+ export { captureBody, captureFunctionSql, createTriggerSql, dispatchGroup, dispatchJobName, dropTriggerSql, outboxSql, quoteIdent, resourceTriggers, tableSql };
@@ -14,6 +14,8 @@ export type CapturedRow = {
14
14
  /** The columns an update changed, sensitive ones included; `null` for inserts and deletes. */
15
15
  changed: string[] | null;
16
16
  actor: EventActor | null;
17
+ /** When the writing transaction started, ISO 8601. */
18
+ at: string;
17
19
  };
18
20
  /**
19
21
  * One record's changes in one transaction, as one: its columns at the start (`before`, none when it was inserted) and
@@ -28,6 +30,8 @@ export type CoalescedChange = {
28
30
  actor: EventActor | null;
29
31
  /** The id of its last outbox row. */
30
32
  sequence: string;
33
+ /** When its last change's transaction started. */
34
+ at: string;
31
35
  };
32
36
  /**
33
37
  * The outbox rows of one or more transactions, in id order, as one change per (transaction, record). An insert then a
@@ -17,7 +17,8 @@ const merged = ({ first, last, actor, listed }) => {
17
17
  transaction: first.transaction,
18
18
  resource: first.resource,
19
19
  actor,
20
- sequence: last.id
20
+ sequence: last.id,
21
+ at: last.at
21
22
  };
22
23
  const before = first.op === "INSERT" ? null : first.old;
23
24
  const after = last.op === "DELETE" ? null : last.new;
@@ -0,0 +1,14 @@
1
+ import { Row } from "../types.js";
2
+ import { CapturedRow } from "./coalesce.js";
3
+ import { ResourceModel } from "@protobase/schema";
4
+ import { Db } from "@protobase/query";
5
+ //#region src/changes/outbox.d.ts
6
+ /**
7
+ * A captured row's columns from the JSON text the database stores: the columns of 64-bit integer and decimal fields as
8
+ * their exact digits, other numbers as numbers. Needs a runtime that passes JSON.parse revivers the source text
9
+ * (Node 21 and later, Bun).
10
+ */
11
+ export declare const parseColumns: (text: string | null, model?: ResourceModel) => Row | null;
12
+ /** The oldest `limit` rows of the outbox that committed, in id order. */
13
+ export declare const readOutbox: (db: Db, schema: string, models: readonly ResourceModel[], limit: number) => Promise<CapturedRow[]>;
14
+ //#endregion
@@ -0,0 +1,51 @@
1
+ import { quoteIdent } from "./capture-sql.js";
2
+ import { sql } from "kysely";
3
+ //#region src/changes/outbox.ts
4
+ var ExactNumber = class {
5
+ source;
6
+ constructor(source) {
7
+ this.source = source;
8
+ }
9
+ };
10
+ const exact = (_key, value, context) => typeof value === "number" && context?.source !== void 0 ? new ExactNumber(context.source) : value;
11
+ const plain = (value) => {
12
+ if (value instanceof ExactNumber) return Number(value.source);
13
+ if (Array.isArray(value)) return value.map(plain);
14
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plain(item)]));
15
+ return value;
16
+ };
17
+ const textTypes = /* @__PURE__ */ new Set(["bigint", "decimal"]);
18
+ /**
19
+ * A captured row's columns from the JSON text the database stores: the columns of 64-bit integer and decimal fields as
20
+ * their exact digits, other numbers as numbers. Needs a runtime that passes JSON.parse revivers the source text
21
+ * (Node 21 and later, Bun).
22
+ */
23
+ const parseColumns = (text, model) => {
24
+ if (text === null) return null;
25
+ const parsed = JSON.parse(text, exact);
26
+ const types = new Map(Object.values(model?.fields ?? {}).map((field) => [field.column, field.type]));
27
+ return Object.fromEntries(Object.entries(parsed).map(([column, value]) => [column, value instanceof ExactNumber && textTypes.has(types.get(column) ?? "") ? value.source : plain(value)]));
28
+ };
29
+ /** The oldest `limit` rows of the outbox that committed, in id order. */
30
+ const readOutbox = async (db, schema, models, limit) => {
31
+ const byName = new Map(models.map((model) => [model.name, model]));
32
+ const { rows } = await sql`select "id"::text as id, "transaction"::text as transaction, "resource", "op", "old"::text as old, "new"::text as new, "changed",
33
+ "actor"::text as actor, to_char("created_at" at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') as at
34
+ from ${sql.raw(`${quoteIdent(schema)}."outbox"`)} where "dispatch_after" <= now() order by "id" limit ${limit}`.execute(db);
35
+ return rows.map((row) => {
36
+ const model = byName.get(row.resource);
37
+ return {
38
+ ...row,
39
+ old: parseColumns(row.old, model),
40
+ new: parseColumns(row.new, model),
41
+ actor: row.actor === null ? null : JSON.parse(row.actor)
42
+ };
43
+ });
44
+ };
45
+ /** Removes dispatched rows from the outbox. */
46
+ const deleteOutbox = async (db, schema, ids) => {
47
+ if (ids.length === 0) return;
48
+ await sql`delete from ${sql.raw(`${quoteIdent(schema)}."outbox"`)} where "id" = any(${ids}::bigint[])`.execute(db);
49
+ };
50
+ //#endregion
51
+ export { deleteOutbox, parseColumns, readOutbox };
@@ -19,6 +19,8 @@ export type ResourceChange = {
19
19
  transaction: string;
20
20
  /** Grows with every change of a record, so receivers can drop stale events. */
21
21
  sequence: string;
22
+ /** When the change's transaction started, ISO 8601. */
23
+ at: string;
22
24
  };
23
25
  /** A captured row by field name, fields without a captured column left out. */
24
26
  export declare const recordOf: (model: ResourceModel, columns: Row) => Row;
@@ -36,7 +36,8 @@ const resourceChange = (model, change) => {
36
36
  source: change.actor ? "api" : "database",
37
37
  ...tenant != null && { tenant },
38
38
  transaction: change.transaction,
39
- sequence: change.sequence
39
+ sequence: change.sequence,
40
+ at: change.at
40
41
  };
41
42
  };
42
43
  //#endregion
@@ -1,11 +1,13 @@
1
1
  import { AdminAuth } from "./better-auth/create-auth.js";
2
2
  import { EventsOptions } from "./changes/events-options.js";
3
+ import { JobDefinition } from "./jobs/job-types.js";
4
+ import { Admin, Authenticator, PageSource, ViewSource } from "./types.js";
5
+ import { WebhooksOptions } from "./webhooks/table/options.js";
6
+ import { SubscriptionDefinition } from "./webhooks/subscription.js";
3
7
  import { FilesOptions } from "./files/options.js";
4
- import { AdminEnv, Authenticator, PageSource, ViewSource } from "./types.js";
5
8
  import { ResourceSource } from "./resource-source.js";
6
9
  import { FunctionSource } from "./functions/define-function.js";
7
10
  import { AdminOptions } from "./admin-options.js";
8
- import { Hono } from "hono";
9
11
  import { UserMenuSource } from "@protobase/schema";
10
12
  import { Db } from "@protobase/query";
11
13
  //#region src/create-admin.d.ts
@@ -26,8 +28,14 @@ export type CreateAdminInput = {
26
28
  files?: FilesOptions;
27
29
  /** Where the changes that resources' events are raised from are captured (see `EventsOptions`). */
28
30
  events?: EventsOptions;
31
+ /** The app's jobs (see `defineJob`): with any, or a resource with events, admins get the failed jobs page (`failedJobs`). */
32
+ jobs?: JobDefinition[];
33
+ /** Where the resources' events are delivered (`subscription(...)` exports of the config module). */
34
+ subscriptions?: SubscriptionDefinition[];
35
+ /** Webhook subscriptions made in the admin app, from `extends: [webhooks()]` (see `WebhooksOptions`). */
36
+ webhooks?: WebhooksOptions;
29
37
  options?: AdminOptions;
30
38
  };
31
39
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
32
- export declare const createAdmin: ({ resources, views, pages, userMenu, db, authenticate, auth, functions, files: filesOptions, events, options }: CreateAdminInput) => Hono<AdminEnv, import("hono/types").BlankSchema, "/">;
40
+ export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, db, authenticate, auth, functions, files: filesOptions, events, jobs, subscriptions, webhooks: webhooksOptions, options }: CreateAdminInput) => Admin;
33
41
  //#endregion
@@ -7,12 +7,15 @@ import { authBasePath } from "./better-auth/create-auth.js";
7
7
  import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
8
8
  import { statusRoute } from "./better-auth/status-route.js";
9
9
  import { changesSchema } from "./changes/events-options.js";
10
+ import { callerRecords } from "./functions/records.js";
11
+ import { toApiFunction } from "./functions/define-function.js";
12
+ import { createWebhooks } from "./webhooks/webhooks.js";
13
+ import { webhookTable } from "./webhooks/table/webhook-table.js";
14
+ import { subscriptionWriteCheck } from "./webhooks/table/validate-hook.js";
10
15
  import { checkShell } from "./check-shell.js";
11
16
  import { checkViews } from "./check-views.js";
12
17
  import { createFilesRuntime } from "./files/runtime.js";
13
18
  import { fileRoutes } from "./files/routes.js";
14
- import { toApiFunction } from "./functions/define-function.js";
15
- import { callerRecords } from "./functions/records.js";
16
19
  import { functionRoutes } from "./functions/routes.js";
17
20
  import { runtimeUrl } from "./runtime/runtime-settings.js";
18
21
  import { createMeta } from "./meta.js";
@@ -23,12 +26,16 @@ import { docsRoutes } from "./routes/docs.js";
23
26
  import { metaRoutes } from "./routes/meta.js";
24
27
  import { recordRoutes } from "./routes/record.js";
25
28
  import { createScanGuard } from "./scan-guard.js";
29
+ import { failedJobs, failedJobsView } from "./jobs/failed-jobs.js";
26
30
  import { Hono } from "hono";
27
31
  import { checkPages } from "@protobase/layout";
28
32
  //#region src/create-admin.ts
29
33
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
30
- const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenticate, auth, functions = {}, files: filesOptions, events, options = {} }) => {
34
+ const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, db, authenticate, auth, functions = {}, files: filesOptions, events, jobs = [], subscriptions = [], webhooks: webhooksOptions, options = {} }) => {
31
35
  const basePath = options.basePath ?? "/api/v1";
36
+ const withJobs = jobs.length > 0 || appResources.some((resource) => resource.toModel().events);
37
+ const resources = withJobs ? [...appResources, failedJobs] : appResources;
38
+ const views = withJobs ? [...appViews, failedJobsView] : appViews;
32
39
  const registry = buildRegistry(resources);
33
40
  const apiFunctions = Object.fromEntries(Object.entries(functions).map(([name, source]) => [name, toApiFunction(name, source)]));
34
41
  const viewModels = views.map((view) => view.toModel());
@@ -49,6 +56,7 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
49
56
  models: registry.entries.map((entry) => entry.model),
50
57
  systemPath
51
58
  });
59
+ const table = webhooksOptions && webhookTable(webhooksOptions, auth);
52
60
  const deps = {
53
61
  db,
54
62
  registry,
@@ -59,7 +67,7 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
59
67
  systemPath,
60
68
  statementTimeoutMs: options.statementTimeoutMs ?? 15e3,
61
69
  guard: createScanGuard(db, options.scanGuard),
62
- hooks: options.writeHooks ?? [],
70
+ hooks: [...options.writeHooks ?? [], ...table ? [subscriptionWriteCheck(registry.entries.map((entry) => entry.model), table.checkUrl)] : []],
63
71
  audit: options.audit ?? consoleAuditQueue(),
64
72
  accessOptions: {
65
73
  ...options.roles && { roles: options.roles },
@@ -133,7 +141,15 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
133
141
  app.route("/", fileRoutes(deps, authenticate));
134
142
  app.route(basePath, api);
135
143
  app.route(deps.systemPath || "/", system);
136
- return app;
144
+ const webhooks = createWebhooks({
145
+ deps,
146
+ subscriptions,
147
+ functions: apiFunctions,
148
+ schema: changesSchema(events),
149
+ log: (line) => console.log(line),
150
+ ...table && { table }
151
+ });
152
+ return Object.assign(app, { jobs: webhooks.jobs });
137
153
  };
138
154
  //#endregion
139
155
  export { createAdmin };
@@ -3,5 +3,5 @@
3
3
  * An environment variable the function cannot work without, such as an API key: `protobase dev` reads it from `.env`,
4
4
  * `protobase serve` from its environment. A missing one is an error, so the call is a 500 the app reports.
5
5
  */
6
- export declare const requireEnv: (name: string) => any;
6
+ export declare const requireEnv: (name: string) => string;
7
7
  //#endregion
package/dist/index.d.ts CHANGED
@@ -7,9 +7,13 @@ import { OrganizationsOptions } from "./better-auth/organizations/options.js";
7
7
  import { PlatformSignIn, readPlatformSignIn } from "./better-auth/platform-sign-in.js";
8
8
  import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
9
9
  import { EventsOptions } from "./changes/events-options.js";
10
+ import { StandardSchema, readJson } from "./functions/read-json.js";
11
+ import { EnqueueOptions, JobContext, JobDefinition, JobOptions } from "./jobs/job-types.js";
12
+ import { Admin, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMode, Session, SessionOrganization, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
13
+ import { WebhooksOptions } from "./webhooks/table/options.js";
14
+ import { EventData, RunContext, SubscriptionDefinition, SubscriptionEvent, SubscriptionOptions, subscription } from "./webhooks/subscription.js";
10
15
  import { FileProvider, FileStore, StoredObject } from "./files/store.js";
11
16
  import { FilesOptions } from "./files/options.js";
12
- import { Admin, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMode, Session, SessionOrganization, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
13
17
  import { CleanupEntry, CleanupSchedule, ScheduledDelete, storageCleanupSchedule } from "./files/cleanup-schedule.js";
14
18
  import { CleanupReport } from "./files/cleanup-runner.js";
15
19
  import { ResourceSource, configExports } from "./resource-source.js";
@@ -38,6 +42,10 @@ import { changesMigration, checkChanges, migrationCommand } from "./changes/capt
38
42
  import { CapturedRow, CoalescedChange, coalesce } from "./changes/coalesce.js";
39
43
  import { ResourceChange, recordOf, resourceChange } from "./changes/resource-change.js";
40
44
  import { eventMatcher } from "./changes/match-events.js";
45
+ import { parseColumns, readOutbox } from "./changes/outbox.js";
46
+ import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
47
+ import { webhooks } from "./webhooks/table/plugin.js";
48
+ import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
41
49
  import { MemberInput, NewOrganization, addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
42
50
  import { AssistantBackend, assistantProtocolRoutes } from "./assistant/protocol-routes.js";
43
51
  import { fileConversationStore } from "./assistant/file-conversation-store.js";
@@ -47,11 +55,15 @@ import { WidgetOptions, maxWidgetPropsBytes, widget } from "./assistant/widget.j
47
55
  import { QueryOptions, QueryResult, runReadOnlyQuery, runReadWriteQuery } from "./assistant/run-query.js";
48
56
  import { readOnlyQueryTool, readWriteQueryTool } from "./assistant/query-tools.js";
49
57
  import { FunctionEnv, PublicFunctionEnv, functionApp } from "./functions/function-app.js";
50
- import { StandardSchema, readJson } from "./functions/read-json.js";
51
58
  import { SignatureOptions, StandardWebhookOptions, verifySignature, verifyStandardWebhook } from "./functions/verify-signature.js";
52
59
  import { requireEnv } from "./functions/require-env.js";
53
60
  import { LocalFilesOptions, localFileStore, localFiles } from "./files/local-files.js";
54
61
  import { FileCleanupInput, createFileCleanup } from "./files/file-cleanup.js";
55
62
  import { FileObject } from "./files/present.js";
56
63
  import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
57
- export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CreateAdminInput, type CreateAuthOptions, type EventsOptions, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, issueToken, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, normalizeRoles, notFound, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, storageCleanupSchedule, streamChatCompletion, unauthorized, verifySignature, verifyStandardWebhook, widget, withCacheBreakpoints };
64
+ import { defineJob, isJobDefinition, jobDefinitions } from "./jobs/define-job.js";
65
+ import { FatalJobError } from "./jobs/run-job.js";
66
+ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
67
+ import { jobsDrainApp } from "./jobs/drain-route.js";
68
+ import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
69
+ export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EnqueueOptions, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webhooks, webhooksMigration, widget, withCacheBreakpoints };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { readOnlyQueryTool, readWriteQueryTool } from "./assistant/query-tools.j
9
9
  import { requestApproval } from "./assistant/approval.js";
10
10
  import { withCacheBreakpoints } from "./assistant/cache-breakpoints.js";
11
11
  import { ModelError, streamChatCompletion } from "./assistant/chat-completions.js";
12
+ import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
12
13
  import { runTurn } from "./assistant/turn.js";
13
14
  import { consoleAuditQueue } from "./audit/console-queue.js";
14
15
  import { readOperatorSettings } from "./better-auth/operator-provider.js";
@@ -18,9 +19,17 @@ import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
18
19
  import { createAuth, normalizeRoles } from "./better-auth/create-auth.js";
19
20
  import { addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
20
21
  import { createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
22
+ import { coalesce } from "./changes/coalesce.js";
23
+ import { eventMatcher } from "./changes/match-events.js";
24
+ import { parseColumns, readOutbox } from "./changes/outbox.js";
25
+ import { recordOf, resourceChange } from "./changes/resource-change.js";
26
+ import { defineJob, isJobDefinition, jobDefinitions } from "./jobs/define-job.js";
27
+ import { FatalJobError } from "./jobs/run-job.js";
28
+ import { defineFunction } from "./functions/define-function.js";
29
+ import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
30
+ import { subscription } from "./webhooks/subscription.js";
21
31
  import { storageCleanupSchedule } from "./files/cleanup-schedule.js";
22
32
  import { localFileStore, localFiles } from "./files/local-files.js";
23
- import { defineFunction } from "./functions/define-function.js";
24
33
  import { runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
25
34
  import { createAdmin } from "./create-admin.js";
26
35
  import { configExports } from "./resource-source.js";
@@ -30,9 +39,8 @@ import { betterAuthAuthenticator } from "./better-auth/authenticator.js";
30
39
  import { issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
31
40
  import { authSchemaMigration, checkAuthSchema } from "./better-auth/auth-schema.js";
32
41
  import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
33
- import { coalesce } from "./changes/coalesce.js";
34
- import { recordOf, resourceChange } from "./changes/resource-change.js";
35
- import { eventMatcher } from "./changes/match-events.js";
42
+ import { webhooks } from "./webhooks/table/plugin.js";
43
+ import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
36
44
  import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
37
45
  import { functionApp } from "./functions/function-app.js";
38
46
  import { readJson } from "./functions/read-json.js";
@@ -40,4 +48,6 @@ import { verifySignature, verifyStandardWebhook } from "./functions/verify-signa
40
48
  import { requireEnv } from "./functions/require-env.js";
41
49
  import { createFileCleanup } from "./files/file-cleanup.js";
42
50
  import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
43
- export { HttpProblem, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, issueToken, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, normalizeRoles, notFound, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, storageCleanupSchedule, streamChatCompletion, unauthorized, verifySignature, verifyStandardWebhook, widget, withCacheBreakpoints };
51
+ import { startJobs } from "./jobs/runtime.js";
52
+ import { jobsDrainApp } from "./jobs/drain-route.js";
53
+ export { FatalJobError, HttpProblem, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webhooks, webhooksMigration, widget, withCacheBreakpoints };
@@ -0,0 +1,10 @@
1
+ import { EnqueueOptions, JobDefinition } from "./job-types.js";
2
+ import { Db } from "@protobase/query";
3
+ //#region src/jobs/current.d.ts
4
+ /** What `defineJob(...).enqueue` reaches: the jobs runtime this process started. */
5
+ export type JobsEnqueuer = {
6
+ enqueue(definition: JobDefinition, db: Db, payload: unknown, options?: EnqueueOptions): Promise<string>;
7
+ /** Has this process's worker of the job look for due jobs now rather than at its next poll; nothing without one. */
8
+ wake(name: string): void;
9
+ };
10
+ //#endregion
@@ -0,0 +1,20 @@
1
+ //#region src/jobs/current.ts
2
+ const key = Symbol.for("protobase.jobs");
3
+ const holder = globalThis;
4
+ /** The running jobs runtime; enqueueing needs one, started by `protobase dev`, `serve` and `worker`, or by `startJobs`. */
5
+ const currentJobs = () => {
6
+ const runtime = holder[key];
7
+ if (!runtime) throw new Error("Jobs are not running in this process: start them with startJobs(), as protobase dev, serve and worker do");
8
+ return runtime;
9
+ };
10
+ /** The running jobs runtime, if this process started one. */
11
+ const runningJobs = () => holder[key];
12
+ const setCurrentJobs = (runtime) => {
13
+ if (holder[key]) throw new Error("Jobs are already running in this process; stop them before starting them again");
14
+ holder[key] = runtime;
15
+ };
16
+ const clearCurrentJobs = (runtime) => {
17
+ if (holder[key] === runtime) delete holder[key];
18
+ };
19
+ //#endregion
20
+ export { clearCurrentJobs, currentJobs, runningJobs, setCurrentJobs };
@@ -0,0 +1,36 @@
1
+ import { CompiledQuery } from "kysely";
2
+ //#region src/jobs/database.ts
3
+ const block = /^\s*BEGIN;\s*(SET LOCAL lock_timeout = \d+);\s*(SET LOCAL idle_in_transaction_session_timeout = \d+);\s*(SELECT pg_advisory_xact_lock\([^;]*\);\s*)?([\s\S]+?);\s*COMMIT;\s*$/;
4
+ /**
5
+ * The statements of one of pg-boss's transaction texts, in order, or `undefined` for any other text. A text that
6
+ * starts a transaction and is not in that shape is an error, so a pg-boss upgrade that changes it fails loudly.
7
+ */
8
+ const transactionStatements = (text) => {
9
+ if (!/^\s*BEGIN;/.test(text)) return void 0;
10
+ const match = block.exec(text);
11
+ if (!match) throw new Error(`Unexpected pg-boss transaction:\n${text}`);
12
+ const [, lockTimeout, idleTimeout, lock, statement] = match;
13
+ return [
14
+ lockTimeout,
15
+ idleTimeout,
16
+ ...lock ? [lock.trim().replace(/;$/, "")] : [],
17
+ statement
18
+ ];
19
+ };
20
+ /**
21
+ * pg-boss's database over the app's own Kysely pool or transaction: one connection pool for the app and its jobs, the
22
+ * same driver on Node, Bun and PGlite. Every statement runs on its own, or inside one transaction for pg-boss's
23
+ * transaction texts, which works through a pooler in transaction mode: pg-boss takes only transaction-scoped locks.
24
+ */
25
+ const jobsDatabase = (db) => ({ executeSql: async (text, values) => {
26
+ if (values && values.length > 0) return { rows: [...(await db.executeQuery(CompiledQuery.raw(text, values))).rows] };
27
+ const statements = transactionStatements(text);
28
+ if (!statements) return { rows: [...(await db.executeQuery(CompiledQuery.raw(text))).rows] };
29
+ return db.transaction().execute(async (trx) => {
30
+ let rows = [];
31
+ for (const statement of statements) rows = [...(await trx.executeQuery(CompiledQuery.raw(statement))).rows];
32
+ return { rows };
33
+ });
34
+ } });
35
+ //#endregion
36
+ export { jobsDatabase, transactionStatements };
@@ -0,0 +1,8 @@
1
+ import { JobDefinition, JobOptions } from "./job-types.js";
2
+ //#region src/jobs/define-job.d.ts
3
+ /** A job the app's workers run: export it from the module that `protobase.config.ts` passes as `jobs`. */
4
+ export declare const defineJob: <Payload = undefined>(options: JobOptions<Payload>) => JobDefinition<Payload>;
5
+ export declare const isJobDefinition: (value: unknown) => value is JobDefinition;
6
+ /** The job definitions among a module's exports, such as `import * as jobs from './config/jobs'`, each name once. */
7
+ export declare const jobDefinitions: (module: Record<string, unknown>) => JobDefinition[];
8
+ //#endregion
@@ -0,0 +1,38 @@
1
+ import { currentJobs } from "./current.js";
2
+ import { nextRun } from "./schedule.js";
3
+ //#region src/jobs/define-job.ts
4
+ const namePattern = /^[\w.\-/]+$/;
5
+ const checkSchedule = (name, schedule, timezone) => {
6
+ try {
7
+ nextRun(schedule, timezone, /* @__PURE__ */ new Date());
8
+ } catch (error) {
9
+ if (!(error instanceof Error)) throw error;
10
+ throw new Error(`Job "${name}" has the schedule "${schedule}" in ${timezone}, which is not a cron schedule there: ${error.message}`, { cause: error });
11
+ }
12
+ };
13
+ /** A job the app's workers run: export it from the module that `protobase.config.ts` passes as `jobs`. */
14
+ const defineJob = (options) => {
15
+ if (!namePattern.test(options.name)) throw new Error(`Job name "${options.name}" may only hold letters, digits, "_", "-", "." and "/"`);
16
+ if (options.schedule !== void 0) checkSchedule(options.name, options.schedule, options.timezone ?? "UTC");
17
+ const definition = {
18
+ kind: "job",
19
+ name: options.name,
20
+ options,
21
+ enqueue: async (db, payload, enqueueOptions) => currentJobs().enqueue(definition, db, payload, enqueueOptions)
22
+ };
23
+ return definition;
24
+ };
25
+ const isJobDefinition = (value) => typeof value === "object" && value !== null && value.kind === "job" && typeof value.enqueue === "function";
26
+ /** The job definitions among a module's exports, such as `import * as jobs from './config/jobs'`, each name once. */
27
+ const jobDefinitions = (module) => {
28
+ const byName = /* @__PURE__ */ new Map();
29
+ for (const value of Object.values(module)) {
30
+ if (!isJobDefinition(value)) continue;
31
+ const known = byName.get(value.name);
32
+ if (known && known !== value) throw new Error(`Two jobs are named "${value.name}"; rename one of them`);
33
+ byName.set(value.name, value);
34
+ }
35
+ return [...byName.values()];
36
+ };
37
+ //#endregion
38
+ export { defineJob, isJobDefinition, jobDefinitions };
@@ -0,0 +1,21 @@
1
+ import { JobsRuntime } from "./runtime.js";
2
+ //#region src/jobs/drain-route.d.ts
3
+ /**
4
+ * `POST /api/jobs/drain?seconds=25`: runs the app's due jobs until none is left or `seconds` have passed, and answers
5
+ * `{ drained, remaining, nextDue }`. For the operator of a host that lets the app sleep and wakes it for its jobs: it
6
+ * takes the operator's `token` as a bearer token, never a user's.
7
+ */
8
+ export declare const jobsDrainApp: ({ runtime, token }: {
9
+ runtime: JobsRuntime;
10
+ token: string;
11
+ }) => import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, {
12
+ "/api/jobs/drain": {
13
+ $post: {
14
+ input: {};
15
+ output: {};
16
+ outputFormat: string;
17
+ status: import("hono/utils/http-status").StatusCode;
18
+ };
19
+ };
20
+ }, "/", "/api/jobs/drain">;
21
+ //#endregion
@@ -0,0 +1,28 @@
1
+ import { HttpProblem, badRequest, problemResponse, unauthorized } from "../problem.js";
2
+ import { Hono } from "hono";
3
+ import { createHash, timingSafeEqual } from "node:crypto";
4
+ //#region src/jobs/drain-route.ts
5
+ const drainPath = "/api/jobs/drain";
6
+ const digest = (text) => createHash("sha256").update(text).digest();
7
+ const sameToken = (given, token) => given !== void 0 && timingSafeEqual(digest(given), digest(token));
8
+ const maxSeconds = 300;
9
+ /**
10
+ * `POST /api/jobs/drain?seconds=25`: runs the app's due jobs until none is left or `seconds` have passed, and answers
11
+ * `{ drained, remaining, nextDue }`. For the operator of a host that lets the app sleep and wakes it for its jobs: it
12
+ * takes the operator's `token` as a bearer token, never a user's.
13
+ */
14
+ const jobsDrainApp = ({ runtime, token }) => new Hono().post(drainPath, async (c) => {
15
+ const problem = (error) => problemResponse(error, c.req.path);
16
+ if (!sameToken(c.req.header("authorization")?.replace(/^Bearer /, ""), token)) return problem(unauthorized("Send the operator token as a bearer token"));
17
+ const seconds = Number(c.req.query("seconds") ?? 25);
18
+ if (!Number.isFinite(seconds) || seconds <= 0 || seconds > maxSeconds) return problem(badRequest("invalid-seconds", `seconds must be more than 0 and at most ${maxSeconds}`));
19
+ if (!runtime.works) return problem(new HttpProblem(409, "jobs-elsewhere", "Conflict", "This process runs no jobs (PROTOBASE_JOBS=off); drain the one that does"));
20
+ const { drained, remaining, nextDue } = await runtime.drain(seconds);
21
+ return c.json({
22
+ drained,
23
+ remaining,
24
+ nextDue: nextDue?.toISOString() ?? null
25
+ });
26
+ });
27
+ //#endregion
28
+ export { drainPath, jobsDrainApp };