better-supabase 0.3.0 → 0.5.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.
- package/README.md +9 -6
- package/bin/better-supabase.js +4 -0
- package/dist/{base64-CQeB1eSf.js → base64-D2gsioCY.js} +2 -2
- package/dist/channel-FQW-CnGR.d.ts +70 -0
- package/dist/claims-BAf6c7is.js +48 -0
- package/dist/cli/_format-Cax5EsM9.js +101 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +48 -0
- package/dist/cli/codemod-DqV7612m.js +388 -0
- package/dist/cli/command-CK14fP91.js +502 -0
- package/dist/cli/compare-BmIWVEui.js +9 -0
- package/dist/cli/config-CPVpXP5I.js +1965 -0
- package/dist/cli/config-dtoekM0v.js +2 -0
- package/dist/cli/db-DOBPt660.js +52 -0
- package/dist/cli/diff-SjkO5rNr.js +752 -0
- package/dist/cli/dist-DBPdlPYK.js +1112 -0
- package/dist/cli/dist-DreZ24Sz.js +129 -0
- package/dist/cli/dist-xn4wlboc.js +732 -0
- package/dist/cli/doctor-L7Z8mYTx.js +2863 -0
- package/dist/cli/env-DmKv_G4A.js +108 -0
- package/dist/cli/env-jIFUADLY.js +511 -0
- package/dist/cli/errors-4v4DTtda.js +56 -0
- package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
- package/dist/cli/gen-BJc9vdyu.js +2 -0
- package/dist/cli/gen-C2osRcr5.js +2204 -0
- package/dist/cli/index.d.ts +493 -0
- package/dist/cli/index.js +184 -0
- package/dist/cli/init-Saf5-R9T.js +550 -0
- package/dist/cli/introspect--H7oGmcd.js +81 -0
- package/dist/cli/io-k41_z-jT.js +24 -0
- package/dist/cli/json5-ChRycquj.js +787 -0
- package/dist/cli/jsonc-BUniUpOk.js +2 -0
- package/dist/cli/jsonc-D5Fu67jw.js +45 -0
- package/dist/cli/keys-ByKHnvP9.js +83 -0
- package/dist/cli/ohash-D7bG_EOb.js +277 -0
- package/dist/cli/openapi-Cp5Fyta-.js +66 -0
- package/dist/cli/project-B_GeXyHh.js +70 -0
- package/dist/cli/prompts-BNcNfAve.js +1142 -0
- package/dist/cli/prompts-BTYQGunc.js +2 -0
- package/dist/cli/read-sets-BfMQcKy8.js +289 -0
- package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
- package/dist/cli/run-BKR2W_79.js +351 -0
- package/dist/cli/seed-CUlqWh-8.js +98 -0
- package/dist/cli/skills-LJJIhLFR.js +137 -0
- package/dist/cli/sql-DzPbfngx.js +348 -0
- package/dist/cli/style-C0NFdJRL.js +29 -0
- package/dist/cli/toml-DB-MNnFs.js +424 -0
- package/dist/cli/version-BDJuO0UR.js +4 -0
- package/dist/cli/yaml-CYCbJ6et.js +2252 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +15 -22
- package/dist/clone-BfgmLU90.js +21 -0
- package/dist/{compiler-2zqGzxVZ.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
- package/dist/config/index.d.ts +4 -4
- package/dist/config/index.js +5 -5
- package/dist/{config-DOsGYSiT.js → config-DreERlKo.js} +1 -1
- package/dist/{config-C1CYcm6o.d.ts → config-T_ejwyd3.d.ts} +176 -11
- package/dist/{define-DUbydmJa.d.ts → define-Bp_BZa5h.d.ts} +37 -29
- package/dist/{define-DF341GDj.js → define-cNKzqwL_.js} +356 -89
- package/dist/edge/index.d.ts +20 -13
- package/dist/edge/index.js +55 -26
- package/dist/{entitlements-DvJXoI02.d.ts → entitlements-DUxU4oHe.d.ts} +2 -2
- package/dist/env/index.d.ts +2 -2
- package/dist/env/index.js +34 -1
- package/dist/{errors-Bl3rkOzo.js → errors-DtwXa6b8.js} +2 -1
- package/dist/events/index.d.ts +3 -2
- package/dist/events/index.js +69 -16
- package/dist/events-aIO7vsIk.d.ts +230 -0
- package/dist/executor-BunPOO-k.d.ts +402 -0
- package/dist/{executor-B26lgbHD.d.ts → executor-Cghdstwu.d.ts} +1 -2
- package/dist/{executor-C57jx2pn.js → executor-XycESQPH.js} +3 -3
- package/dist/hono/index.d.ts +27 -23
- package/dist/hono/index.js +46 -11
- package/dist/impersonation-VfaGc4Q_.js +41 -0
- package/dist/{index-BNsABq7P.d.ts → index-BGdgBPdf.d.ts} +66 -25
- package/dist/{index-qriRM9cZ.d.ts → index-C7f8cOBM.d.ts} +4 -4
- package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
- package/dist/{index-GV77NpRP.d.ts → index-DOESKG27.d.ts} +34 -9
- package/dist/{index-CriFFJ_p.d.ts → index-DgILJRXj.d.ts} +43 -17
- package/dist/{index-UJ-LUxtI.d.ts → index-DmXSxbLi.d.ts} +14 -14
- package/dist/{index-Cf-D2jk1.d.ts → index-Dxn2a5cw.d.ts} +2 -2
- package/dist/{index-CTTMS6_1.d.ts → index-SaTsEfGP.d.ts} +86 -9
- package/dist/{index-CADlNHK-.d.ts → index-fag8VJZ2.d.ts} +5 -6
- package/dist/index.d.ts +34 -17
- package/dist/index.js +15 -14
- package/dist/jobs/index.d.ts +147 -95
- package/dist/jobs/index.js +773 -250
- package/dist/{json-schema--aOXAGfU.js → json-schema-m1fiwKgN.js} +23 -2
- package/dist/kit-events-CTFDfSwb.js +60 -0
- package/dist/kit-transport-B82k1djG.d.ts +33 -0
- package/dist/kit-transport-DJlHQUBU.js +55 -0
- package/dist/{config-Be4GwsqI.js → kits-CGLLrNPd.js} +38 -10
- package/dist/list/index.d.ts +1 -1
- package/dist/list/index.js +3 -3
- package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
- package/dist/{live-QE3UPbGm.d.ts → live-D9W2M5aO.d.ts} +16 -15
- package/dist/lookup-BaP5bI27.js +18 -0
- package/dist/mcp/index.d.ts +39 -22
- package/dist/mcp/index.js +39 -25
- package/dist/{mfa-x3V3vGh-.d.ts → mfa-CusDgPaD.d.ts} +5 -1
- package/dist/next/image/index.js +6 -3
- package/dist/next/index.d.ts +60 -36
- package/dist/next/index.js +167 -50
- package/dist/notifications/index.d.ts +162 -0
- package/dist/notifications/index.js +264 -0
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +6 -6
- package/dist/orgs/index.d.ts +132 -0
- package/dist/orgs/index.js +245 -0
- package/dist/orpc/index.d.ts +37 -20
- package/dist/orpc/index.js +24 -11
- package/dist/otel/index.d.ts +24 -5
- package/dist/otel/index.js +124 -64
- package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
- package/dist/permdock-sql-DgOfeixi.js +51 -0
- package/dist/plugin-DwARMuQp.js +59 -0
- package/dist/{plugin-DVRB8qNE.d.ts → plugin-o144f322.d.ts} +17 -5
- package/dist/plugins/actor/index.d.ts +5 -2
- package/dist/plugins/actor/index.js +22 -8
- package/dist/plugins/rules/index.d.ts +2 -2
- package/dist/plugins/rules/index.js +52 -17
- package/dist/plugins/soft-delete/index.d.ts +3 -3
- package/dist/plugins/soft-delete/index.js +14 -5
- package/dist/plugins/tenant/index.d.ts +3 -3
- package/dist/plugins/tenant/index.js +45 -21
- package/dist/plugins/timestamps/index.d.ts +1 -1
- package/dist/plugins/timestamps/index.js +4 -3
- package/dist/plugins/validation/index.d.ts +2 -2
- package/dist/plugins/validation/index.js +26 -6
- package/dist/{pool-B9qsLyFR.d.ts → pool-C9voM4Kw.d.ts} +24 -6
- package/dist/postgres/index.d.ts +5 -5
- package/dist/postgres/index.js +20 -10
- package/dist/{postgrest-DmBlevh-.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
- package/dist/{problem-CGt1ADix.js → problem-BW1KhzSG.js} +2 -1
- package/dist/{problem-Z2RTQjZD.d.ts → problem-Blxe8S31.d.ts} +1 -1
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +3 -3
- package/dist/{query-BdKLTPlj.js → query-CCbo44lG.js} +18 -18
- package/dist/queue-BYUCjr59.d.ts +252 -0
- package/dist/react/index.d.ts +8 -7
- package/dist/react/index.js +4 -3
- package/dist/react/server.d.ts +16 -5
- package/dist/react/server.js +12 -4
- package/dist/react/session.d.ts +10 -4
- package/dist/react/session.js +11 -3
- package/dist/{react-DNBYNazu.js → react-C_s7kiNz.js} +138 -46
- package/dist/{read-set-D0qBcboM.js → read-set-DQFt1N_Y.js} +71 -15
- package/dist/realtime/index.d.ts +3 -3
- package/dist/realtime/index.js +14 -12
- package/dist/{index-kpgopLNv.d.ts → refresh-BTrQ_ZTp.d.ts} +6 -3
- package/dist/{resolve-BRciX2jK.d.ts → resolve-C15TJq77.d.ts} +22 -11
- package/dist/{resource-D30rp0GN.js → resource-CRxxwkQq.js} +24 -9
- package/dist/{resource-CwVgiI5q.d.ts → resource-D8iqWMr-.d.ts} +6 -6
- package/dist/respond-D2OQl62-.d.ts +371 -0
- package/dist/{result-DWXatkd6.js → result-BKiliPY7.js} +2 -2
- package/dist/server/index.d.ts +17 -15
- package/dist/server/index.js +16 -22
- package/dist/server-Csx2sRjg.js +655 -0
- package/dist/{shared-_6-uPL7E.js → shared-D1dRUrAg.js} +19 -4
- package/dist/signers-Cc6r98SW.d.ts +162 -0
- package/dist/{simplify-LakqK1vH.js → simplify-_0oUKqCd.js} +27 -95
- package/dist/sql/index.d.ts +247 -10
- package/dist/sql/index.js +8591 -780
- package/dist/{sql-BIgk12ew.js → sql-Coh6n4cC.js} +11 -3
- package/dist/ssr/index.d.ts +4 -4
- package/dist/ssr/index.js +1 -2
- package/dist/{standard-FUi3Z3-7.js → standard-D7r6dMUu.js} +6 -3
- package/dist/{standard-eudBiRk1.d.ts → standard-J4YgMOLm.d.ts} +1 -1
- package/dist/{stats-D78UQ8Yu.js → stats-Bqob3WqT.js} +34 -9
- package/dist/storage/index.d.ts +4 -4
- package/dist/storage/index.js +174 -27
- package/dist/support-DT-OhSxD.d.ts +88 -0
- package/dist/support-Dk6vdmEw.js +291 -0
- package/dist/support-view-Da5peKFg.d.ts +17 -0
- package/dist/support-view-g4YLBeqX.js +18 -0
- package/dist/{tables-Dhha3BXM.js → tables-Cb-3_VWc.js} +14 -0
- package/dist/{permdock-sql-Bta_G0v-.js → template-TxrGI5jR.js} +2 -31
- package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
- package/dist/{temporal-required-CMxeSVPo.js → temporal-required-CAt4Dc9Z.js} +2 -2
- package/dist/testing/index.d.ts +113 -24
- package/dist/testing/index.js +375 -47
- package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
- package/dist/user-change-CHFEUro1.js +49 -0
- package/dist/{webhooks-CAIFQSTM.js → verify-Ce49ETPz.js} +38 -19
- package/dist/{version-BfpG2gks.js → version-DM_Roffb.js} +1 -1
- package/dist/{view-3Po_PVlr.d.ts → view-BEf0diC5.d.ts} +10 -5
- package/dist/{mfa-BxoR1RkN.js → view-Bq64ki8-.js} +210 -64
- package/dist/webhooks/index.d.ts +136 -5
- package/dist/webhooks/index.js +588 -2
- package/dist/wire-D2dT86vi.js +35 -0
- package/package.json +50 -10
- package/schemas/config-v1.json +131 -1
- package/schemas/doctor-report-v1.json +10 -0
- package/skills/better-supabase/SKILL.md +60 -10
- package/skills/better-supabase/references/plugins.md +13 -14
- package/skills/better-supabase/references/troubleshooting.md +25 -4
- package/skills/better-supabase-api/SKILL.md +74 -19
- package/skills/better-supabase-api/references/adapters.md +48 -27
- package/skills/better-supabase-auth/SKILL.md +149 -0
- package/skills/better-supabase-auth/references/permdock.md +100 -0
- package/skills/better-supabase-testing/SKILL.md +58 -4
- package/dist/claims-CoTobNC-.js +0 -25
- package/dist/events-Do8jXh4D.d.ts +0 -84
- package/dist/executor-DynmIWhq.d.ts +0 -195
- package/dist/invalidate-DbxuL2jL.js +0 -18
- package/dist/permdock-sql-DImJULY-.d.ts +0 -17
- package/dist/plugin-BqR6wKMB.js +0 -23
- package/dist/respond-C44EZAJo.js +0 -399
- package/dist/respond-DAkPIdTH.d.ts +0 -175
- package/dist/result-c2OYgCV2.d.ts +0 -203
- package/dist/view-wZ-9qIUz.js +0 -45
package/dist/jobs/index.js
CHANGED
|
@@ -1,19 +1,168 @@
|
|
|
1
|
-
import { c as mapDbError, n as dbError, t as DbException } from "../errors-
|
|
2
|
-
import { n as nowInstant } from "../temporal-
|
|
3
|
-
import { t as temporal } from "../temporal-required-
|
|
4
|
-
import { i as ok, n as err, o as toDbError, t as AsyncResult } from "../result-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
1
|
+
import { c as mapDbError, n as dbError, t as DbException } from "../errors-DtwXa6b8.js";
|
|
2
|
+
import { n as nowInstant } from "../temporal-CVlIV2DZ.js";
|
|
3
|
+
import { t as temporal } from "../temporal-required-CAt4Dc9Z.js";
|
|
4
|
+
import { i as ok, n as err, o as toDbError, t as AsyncResult } from "../result-BKiliPY7.js";
|
|
5
|
+
import { n as allTenantsContext, s as tenantOf } from "../plugin-DwARMuQp.js";
|
|
6
|
+
import { t as validate } from "../standard-D7r6dMUu.js";
|
|
7
|
+
import { a as problemResponse } from "../problem-BW1KhzSG.js";
|
|
8
|
+
import { i as tenantClaimPaths, n as claimAt } from "../claims-BAf6c7is.js";
|
|
9
|
+
import { r as sqlIdent } from "../template-TxrGI5jR.js";
|
|
10
|
+
import { t as fromPgError } from "../executor-XycESQPH.js";
|
|
11
|
+
import { c as verifyWebhook, s as verifySharedSecret } from "../verify-Ce49ETPz.js";
|
|
12
|
+
//#region src/jobs/cron.ts
|
|
13
|
+
const MACROS = {
|
|
14
|
+
"@yearly": "0 0 1 1 *",
|
|
15
|
+
"@annually": "0 0 1 1 *",
|
|
16
|
+
"@monthly": "0 0 1 * *",
|
|
17
|
+
"@weekly": "0 0 * * 0",
|
|
18
|
+
"@daily": "0 0 * * *",
|
|
19
|
+
"@midnight": "0 0 * * *",
|
|
20
|
+
"@hourly": "0 * * * *"
|
|
21
|
+
};
|
|
22
|
+
const MONTHS = [
|
|
23
|
+
"jan",
|
|
24
|
+
"feb",
|
|
25
|
+
"mar",
|
|
26
|
+
"apr",
|
|
27
|
+
"may",
|
|
28
|
+
"jun",
|
|
29
|
+
"jul",
|
|
30
|
+
"aug",
|
|
31
|
+
"sep",
|
|
32
|
+
"oct",
|
|
33
|
+
"nov",
|
|
34
|
+
"dec"
|
|
35
|
+
];
|
|
36
|
+
const WEEKDAYS = [
|
|
37
|
+
"sun",
|
|
38
|
+
"mon",
|
|
39
|
+
"tue",
|
|
40
|
+
"wed",
|
|
41
|
+
"thu",
|
|
42
|
+
"fri",
|
|
43
|
+
"sat"
|
|
44
|
+
];
|
|
45
|
+
/** Replaces `JAN` or `MON` style names with their numbers. */
|
|
46
|
+
function named(text, names, first) {
|
|
47
|
+
return text.replaceAll(/[a-z]{3}/gi, (name) => {
|
|
48
|
+
const index = names.indexOf(name.toLowerCase());
|
|
49
|
+
return index === -1 ? name : String(index + first);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const INTERVAL = /^(\d+)\s+(second|minute|hour)s?$/i;
|
|
53
|
+
/** Gives up when no minute matches within this many days (`0 0 30 2 *`). */
|
|
54
|
+
const SEARCH_DAYS = 1830;
|
|
55
|
+
function invalid(expression, reason) {
|
|
56
|
+
return /* @__PURE__ */ new TypeError(`Invalid cron "${expression}": ${reason}`);
|
|
57
|
+
}
|
|
58
|
+
function field(expression, text, min, max) {
|
|
59
|
+
const values = /* @__PURE__ */ new Set();
|
|
60
|
+
for (const part of text.split(",")) {
|
|
61
|
+
const match = /^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/.exec(part);
|
|
62
|
+
if (!match) throw invalid(expression, `"${part}" is not a cron field`);
|
|
63
|
+
const [, range = "", stepText] = match;
|
|
64
|
+
const step = stepText === void 0 ? 1 : Number(stepText);
|
|
65
|
+
let from = min;
|
|
66
|
+
let to = max;
|
|
67
|
+
if (range !== "*") {
|
|
68
|
+
const [start = "", end] = range.split("-");
|
|
69
|
+
from = Number(start);
|
|
70
|
+
to = end === void 0 ? stepText === void 0 ? from : max : Number(end);
|
|
71
|
+
}
|
|
72
|
+
if (step < 1 || from < min || to > max || from > to) throw invalid(expression, `"${part}" is outside ${String(min)}-${String(max)}`);
|
|
73
|
+
for (let value = from; value <= to; value += step) values.add(value);
|
|
74
|
+
}
|
|
75
|
+
return [...values].toSorted((a, b) => a - b);
|
|
76
|
+
}
|
|
77
|
+
function parse(expression) {
|
|
78
|
+
const parts = (MACROS[expression.trim().toLowerCase()] ?? expression).trim().split(/\s+/);
|
|
79
|
+
if (parts.length !== 5) throw invalid(expression, "expected 5 fields (minute hour day month weekday)");
|
|
80
|
+
const [minute = "", hour = "", day = "", monthText = "", weekdayText = ""] = parts;
|
|
81
|
+
const month = named(monthText, MONTHS, 1);
|
|
82
|
+
const weekday = named(weekdayText, WEEKDAYS, 0);
|
|
83
|
+
return {
|
|
84
|
+
minutes: field(expression, minute, 0, 59),
|
|
85
|
+
hours: field(expression, hour, 0, 23),
|
|
86
|
+
days: new Set(field(expression, day, 1, 31)),
|
|
87
|
+
months: new Set(field(expression, month, 1, 12)),
|
|
88
|
+
weekdays: new Set(field(expression, weekday, 0, 7).map((value) => value === 0 ? 7 : value)),
|
|
89
|
+
daysRestricted: day !== "*",
|
|
90
|
+
weekdaysRestricted: weekday !== "*"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function dayMatches(fields, date) {
|
|
94
|
+
if (!fields.months.has(date.month)) return false;
|
|
95
|
+
const day = fields.days.has(date.day);
|
|
96
|
+
const weekday = fields.weekdays.has(date.dayOfWeek);
|
|
97
|
+
if (fields.daysRestricted && fields.weekdaysRestricted) return day || weekday;
|
|
98
|
+
if (fields.daysRestricted) return day;
|
|
99
|
+
if (fields.weekdaysRestricted) return weekday;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** The first matching minute of `date` after `after`, from `start` on its first day. */
|
|
103
|
+
function firstRunOn(fields, date, start, timeZone, after) {
|
|
104
|
+
for (const hour of fields.hours) {
|
|
105
|
+
if (start && hour < start.hour) continue;
|
|
106
|
+
for (const minute of fields.minutes) {
|
|
107
|
+
if (hour === start?.hour && minute < start.minute) continue;
|
|
108
|
+
const instant = date.toPlainDateTime({
|
|
109
|
+
hour,
|
|
110
|
+
minute
|
|
111
|
+
}).toZonedDateTime(timeZone, { disambiguation: "compatible" }).toInstant();
|
|
112
|
+
if (temporal().Instant.compare(instant, after) > 0) return instant;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Throws a `TypeError` unless `expression` is a schedule `nextCronRun`
|
|
118
|
+
* understands: five cron fields, a macro such as `@daily`, or an interval
|
|
119
|
+
* such as `30 seconds`.
|
|
120
|
+
*/
|
|
121
|
+
function assertCron(expression) {
|
|
122
|
+
if (!INTERVAL.test(expression.trim())) parse(expression);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The first run of `expression` after `after`, in `timeZone`.
|
|
126
|
+
*
|
|
127
|
+
* Accepts five cron fields (lists, ranges and steps; a restricted day of
|
|
128
|
+
* month and day of week match either, as in cron), the macros `@hourly`,
|
|
129
|
+
* `@daily`, `@weekly`, `@monthly` and `@yearly`, and intervals such as
|
|
130
|
+
* `30 seconds`, `5 minutes` or `2 hours`, counted from `after`. A local time
|
|
131
|
+
* that a DST change skips runs at the same offset after the gap.
|
|
132
|
+
*/
|
|
133
|
+
function nextCronRun(expression, timeZone, after) {
|
|
134
|
+
const interval = INTERVAL.exec(expression.trim());
|
|
135
|
+
if (interval) {
|
|
136
|
+
const amount = Number(interval[1]);
|
|
137
|
+
if (amount < 1) throw invalid(expression, "the interval must be positive");
|
|
138
|
+
const unit = interval[2].toLowerCase();
|
|
139
|
+
return after.add(unit === "second" ? { seconds: amount } : unit === "minute" ? { minutes: amount } : { hours: amount });
|
|
140
|
+
}
|
|
141
|
+
const fields = parse(expression);
|
|
142
|
+
const start = after.toZonedDateTimeISO(timeZone).toPlainDateTime().round({
|
|
143
|
+
smallestUnit: "minute",
|
|
144
|
+
roundingMode: "floor"
|
|
145
|
+
}).add({ minutes: 1 });
|
|
146
|
+
let date = start.toPlainDate();
|
|
147
|
+
for (let offset = 0; offset < SEARCH_DAYS; offset += 1) {
|
|
148
|
+
const found = dayMatches(fields, date) && firstRunOn(fields, date, offset === 0 ? start : void 0, timeZone, after);
|
|
149
|
+
if (found) return found;
|
|
150
|
+
date = date.add({ days: 1 });
|
|
151
|
+
}
|
|
152
|
+
throw invalid(expression, "it never matches a date");
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/jobs/shared.ts
|
|
156
|
+
function asDbError(cause) {
|
|
157
|
+
const raw = fromPgError(cause);
|
|
158
|
+
return raw ? mapDbError(raw) : toDbError(cause);
|
|
159
|
+
}
|
|
10
160
|
function run(fn) {
|
|
11
161
|
return AsyncResult.from(async () => {
|
|
12
162
|
try {
|
|
13
163
|
return ok(await fn());
|
|
14
164
|
} catch (cause) {
|
|
15
|
-
|
|
16
|
-
return err(raw ? mapDbError(raw) : toDbError(cause));
|
|
165
|
+
return err(asDbError(cause));
|
|
17
166
|
}
|
|
18
167
|
});
|
|
19
168
|
}
|
|
@@ -36,254 +185,27 @@ const sleep = (ms, signal) => new Promise((resolve) => {
|
|
|
36
185
|
}
|
|
37
186
|
signal?.addEventListener("abort", done, { once: true });
|
|
38
187
|
});
|
|
39
|
-
function sqlTransport(sql) {
|
|
40
|
-
return {
|
|
41
|
-
name: "sql",
|
|
42
|
-
async send(queue, payload, delay, maxAttempts, dedupeKey) {
|
|
43
|
-
const [row] = await sql.queryRaw("select better_supabase.enqueue_job($1, $2, $3, $4, $5) as id", [
|
|
44
|
-
queue,
|
|
45
|
-
JSON.stringify(payload ?? {}),
|
|
46
|
-
delay,
|
|
47
|
-
maxAttempts,
|
|
48
|
-
dedupeKey ?? null
|
|
49
|
-
]);
|
|
50
|
-
return Number(row.id);
|
|
51
|
-
},
|
|
52
|
-
read: (queue, lease, batch) => sql.queryRaw("select * from better_supabase.claim_jobs($1, $2, $3)", [
|
|
53
|
-
queue,
|
|
54
|
-
lease,
|
|
55
|
-
batch
|
|
56
|
-
]),
|
|
57
|
-
async complete(job) {
|
|
58
|
-
const [row] = await sql.queryRaw("select better_supabase.complete_job($1, $2, $3) as done", [
|
|
59
|
-
job.queue,
|
|
60
|
-
job.id,
|
|
61
|
-
job.attempts
|
|
62
|
-
]);
|
|
63
|
-
return row?.done ?? false;
|
|
64
|
-
},
|
|
65
|
-
async fail(job, error, retryIn) {
|
|
66
|
-
const [row] = await sql.queryRaw("select better_supabase.fail_job($1, $2, $3, $4, $5) as status", [
|
|
67
|
-
job.queue,
|
|
68
|
-
job.id,
|
|
69
|
-
job.attempts,
|
|
70
|
-
error,
|
|
71
|
-
retryIn ?? null
|
|
72
|
-
]);
|
|
73
|
-
return row?.status ?? null;
|
|
74
|
-
},
|
|
75
|
-
async extend(job, lease) {
|
|
76
|
-
const [row] = await sql.queryRaw("select better_supabase.extend_job_lease($1, $2, $3, $4) as extended", [
|
|
77
|
-
job.queue,
|
|
78
|
-
job.id,
|
|
79
|
-
job.attempts,
|
|
80
|
-
lease
|
|
81
|
-
]);
|
|
82
|
-
return row?.extended ?? false;
|
|
83
|
-
},
|
|
84
|
-
async schedule(name, cron, queue, payload) {
|
|
85
|
-
await sql.queryRaw("select better_supabase.schedule_job($1, $2, $3, $4)", [
|
|
86
|
-
name,
|
|
87
|
-
cron,
|
|
88
|
-
queue,
|
|
89
|
-
JSON.stringify(payload ?? {})
|
|
90
|
-
]);
|
|
91
|
-
},
|
|
92
|
-
async unschedule(name) {
|
|
93
|
-
const [row] = await sql.queryRaw("select better_supabase.unschedule_job($1) as done", [name]);
|
|
94
|
-
return row?.done ?? false;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
function sqlOnly(feature) {
|
|
99
|
-
throw new DbException(dbError("invalid_request", `${feature} needs a SQL connection; pgmq_public (PostgREST) does not support it`));
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Supabase's `pgmq_public` RPCs (Integrations → Queues → "Expose Queues via
|
|
103
|
-
* PostgREST"). No lease check on complete, no retry delay and no
|
|
104
|
-
* deduplication: a failed job simply reappears when its lease ends.
|
|
105
|
-
*/
|
|
106
|
-
function postgrestTransport(client) {
|
|
107
|
-
const call = async (fn, args) => {
|
|
108
|
-
const { data, error } = await client.schema("pgmq_public").rpc(fn, args);
|
|
109
|
-
if (error) {
|
|
110
|
-
const message = typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
111
|
-
throw new Error(`pgmq_public.${fn}: ${message}`, { cause: error });
|
|
112
|
-
}
|
|
113
|
-
return data;
|
|
114
|
-
};
|
|
115
|
-
const archive = (job) => call("archive", {
|
|
116
|
-
queue_name: job.queue,
|
|
117
|
-
message_id: job.id
|
|
118
|
-
});
|
|
119
|
-
return {
|
|
120
|
-
name: "postgrest",
|
|
121
|
-
async send(queue, payload, delay, maxAttempts, dedupeKey) {
|
|
122
|
-
if (dedupeKey !== void 0) sqlOnly("dedupeKey");
|
|
123
|
-
const ids = await call("send", {
|
|
124
|
-
queue_name: queue,
|
|
125
|
-
message: {
|
|
126
|
-
payload: payload ?? {},
|
|
127
|
-
max_attempts: maxAttempts
|
|
128
|
-
},
|
|
129
|
-
sleep_seconds: delay
|
|
130
|
-
});
|
|
131
|
-
return Number(ids[0]);
|
|
132
|
-
},
|
|
133
|
-
async read(queue, lease, batch) {
|
|
134
|
-
return (await call("read", {
|
|
135
|
-
queue_name: queue,
|
|
136
|
-
sleep_seconds: lease,
|
|
137
|
-
n: batch
|
|
138
|
-
})).map((row) => ({
|
|
139
|
-
id: row.msg_id,
|
|
140
|
-
attempts: row.read_ct,
|
|
141
|
-
enqueued_at: row.enqueued_at,
|
|
142
|
-
visible_until: row.vt,
|
|
143
|
-
message: row.message
|
|
144
|
-
}));
|
|
145
|
-
},
|
|
146
|
-
complete: archive,
|
|
147
|
-
async fail(job) {
|
|
148
|
-
if (job.attempts < job.maxAttempts) return "queued";
|
|
149
|
-
await archive(job);
|
|
150
|
-
return "dead";
|
|
151
|
-
},
|
|
152
|
-
extend: () => Promise.resolve(false),
|
|
153
|
-
schedule: () => sqlOnly("schedule"),
|
|
154
|
-
unschedule: () => sqlOnly("unschedule")
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
188
|
/** pgmq and the inbox return `timestamptz` as text over RPC and as `Date` from `pg`. */
|
|
158
189
|
const toInstant = (value) => value instanceof Date ? temporal().Instant.fromEpochMilliseconds(value.getTime()) : temporal().Instant.from(value);
|
|
159
|
-
function toJob(queue, row) {
|
|
160
|
-
const message = row.message ?? {};
|
|
161
|
-
return {
|
|
162
|
-
id: Number(row.id),
|
|
163
|
-
queue,
|
|
164
|
-
payload: message.payload,
|
|
165
|
-
attempts: row.attempts,
|
|
166
|
-
maxAttempts: message.max_attempts ?? 5,
|
|
167
|
-
enqueuedAt: toInstant(row.enqueued_at),
|
|
168
|
-
visibleUntil: toInstant(row.visible_until),
|
|
169
|
-
lastError: message.last_error ?? null
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
190
|
function errorText(error) {
|
|
173
191
|
if (error instanceof Error) return error.message;
|
|
174
192
|
if (typeof error === "object" && error !== null && "message" in error) return String(error.message);
|
|
175
193
|
return String(error);
|
|
176
194
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return typeof source.queryRaw === "function";
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Typed jobs on Supabase Queues (pgmq), over the `jobs` SQL kit module.
|
|
183
|
-
*
|
|
184
|
-
* Pass a service SQL connection (`postgres.admin`, `ctx.sql`) for the full
|
|
185
|
-
* feature set, or a service-role Supabase client to go through the
|
|
186
|
-
* `pgmq_public` RPCs where no direct connection exists (edge functions).
|
|
187
|
-
* Queue names are lowercase letters, digits and underscores.
|
|
188
|
-
*/
|
|
189
|
-
function createJobs(source, queues) {
|
|
190
|
-
for (const name of Object.keys(queues)) if (!QUEUE_NAME.test(name)) throw new TypeError(`Queue "${name}": pgmq queue names are lowercase letters, digits and underscores (at most 47)`);
|
|
191
|
-
const transport = isSqlClient(source) ? sqlTransport(source) : postgrestTransport(source);
|
|
192
|
-
const schemaOf = (queue) => {
|
|
193
|
-
const schema = queues[queue];
|
|
194
|
-
if (!schema) throw new TypeError(`Unknown queue "${queue}". Queues: ${Object.keys(queues).join(", ")}`);
|
|
195
|
-
return schema;
|
|
196
|
-
};
|
|
197
|
-
const complete = (job) => run(() => transport.complete(job));
|
|
198
|
-
const fail = (job, error, failOptions = {}) => run(() => transport.fail(job, errorText(error), failOptions.retryIn));
|
|
199
|
-
const claim = (queue, claimOptions = {}) => run(async () => {
|
|
200
|
-
schemaOf(queue);
|
|
201
|
-
return (await transport.read(queue, claimOptions.lease ?? 300, claimOptions.batch ?? 1)).map((row) => toJob(queue, row));
|
|
202
|
-
});
|
|
203
|
-
const handle = async (job, handler, workOptions) => {
|
|
204
|
-
const controller = new AbortController();
|
|
205
|
-
const lease = workOptions.lease ?? 300;
|
|
206
|
-
const heartbeat = transport.name === "sql" ? setInterval(() => {
|
|
207
|
-
run(() => transport.extend(job, lease));
|
|
208
|
-
}, Math.max(1e3, lease * 1e3 / 2)) : void 0;
|
|
209
|
-
try {
|
|
210
|
-
const payload = await validate(schemaOf(job.queue), job.payload, "payload");
|
|
211
|
-
if (!payload.ok) {
|
|
212
|
-
await fail(job, payload.error.message);
|
|
213
|
-
workOptions.onError?.(payload.error, job);
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
const outcome = await handler(payload.data, {
|
|
217
|
-
...job,
|
|
218
|
-
payload: payload.data
|
|
219
|
-
}, controller.signal);
|
|
220
|
-
if (typeof outcome === "object" && outcome !== null && "ok" in outcome && outcome.ok === false && "error" in outcome) throw outcome.error;
|
|
221
|
-
await complete(job);
|
|
222
|
-
return true;
|
|
223
|
-
} catch (cause) {
|
|
224
|
-
controller.abort();
|
|
225
|
-
await fail(job, cause);
|
|
226
|
-
workOptions.onError?.(typeof cause === "object" && cause !== null && "kind" in cause ? cause : toDbError(cause), job);
|
|
227
|
-
return false;
|
|
228
|
-
} finally {
|
|
229
|
-
clearInterval(heartbeat);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
const loop = async (queue, handler, workOptions, forever) => {
|
|
233
|
-
const concurrency = Math.max(1, workOptions.concurrency ?? 1);
|
|
234
|
-
let succeeded = 0;
|
|
235
|
-
let failed = 0;
|
|
236
|
-
const lanes = Array.from({ length: concurrency }, async () => {
|
|
237
|
-
while (!workOptions.signal?.aborted) {
|
|
238
|
-
const claimed = await claim(queue, {
|
|
239
|
-
...workOptions,
|
|
240
|
-
batch: workOptions.batch ?? 1
|
|
241
|
-
});
|
|
242
|
-
if (!claimed.ok) throw new TypeError(claimed.error.message, { cause: claimed.error });
|
|
243
|
-
if (claimed.data.length === 0) {
|
|
244
|
-
if (!forever) return;
|
|
245
|
-
await sleep(workOptions.pollInterval ?? 1e3, workOptions.signal);
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
for (const job of claimed.data) if (await handle(job, handler, workOptions)) succeeded += 1;
|
|
249
|
-
else failed += 1;
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
await Promise.all(lanes);
|
|
253
|
-
return {
|
|
254
|
-
succeeded,
|
|
255
|
-
failed
|
|
256
|
-
};
|
|
257
|
-
};
|
|
258
|
-
return {
|
|
259
|
-
enqueue(queue, payload, enqueueOptions = {}) {
|
|
260
|
-
return AsyncResult.from(async () => {
|
|
261
|
-
const valid = await validate(schemaOf(queue), payload, "payload");
|
|
262
|
-
if (!valid.ok) return valid;
|
|
263
|
-
const delay = enqueueOptions.delay ?? (enqueueOptions.runAt ? Math.max(0, Math.ceil((enqueueOptions.runAt.epochMilliseconds - nowInstant().epochMilliseconds) / 1e3)) : 0);
|
|
264
|
-
return run(() => transport.send(queue, valid.data, delay, enqueueOptions.maxAttempts ?? 5, enqueueOptions.dedupeKey));
|
|
265
|
-
});
|
|
266
|
-
},
|
|
267
|
-
claim,
|
|
268
|
-
complete,
|
|
269
|
-
fail,
|
|
270
|
-
extend: (job, lease) => run(() => transport.extend(job, lease)),
|
|
271
|
-
schedule(name, cron, queue, payload) {
|
|
272
|
-
return AsyncResult.from(async () => {
|
|
273
|
-
const valid = await validate(schemaOf(queue), payload, "payload");
|
|
274
|
-
if (!valid.ok) return valid;
|
|
275
|
-
return run(() => transport.schedule(name, cron, queue, valid.data));
|
|
276
|
-
});
|
|
277
|
-
},
|
|
278
|
-
unschedule: (name) => run(() => transport.unschedule(name)),
|
|
279
|
-
drain: (queue, handler, drainOptions = {}) => loop(queue, handler, drainOptions, false),
|
|
280
|
-
work: (queue, handler, workOptions = {}) => loop(queue, handler, workOptions, true)
|
|
281
|
-
};
|
|
282
|
-
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/jobs/jobs.ts
|
|
283
197
|
async function sha256(text) {
|
|
284
198
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
285
199
|
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
286
200
|
}
|
|
201
|
+
const AUTH_COOKIE = /^sb-.+-auth-token(?:\.\d+)?$/;
|
|
202
|
+
/** A hash of the credentials the request carries, or `''` without any. */
|
|
203
|
+
async function callerScope(request) {
|
|
204
|
+
const authorization = request.headers.get("authorization");
|
|
205
|
+
const cookies = (request.headers.get("cookie") ?? "").split(";").map((pair) => pair.trim()).filter((pair) => AUTH_COOKIE.test(pair.split("=", 1)[0] ?? "")).sort();
|
|
206
|
+
const credential = authorization ?? cookies.join(";");
|
|
207
|
+
return credential === "" ? "" : `caller:${await sha256(credential)}`;
|
|
208
|
+
}
|
|
287
209
|
/** HTTP idempotency keys (IETF `Idempotency-Key` header) over the `idempotency` SQL kit module. */
|
|
288
210
|
function createIdempotency(sql, options = {}) {
|
|
289
211
|
const headerName = options.header ?? "idempotency-key";
|
|
@@ -323,7 +245,7 @@ function createIdempotency(sql, options = {}) {
|
|
|
323
245
|
const instance = new URL(request.url).pathname;
|
|
324
246
|
if (!key) return options.required ? problemResponse(dbError("invalid_request", `The ${headerName} header is required`, { code: "IDEMPOTENCY_KEY_MISSING" }), { instance }) : handler(request);
|
|
325
247
|
if (key.length > 255) return problemResponse(dbError("invalid_request", `The ${headerName} header is too long`, { code: "IDEMPOTENCY_KEY_INVALID" }), { instance });
|
|
326
|
-
const scope = typeof options.scope === "function" ? options.scope(request) : options.scope ??
|
|
248
|
+
const scope = typeof options.scope === "function" ? options.scope(request) : options.scope ?? await callerScope(request);
|
|
327
249
|
const body = await request.clone().text();
|
|
328
250
|
const fingerprint = await sha256(`${request.method} ${instance}\n${body}`);
|
|
329
251
|
const started = await begin(key, fingerprint, scope);
|
|
@@ -480,5 +402,606 @@ function entitlementMembers(sql, payload) {
|
|
|
480
402
|
if (customer === void 0) return AsyncResult.ok([]);
|
|
481
403
|
return run(() => sql.queryRaw("select user_id from better_supabase.entitlement_members($1) as user_id", [customer])).map((rows) => rows.map((row) => row.user_id));
|
|
482
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Deletes audit entries past their retention and returns how many. Without
|
|
407
|
+
* `retention` it is one `purge_audit_log` call (which honours an
|
|
408
|
+
* `audit_retention` SQL hook); with it, each tenant is purged with its own
|
|
409
|
+
* interval. Run it from a schedule until it returns 0.
|
|
410
|
+
*/
|
|
411
|
+
function purgeAuditLog(sql, options = {}) {
|
|
412
|
+
const olderThan = seconds(options.olderThan ?? "1 year");
|
|
413
|
+
const batch = options.batch ?? 1e4;
|
|
414
|
+
const purge = async (params, call) => {
|
|
415
|
+
const [row] = await sql.queryRaw(`select better_supabase.purge_audit_log(${call}) as n`, params);
|
|
416
|
+
return Number(row?.n ?? 0);
|
|
417
|
+
};
|
|
418
|
+
const { retention } = options;
|
|
419
|
+
if (!retention) return run(() => purge([olderThan, batch], "$1::interval, $2"));
|
|
420
|
+
return run(async () => {
|
|
421
|
+
const tenants = await sql.queryRaw("select tenant::text from better_supabase.audit_events_tenants($1::interval) as tenant", ["1 day"]);
|
|
422
|
+
let purged = 0;
|
|
423
|
+
for (const { tenant } of tenants) {
|
|
424
|
+
const days = await retention(tenant);
|
|
425
|
+
const keep = days === void 0 ? olderThan : `${days} days`;
|
|
426
|
+
purged += await purge([
|
|
427
|
+
keep,
|
|
428
|
+
batch,
|
|
429
|
+
tenant
|
|
430
|
+
], "$1::interval, $2, $3, true");
|
|
431
|
+
}
|
|
432
|
+
return purged;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/jobs/outbox.ts
|
|
437
|
+
function isOutboxRow(value) {
|
|
438
|
+
return typeof value === "object" && value !== null && "position" in value && "type" in value && "created_at" in value;
|
|
439
|
+
}
|
|
440
|
+
function toEvent(row) {
|
|
441
|
+
return {
|
|
442
|
+
position: row.position,
|
|
443
|
+
id: String(row.id),
|
|
444
|
+
type: row.type,
|
|
445
|
+
payload: row.payload,
|
|
446
|
+
source: row.source ?? null,
|
|
447
|
+
subject: row.subject ?? null,
|
|
448
|
+
tenant: row.tenant === void 0 || row.tenant === null ? null : String(row.tenant),
|
|
449
|
+
key: row.key ?? null,
|
|
450
|
+
actorId: row.actor_id ?? null,
|
|
451
|
+
createdAt: toInstant(row.created_at)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function toEvents(value) {
|
|
455
|
+
if (!Array.isArray(value)) return [];
|
|
456
|
+
return value.filter(isOutboxRow).map(toEvent);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* The CloudEvent for an outbox row: the row id as `id` (stable, so receivers
|
|
460
|
+
* can deduplicate), the payload as `data`, the tenant as `partitionkey` and
|
|
461
|
+
* the writing module as `producer`. The actor stays out of the context
|
|
462
|
+
* attributes, which must not carry personal data.
|
|
463
|
+
*/
|
|
464
|
+
function outboxCloudEvent(event, options) {
|
|
465
|
+
return {
|
|
466
|
+
specversion: "1.0",
|
|
467
|
+
id: event.id,
|
|
468
|
+
source: options.source,
|
|
469
|
+
type: `${options.typePrefix ?? "dev.better-supabase"}.${event.type}`,
|
|
470
|
+
...event.subject ? { subject: event.subject } : {},
|
|
471
|
+
time: event.createdAt.toString(),
|
|
472
|
+
datacontenttype: "application/json",
|
|
473
|
+
data: event.payload,
|
|
474
|
+
...event.tenant ? { partitionkey: event.tenant } : {},
|
|
475
|
+
...event.source ? { producer: event.source } : {}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function createOutbox(sql, options) {
|
|
479
|
+
const schema = sqlIdent(options.schema ?? "better_supabase");
|
|
480
|
+
const call = async (fn, args) => {
|
|
481
|
+
const params = args.map((_, index) => `$${String(index + 1)}`).join(", ");
|
|
482
|
+
const [row] = await sql.queryRaw(`select ${schema}.${sqlIdent(fn)}(${params}) as value`, args);
|
|
483
|
+
return row?.value ?? null;
|
|
484
|
+
};
|
|
485
|
+
async function relay(consumer, sink, relayOptions = {}, deadline) {
|
|
486
|
+
const owner = relayOptions.owner ?? workerId();
|
|
487
|
+
const batch = relayOptions.batch ?? 100;
|
|
488
|
+
const lease = relayOptions.lease ?? "1 minute";
|
|
489
|
+
const until = deadline ?? (relayOptions.budgetMs === void 0 ? Number.POSITIVE_INFINITY : Date.now() + relayOptions.budgetMs);
|
|
490
|
+
let delivered = 0;
|
|
491
|
+
while (Date.now() < until) {
|
|
492
|
+
let events;
|
|
493
|
+
try {
|
|
494
|
+
events = toEvents(await call("outbox_claim", [
|
|
495
|
+
consumer,
|
|
496
|
+
owner,
|
|
497
|
+
batch,
|
|
498
|
+
lease
|
|
499
|
+
]));
|
|
500
|
+
} catch (cause) {
|
|
501
|
+
return {
|
|
502
|
+
delivered,
|
|
503
|
+
error: asDbError(cause)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const last = events.at(-1);
|
|
507
|
+
if (last === void 0) {
|
|
508
|
+
await call("outbox_ack", [
|
|
509
|
+
consumer,
|
|
510
|
+
owner,
|
|
511
|
+
null
|
|
512
|
+
]).catch(() => null);
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
await sink.send(events.map((event) => outboxCloudEvent(event, options)));
|
|
517
|
+
} catch (cause) {
|
|
518
|
+
await call("outbox_ack", [
|
|
519
|
+
consumer,
|
|
520
|
+
owner,
|
|
521
|
+
null
|
|
522
|
+
]).catch(() => null);
|
|
523
|
+
return {
|
|
524
|
+
delivered,
|
|
525
|
+
error: toDbError(cause)
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
await call("outbox_ack", [
|
|
530
|
+
consumer,
|
|
531
|
+
owner,
|
|
532
|
+
last.position
|
|
533
|
+
]);
|
|
534
|
+
} catch (cause) {
|
|
535
|
+
return {
|
|
536
|
+
delivered,
|
|
537
|
+
error: asDbError(cause)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
delivered += events.length;
|
|
541
|
+
if (events.length < batch) break;
|
|
542
|
+
}
|
|
543
|
+
return { delivered };
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
emit: (type, payload = {}, emitOptions = {}) => run(async () => String(await call("emit_event", [
|
|
547
|
+
type,
|
|
548
|
+
JSON.stringify(payload),
|
|
549
|
+
emitOptions.subject ?? null,
|
|
550
|
+
emitOptions.tenant ?? null,
|
|
551
|
+
emitOptions.key ?? null,
|
|
552
|
+
emitOptions.source ?? null
|
|
553
|
+
]))),
|
|
554
|
+
register: (consumer, registerOptions = {}) => run(async () => Number(await call("outbox_register", [
|
|
555
|
+
consumer,
|
|
556
|
+
registerOptions.types ?? null,
|
|
557
|
+
registerOptions.fromStart ?? false
|
|
558
|
+
]))),
|
|
559
|
+
unregister: (consumer) => run(async () => await call("outbox_unregister", [consumer]) === true),
|
|
560
|
+
relay: (consumer, sink, relayOptions) => relay(consumer, sink, relayOptions),
|
|
561
|
+
relayRoute(routeOptions) {
|
|
562
|
+
const secret = routeOptions.secret;
|
|
563
|
+
if (!secret) throw new TypeError("relayRoute needs a secret, such as process.env.CRON_SECRET");
|
|
564
|
+
return async (request) => {
|
|
565
|
+
const instance = new URL(request.url).pathname;
|
|
566
|
+
if (request.method !== "GET" && request.method !== "POST") return new Response(null, {
|
|
567
|
+
status: 405,
|
|
568
|
+
headers: { allow: "GET, POST" }
|
|
569
|
+
});
|
|
570
|
+
if (!verifySharedSecret(request, secret)) return problemResponse(dbError("unauthorized", "The relay route needs its bearer secret"), { instance });
|
|
571
|
+
const deadline = Date.now() + (routeOptions.budgetMs ?? 5e4);
|
|
572
|
+
const results = {};
|
|
573
|
+
for (const [consumer, sink] of Object.entries(routeOptions.consumers)) {
|
|
574
|
+
if (Date.now() >= deadline) break;
|
|
575
|
+
const result = await relay(consumer, sink, routeOptions, deadline);
|
|
576
|
+
if (result.error) routeOptions.onError?.(result.error, consumer);
|
|
577
|
+
results[consumer] = result;
|
|
578
|
+
}
|
|
579
|
+
const result = {
|
|
580
|
+
consumers: results,
|
|
581
|
+
budgetExhausted: Date.now() >= deadline
|
|
582
|
+
};
|
|
583
|
+
return Response.json(result);
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
history: (filter = {}) => run(async () => toEvents(await call("outbox_history", [
|
|
587
|
+
filter.subject ?? null,
|
|
588
|
+
filter.type ?? null,
|
|
589
|
+
filter.after ?? 0,
|
|
590
|
+
filter.limit ?? 100
|
|
591
|
+
]))),
|
|
592
|
+
purge: (olderThan, batch) => run(async () => Number(await call("purge_outbox", [olderThan ?? null, batch ?? null])))
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/jobs/queue.ts
|
|
597
|
+
/**
|
|
598
|
+
* The `jobs` SQL kit module over a service SQL connection. The same
|
|
599
|
+
* functions back both `kits.jobs.options.backend` values (pgmq and table)
|
|
600
|
+
* and both schedulers.
|
|
601
|
+
*/
|
|
602
|
+
function sqlQueueBackend(sql) {
|
|
603
|
+
return {
|
|
604
|
+
apiVersion: 1,
|
|
605
|
+
name: "sql",
|
|
606
|
+
leases: true,
|
|
607
|
+
async send(queue, payload, delay, maxAttempts, dedupeKey) {
|
|
608
|
+
const [row] = await sql.queryRaw("select better_supabase.enqueue_job($1, $2, $3, $4, $5) as id", [
|
|
609
|
+
queue,
|
|
610
|
+
JSON.stringify(payload ?? {}),
|
|
611
|
+
delay,
|
|
612
|
+
maxAttempts,
|
|
613
|
+
dedupeKey ?? null
|
|
614
|
+
]);
|
|
615
|
+
return Number(row.id);
|
|
616
|
+
},
|
|
617
|
+
read: (queue, lease, batch) => sql.queryRaw("select * from better_supabase.claim_jobs($1, $2, $3)", [
|
|
618
|
+
queue,
|
|
619
|
+
lease,
|
|
620
|
+
batch
|
|
621
|
+
]),
|
|
622
|
+
async complete(job) {
|
|
623
|
+
const [row] = await sql.queryRaw("select better_supabase.complete_job($1, $2, $3) as done", [
|
|
624
|
+
job.queue,
|
|
625
|
+
job.id,
|
|
626
|
+
job.attempts
|
|
627
|
+
]);
|
|
628
|
+
return row?.done ?? false;
|
|
629
|
+
},
|
|
630
|
+
async fail(job, error, retryIn) {
|
|
631
|
+
const [row] = await sql.queryRaw("select better_supabase.fail_job($1, $2, $3, $4, $5) as status", [
|
|
632
|
+
job.queue,
|
|
633
|
+
job.id,
|
|
634
|
+
job.attempts,
|
|
635
|
+
error,
|
|
636
|
+
retryIn ?? null
|
|
637
|
+
]);
|
|
638
|
+
return row?.status ?? null;
|
|
639
|
+
},
|
|
640
|
+
async extend(job, lease) {
|
|
641
|
+
const [row] = await sql.queryRaw("select better_supabase.extend_job_lease($1, $2, $3, $4) as extended", [
|
|
642
|
+
job.queue,
|
|
643
|
+
job.id,
|
|
644
|
+
job.attempts,
|
|
645
|
+
lease
|
|
646
|
+
]);
|
|
647
|
+
return row?.extended ?? false;
|
|
648
|
+
},
|
|
649
|
+
async replay(queue, id) {
|
|
650
|
+
const [row] = await sql.queryRaw("select better_supabase.replay_dead_job($1, $2) as id", [queue, id]);
|
|
651
|
+
return row?.id === null || row?.id === void 0 ? null : Number(row.id);
|
|
652
|
+
},
|
|
653
|
+
async schedule(name, cron, queue, payload, timeZone, nextRun) {
|
|
654
|
+
await sql.queryRaw("select better_supabase.schedule_job($1, $2, $3, $4, $5, $6)", [
|
|
655
|
+
name,
|
|
656
|
+
cron,
|
|
657
|
+
queue,
|
|
658
|
+
JSON.stringify(payload ?? {}),
|
|
659
|
+
timeZone,
|
|
660
|
+
nextRun.toString()
|
|
661
|
+
]);
|
|
662
|
+
},
|
|
663
|
+
async unschedule(name) {
|
|
664
|
+
const [row] = await sql.queryRaw("select better_supabase.unschedule_job($1) as done", [name]);
|
|
665
|
+
return row?.done ?? false;
|
|
666
|
+
},
|
|
667
|
+
async dueSchedules(lease, batch) {
|
|
668
|
+
return (await sql.queryRaw("select * from better_supabase.claim_due_schedules($1, $2)", [lease, batch])).map((row) => ({
|
|
669
|
+
name: row.job_name,
|
|
670
|
+
cron: row.schedule,
|
|
671
|
+
timeZone: row.timezone,
|
|
672
|
+
queue: row.queue,
|
|
673
|
+
payload: row.payload,
|
|
674
|
+
nextRun: toInstant(row.next_run)
|
|
675
|
+
}));
|
|
676
|
+
},
|
|
677
|
+
async advanceSchedule(name, ran, next) {
|
|
678
|
+
const [row] = await sql.queryRaw("select better_supabase.advance_schedule($1, $2, $3) as advanced", [
|
|
679
|
+
name,
|
|
680
|
+
ran.toString(),
|
|
681
|
+
next.toString()
|
|
682
|
+
]);
|
|
683
|
+
return row?.advanced ?? false;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function sqlOnly(feature) {
|
|
688
|
+
throw new DbException(dbError("invalid_request", `${feature} needs a SQL connection; pgmq_public (PostgREST) does not support it`));
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Supabase's `pgmq_public` RPCs (Integrations, Queues, "Expose Queues via
|
|
692
|
+
* PostgREST"). No lease check on complete, no retry delay, no deduplication
|
|
693
|
+
* and no schedules: a failed job reappears when its lease ends.
|
|
694
|
+
*/
|
|
695
|
+
function pgmqPublicBackend(client) {
|
|
696
|
+
const call = async (fn, args) => {
|
|
697
|
+
const { data, error } = await client.schema("pgmq_public").rpc(fn, args);
|
|
698
|
+
if (error) {
|
|
699
|
+
const message = typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
700
|
+
throw new Error(`pgmq_public.${fn}: ${message}`, { cause: error });
|
|
701
|
+
}
|
|
702
|
+
return data;
|
|
703
|
+
};
|
|
704
|
+
const archive = (job) => call("archive", {
|
|
705
|
+
queue_name: job.queue,
|
|
706
|
+
message_id: job.id
|
|
707
|
+
});
|
|
708
|
+
return {
|
|
709
|
+
apiVersion: 1,
|
|
710
|
+
name: "pgmq_public",
|
|
711
|
+
leases: false,
|
|
712
|
+
async send(queue, payload, delay, maxAttempts, dedupeKey) {
|
|
713
|
+
if (dedupeKey !== void 0) sqlOnly("dedupeKey");
|
|
714
|
+
const ids = await call("send", {
|
|
715
|
+
queue_name: queue,
|
|
716
|
+
message: {
|
|
717
|
+
payload: payload ?? {},
|
|
718
|
+
max_attempts: maxAttempts
|
|
719
|
+
},
|
|
720
|
+
sleep_seconds: delay
|
|
721
|
+
});
|
|
722
|
+
return Number(ids[0]);
|
|
723
|
+
},
|
|
724
|
+
async read(queue, lease, batch) {
|
|
725
|
+
const rows = await call("read", {
|
|
726
|
+
queue_name: queue,
|
|
727
|
+
sleep_seconds: lease,
|
|
728
|
+
n: batch
|
|
729
|
+
});
|
|
730
|
+
const live = [];
|
|
731
|
+
for (const row of rows) if (row.read_ct > (row.message?.max_attempts ?? 5)) await call("archive", {
|
|
732
|
+
queue_name: queue,
|
|
733
|
+
message_id: row.msg_id
|
|
734
|
+
});
|
|
735
|
+
else live.push(row);
|
|
736
|
+
return live.map((row) => ({
|
|
737
|
+
id: row.msg_id,
|
|
738
|
+
attempts: row.read_ct,
|
|
739
|
+
enqueued_at: row.enqueued_at,
|
|
740
|
+
visible_until: row.vt,
|
|
741
|
+
message: row.message
|
|
742
|
+
}));
|
|
743
|
+
},
|
|
744
|
+
complete: archive,
|
|
745
|
+
async fail(job) {
|
|
746
|
+
if (job.attempts < job.maxAttempts) return "queued";
|
|
747
|
+
await archive(job);
|
|
748
|
+
return "dead";
|
|
749
|
+
},
|
|
750
|
+
extend: () => Promise.resolve(false),
|
|
751
|
+
schedule: () => sqlOnly("schedule"),
|
|
752
|
+
unschedule: () => sqlOnly("unschedule")
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
/** Marks a payload that carries a job context; pgmq stores it as the payload. */
|
|
756
|
+
const ENVELOPE = "$bs";
|
|
757
|
+
/** Schedule times are stored to the millisecond, the precision `Date` reads back. */
|
|
758
|
+
const millis = (instant) => instant.round({
|
|
759
|
+
smallestUnit: "millisecond",
|
|
760
|
+
roundingMode: "floor"
|
|
761
|
+
});
|
|
762
|
+
function withContext(payload, context) {
|
|
763
|
+
if (!context) return payload;
|
|
764
|
+
const tenant = tenantOf(context) ?? tenantClaimPaths().map((path) => claimAt(context.claims, path)).find((id) => id !== void 0);
|
|
765
|
+
const recorded = {
|
|
766
|
+
...context.actor ? { actor: context.actor } : {},
|
|
767
|
+
...tenant === void 0 ? {} : { tenant }
|
|
768
|
+
};
|
|
769
|
+
if (!recorded.actor && recorded.tenant === void 0) return payload;
|
|
770
|
+
return {
|
|
771
|
+
[ENVELOPE]: 1,
|
|
772
|
+
context: recorded,
|
|
773
|
+
payload
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function isRecord(value) {
|
|
777
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
778
|
+
}
|
|
779
|
+
function isActor(value) {
|
|
780
|
+
return isRecord(value) && typeof value["id"] === "string" && (value["kind"] === "user" || value["kind"] === "service" || value["kind"] === "anon");
|
|
781
|
+
}
|
|
782
|
+
function unwrap(stored) {
|
|
783
|
+
if (!isRecord(stored) || stored[ENVELOPE] !== 1 || !Object.hasOwn(stored, "payload")) return {
|
|
784
|
+
payload: stored,
|
|
785
|
+
context: {}
|
|
786
|
+
};
|
|
787
|
+
const { actor, tenant } = isRecord(stored["context"]) ? stored["context"] : {};
|
|
788
|
+
return {
|
|
789
|
+
payload: stored["payload"],
|
|
790
|
+
context: {
|
|
791
|
+
...isActor(actor) ? { actor } : {},
|
|
792
|
+
...typeof tenant === "string" && tenant.length > 0 ? { tenant } : {}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function toJob(queue, row) {
|
|
797
|
+
const message = row.message ?? {};
|
|
798
|
+
const { payload, context } = unwrap(message.payload);
|
|
799
|
+
return {
|
|
800
|
+
id: Number(row.id),
|
|
801
|
+
queue,
|
|
802
|
+
payload,
|
|
803
|
+
context,
|
|
804
|
+
attempts: row.attempts,
|
|
805
|
+
maxAttempts: message.max_attempts ?? 5,
|
|
806
|
+
enqueuedAt: toInstant(row.enqueued_at),
|
|
807
|
+
visibleUntil: toInstant(row.visible_until),
|
|
808
|
+
lastError: message.last_error ?? null
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const QUEUE_NAME = /^[a-z_][a-z0-9_]{0,46}$/;
|
|
812
|
+
function isBackend(source) {
|
|
813
|
+
return "apiVersion" in source;
|
|
814
|
+
}
|
|
815
|
+
function isSqlClient(source) {
|
|
816
|
+
return typeof source.queryRaw === "function";
|
|
817
|
+
}
|
|
818
|
+
function backendOf(source) {
|
|
819
|
+
if (isBackend(source)) return source;
|
|
820
|
+
return isSqlClient(source) ? sqlQueueBackend(source) : pgmqPublicBackend(source);
|
|
821
|
+
}
|
|
822
|
+
const later = (a, b) => temporal().Instant.compare(a, b) >= 0 ? a : b;
|
|
823
|
+
/**
|
|
824
|
+
* Typed jobs on Supabase Queues (pgmq), over the `jobs` SQL kit module.
|
|
825
|
+
*
|
|
826
|
+
* Pass a service SQL connection (`postgres.admin`, `ctx.sql`) for the full
|
|
827
|
+
* feature set, a service-role Supabase client to go through the
|
|
828
|
+
* `pgmq_public` RPCs where no direct connection exists (edge functions), or
|
|
829
|
+
* any `QueueBackend`. Queue names are lowercase letters, digits and
|
|
830
|
+
* underscores.
|
|
831
|
+
*/
|
|
832
|
+
function createJobs(source, queues) {
|
|
833
|
+
for (const name of Object.keys(queues)) if (!QUEUE_NAME.test(name)) throw new TypeError(`Queue "${name}": pgmq queue names are lowercase letters, digits and underscores (at most 47)`);
|
|
834
|
+
const transport = backendOf(source);
|
|
835
|
+
const schemaOf = (queue) => {
|
|
836
|
+
const schema = queues[queue];
|
|
837
|
+
if (!schema) throw new TypeError(`Unknown queue "${queue}". Queues: ${Object.keys(queues).join(", ")}`);
|
|
838
|
+
return schema;
|
|
839
|
+
};
|
|
840
|
+
const complete = (job) => run(() => transport.complete(job));
|
|
841
|
+
const fail = (job, error, failOptions = {}) => run(() => transport.fail(job, errorText(error), failOptions.retryIn));
|
|
842
|
+
const claim = (queue, claimOptions = {}) => run(async () => {
|
|
843
|
+
schemaOf(queue);
|
|
844
|
+
return (await transport.read(queue, claimOptions.lease ?? 300, claimOptions.batch ?? 1)).map((row) => toJob(queue, row));
|
|
845
|
+
});
|
|
846
|
+
/**
|
|
847
|
+
* Extends the lease of every job in `held` at half the lease, so jobs
|
|
848
|
+
* waiting their turn in a claimed batch don't expire before they run.
|
|
849
|
+
*/
|
|
850
|
+
const heartbeat = (held, lease) => transport.leases ? setInterval(() => {
|
|
851
|
+
for (const job of held) run(() => transport.extend(job, lease));
|
|
852
|
+
}, Math.max(1e3, lease * 1e3 / 2)) : void 0;
|
|
853
|
+
const handle = async (job, handler, workOptions) => {
|
|
854
|
+
const controller = new AbortController();
|
|
855
|
+
try {
|
|
856
|
+
const payload = await validate(schemaOf(job.queue), job.payload, "payload");
|
|
857
|
+
if (!payload.ok) {
|
|
858
|
+
await fail(job, payload.error.message);
|
|
859
|
+
workOptions.onError?.(payload.error, job);
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
const context = workOptions.allTenants && job.context.tenant === void 0 ? allTenantsContext(job.context) : job.context;
|
|
863
|
+
const outcome = await handler(payload.data, {
|
|
864
|
+
...job,
|
|
865
|
+
payload: payload.data,
|
|
866
|
+
context
|
|
867
|
+
}, controller.signal);
|
|
868
|
+
if (typeof outcome === "object" && outcome !== null && "ok" in outcome && outcome.ok === false && "error" in outcome) throw outcome.error;
|
|
869
|
+
await complete(job);
|
|
870
|
+
return true;
|
|
871
|
+
} catch (cause) {
|
|
872
|
+
controller.abort();
|
|
873
|
+
await fail(job, cause);
|
|
874
|
+
workOptions.onError?.(typeof cause === "object" && cause !== null && "kind" in cause ? cause : toDbError(cause), job);
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
const loop = async (queue, handler, workOptions, forever, deadline) => {
|
|
879
|
+
const concurrency = Math.max(1, workOptions.concurrency ?? 1);
|
|
880
|
+
const lease = workOptions.lease ?? 300;
|
|
881
|
+
const firstWait = workOptions.pollInterval ?? 1e3;
|
|
882
|
+
const maxWait = Math.max(firstWait, workOptions.maxPollInterval ?? 3e4);
|
|
883
|
+
let succeeded = 0;
|
|
884
|
+
let failed = 0;
|
|
885
|
+
const lanes = Array.from({ length: concurrency }, async () => {
|
|
886
|
+
let wait = firstWait;
|
|
887
|
+
while (!workOptions.signal?.aborted) {
|
|
888
|
+
if (deadline !== void 0 && Date.now() >= deadline) return;
|
|
889
|
+
const claimed = await claim(queue, {
|
|
890
|
+
...workOptions,
|
|
891
|
+
batch: workOptions.batch ?? 1
|
|
892
|
+
});
|
|
893
|
+
if (!claimed.ok) throw new TypeError(claimed.error.message, { cause: claimed.error });
|
|
894
|
+
if (claimed.data.length === 0) {
|
|
895
|
+
if (!forever) return;
|
|
896
|
+
await sleep(wait, workOptions.signal);
|
|
897
|
+
wait = Math.min(maxWait, wait * 2);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
wait = firstWait;
|
|
901
|
+
const held = new Set(claimed.data);
|
|
902
|
+
const timer = heartbeat(held, lease);
|
|
903
|
+
try {
|
|
904
|
+
for (const job of claimed.data) {
|
|
905
|
+
if (await handle(job, handler, workOptions)) succeeded += 1;
|
|
906
|
+
else failed += 1;
|
|
907
|
+
held.delete(job);
|
|
908
|
+
}
|
|
909
|
+
} finally {
|
|
910
|
+
clearInterval(timer);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
await Promise.all(lanes);
|
|
915
|
+
return {
|
|
916
|
+
succeeded,
|
|
917
|
+
failed
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
const runSchedules = async (scheduleOptions) => {
|
|
921
|
+
if (!transport.dueSchedules || !transport.advanceSchedule) return 0;
|
|
922
|
+
const due = await transport.dueSchedules(scheduleOptions.lease ?? 60, scheduleOptions.batch ?? 100);
|
|
923
|
+
let ran = 0;
|
|
924
|
+
for (const schedule of due) {
|
|
925
|
+
await transport.send(schedule.queue, schedule.payload, 0, 5, `schedule:${schedule.name}:${schedule.nextRun.toString()}`);
|
|
926
|
+
const next = millis(nextCronRun(schedule.cron, schedule.timeZone, later(nowInstant(), schedule.nextRun)));
|
|
927
|
+
if (await transport.advanceSchedule(schedule.name, schedule.nextRun, next)) ran += 1;
|
|
928
|
+
}
|
|
929
|
+
return ran;
|
|
930
|
+
};
|
|
931
|
+
return {
|
|
932
|
+
enqueue(queue, payload, enqueueOptions = {}) {
|
|
933
|
+
return AsyncResult.from(async () => {
|
|
934
|
+
const valid = await validate(schemaOf(queue), payload, "payload");
|
|
935
|
+
if (!valid.ok) return valid;
|
|
936
|
+
const delay = enqueueOptions.delay ?? (enqueueOptions.runAt ? Math.max(0, Math.ceil((enqueueOptions.runAt.epochMilliseconds - nowInstant().epochMilliseconds) / 1e3)) : 0);
|
|
937
|
+
return run(() => transport.send(queue, withContext(valid.data, enqueueOptions.context), delay, enqueueOptions.maxAttempts ?? 5, enqueueOptions.dedupeKey));
|
|
938
|
+
});
|
|
939
|
+
},
|
|
940
|
+
claim,
|
|
941
|
+
complete,
|
|
942
|
+
fail,
|
|
943
|
+
extend: (job, lease) => run(() => transport.extend(job, lease)),
|
|
944
|
+
replay: (queue, id) => run(() => {
|
|
945
|
+
if (!transport.replay) sqlOnly("replay");
|
|
946
|
+
return transport.replay(queue, id);
|
|
947
|
+
}),
|
|
948
|
+
schedule(name, cron, queue, payload, scheduleOptions = {}) {
|
|
949
|
+
return AsyncResult.from(async () => {
|
|
950
|
+
const valid = await validate(schemaOf(queue), payload, "payload");
|
|
951
|
+
if (!valid.ok) return valid;
|
|
952
|
+
const stored = withContext(valid.data, scheduleOptions.context);
|
|
953
|
+
const timeZone = scheduleOptions.timeZone ?? "UTC";
|
|
954
|
+
return run(() => {
|
|
955
|
+
const nextRun = millis(nextCronRun(cron, timeZone, nowInstant()));
|
|
956
|
+
return transport.schedule(name, cron, queue, stored, timeZone, nextRun);
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
},
|
|
960
|
+
unschedule: (name) => run(() => transport.unschedule(name)),
|
|
961
|
+
runSchedules: (scheduleOptions = {}) => run(() => runSchedules(scheduleOptions)),
|
|
962
|
+
drain: (queue, handler, drainOptions = {}) => loop(queue, handler, drainOptions, false, drainOptions.budgetMs === void 0 ? void 0 : Date.now() + drainOptions.budgetMs),
|
|
963
|
+
work: (queue, handler, workOptions = {}) => loop(queue, handler, workOptions, true),
|
|
964
|
+
drainRoute(routeOptions) {
|
|
965
|
+
const secret = routeOptions.secret;
|
|
966
|
+
if (!secret) throw new TypeError("drainRoute needs a secret, such as process.env.CRON_SECRET");
|
|
967
|
+
for (const queue of Object.keys(routeOptions.handlers)) schemaOf(queue);
|
|
968
|
+
return async (request) => {
|
|
969
|
+
const instance = new URL(request.url).pathname;
|
|
970
|
+
if (request.method !== "GET" && request.method !== "POST") return new Response(null, {
|
|
971
|
+
status: 405,
|
|
972
|
+
headers: { allow: "GET, POST" }
|
|
973
|
+
});
|
|
974
|
+
if (!verifySharedSecret(request, secret)) return problemResponse(dbError("unauthorized", "The drain route needs its bearer secret"), { instance });
|
|
975
|
+
const deadline = Date.now() + (routeOptions.budgetMs ?? 5e4);
|
|
976
|
+
let scheduled = 0;
|
|
977
|
+
if (routeOptions.schedules ?? true) {
|
|
978
|
+
const ran = await run(() => runSchedules({}));
|
|
979
|
+
if (ran.ok) scheduled = ran.data;
|
|
980
|
+
else routeOptions.onError?.(ran.error);
|
|
981
|
+
}
|
|
982
|
+
const results = {};
|
|
983
|
+
for (const [queue, handler] of Object.entries(routeOptions.handlers)) {
|
|
984
|
+
if (handler === void 0) continue;
|
|
985
|
+
if (Date.now() >= deadline) break;
|
|
986
|
+
try {
|
|
987
|
+
results[queue] = await loop(queue, handler, routeOptions, false, deadline);
|
|
988
|
+
} catch (cause) {
|
|
989
|
+
routeOptions.onError?.(toDbError(cause));
|
|
990
|
+
results[queue] = {
|
|
991
|
+
succeeded: 0,
|
|
992
|
+
failed: 0
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const result = {
|
|
997
|
+
schedules: scheduled,
|
|
998
|
+
queues: results,
|
|
999
|
+
budgetExhausted: Date.now() >= deadline
|
|
1000
|
+
};
|
|
1001
|
+
return Response.json(result);
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
483
1006
|
//#endregion
|
|
484
|
-
export { ENTITLEMENTS_UPDATED, createIdempotency, createInbox, createJobs, entitlementMembers };
|
|
1007
|
+
export { ENTITLEMENTS_UPDATED, assertCron, createIdempotency, createInbox, createJobs, createOutbox, entitlementMembers, nextCronRun, outboxCloudEvent, pgmqPublicBackend, purgeAuditLog, sqlQueueBackend };
|