@specific.dev/spectest 0.63.0 → 0.64.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.
|
@@ -698,12 +698,60 @@ async function validJwt(token: string): Promise<boolean> {
|
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
/** One isolate per function, reused across requests (the runtime pools by
|
|
702
|
+
* servicePath). The idle timeout is a day, not upstream's five minutes: a
|
|
703
|
+
* test environment is a snapshot lineage whose forks inherit these
|
|
704
|
+
* isolates, and an isolate reaped for idleness is rebooted by the next
|
|
705
|
+
* test that calls the function — a boot's worth of dirtied memory in that
|
|
706
|
+
* test's own snapshot diff. */
|
|
707
|
+
function workerFor(name: string) {
|
|
708
|
+
return EdgeRuntime.userWorkers.create({
|
|
709
|
+
servicePath: FUNCTIONS_DIR + "/" + name,
|
|
710
|
+
memoryLimitMb: 256,
|
|
711
|
+
workerTimeoutMs: 24 * 60 * 60 * 1000,
|
|
712
|
+
noModuleCache: false,
|
|
713
|
+
importMapPath: null,
|
|
714
|
+
envVars: Object.entries(Deno.env.toObject()),
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Boot every function's isolate now, without calling it. Creating the
|
|
719
|
+
* worker loads the module graph and runs the module's top level — for a
|
|
720
|
+
* function that is its \`Deno.serve(handler)\` registration, no business
|
|
721
|
+
* logic. Called once at environment setup, before the warm snapshot, so
|
|
722
|
+
* the isolates are shared, clean pages of the snapshot rather than a boot
|
|
723
|
+
* in every test that first touches a function. */
|
|
724
|
+
async function warmAll(): Promise<{ warmed: string[]; failed: Record<string, string> }> {
|
|
725
|
+
const names: string[] = [];
|
|
726
|
+
for await (const entry of Deno.readDir(FUNCTIONS_DIR)) {
|
|
727
|
+
if (!entry.isDirectory || entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
728
|
+
try {
|
|
729
|
+
await Deno.stat(FUNCTIONS_DIR + "/" + entry.name + "/index.ts");
|
|
730
|
+
names.push(entry.name);
|
|
731
|
+
} catch {
|
|
732
|
+
// not a function directory
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// All at once: a project can have dozens of functions, and each boot is
|
|
736
|
+
// mostly module loading — the isolates come up concurrently.
|
|
737
|
+
const results = await Promise.allSettled(names.map((name) => workerFor(name)));
|
|
738
|
+
const warmed: string[] = [];
|
|
739
|
+
const failed: Record<string, string> = {};
|
|
740
|
+
results.forEach((r, i) => {
|
|
741
|
+
if (r.status === "fulfilled") warmed.push(names[i]);
|
|
742
|
+
else failed[names[i]] = String(r.reason);
|
|
743
|
+
});
|
|
744
|
+
return { warmed: warmed.sort(), failed };
|
|
745
|
+
}
|
|
746
|
+
|
|
701
747
|
Deno.serve(async (req: Request) => {
|
|
702
748
|
const { pathname } = new URL(req.url);
|
|
703
749
|
|
|
704
750
|
// Ready probe — answered before any auth check, so bring-up never needs a
|
|
705
751
|
// token, and never treated as a function name.
|
|
706
752
|
if (pathname === "/_spectest/health") return new Response("ok");
|
|
753
|
+
// Setup-time warm-up (see warmAll); never treated as a function name.
|
|
754
|
+
if (pathname === "/_spectest/warm") return json(await warmAll());
|
|
707
755
|
|
|
708
756
|
const name = pathname.split("/")[1] ?? "";
|
|
709
757
|
if (name === "") return json({ msg: "missing function name in request" }, 400);
|
|
@@ -718,14 +766,7 @@ Deno.serve(async (req: Request) => {
|
|
|
718
766
|
|
|
719
767
|
const servicePath = FUNCTIONS_DIR + "/" + name;
|
|
720
768
|
try {
|
|
721
|
-
const worker = await
|
|
722
|
-
servicePath,
|
|
723
|
-
memoryLimitMb: 256,
|
|
724
|
-
workerTimeoutMs: 5 * 60 * 1000,
|
|
725
|
-
noModuleCache: false,
|
|
726
|
-
importMapPath: null,
|
|
727
|
-
envVars: Object.entries(Deno.env.toObject()),
|
|
728
|
-
});
|
|
769
|
+
const worker = await workerFor(name);
|
|
729
770
|
return await worker.fetch(req);
|
|
730
771
|
} catch (e) {
|
|
731
772
|
// A missing directory lands here too, so say which function was asked
|
|
@@ -1416,7 +1457,7 @@ export function supabase(opts = {}) {
|
|
|
1416
1457
|
}
|
|
1417
1458
|
// ── functions (edge runtime) ────────────────────────────────────
|
|
1418
1459
|
if (withFunctions) {
|
|
1419
|
-
|
|
1460
|
+
const functionsDef = {
|
|
1420
1461
|
// Built rather than pulled, so the functions' remote imports are
|
|
1421
1462
|
// fetched by the host builder and cached into the image — see
|
|
1422
1463
|
// `functionsDockerfile`.
|
|
@@ -1472,7 +1513,27 @@ export function supabase(opts = {}) {
|
|
|
1472
1513
|
path: "/_spectest/health",
|
|
1473
1514
|
timeoutSecs: 120,
|
|
1474
1515
|
},
|
|
1516
|
+
// Boot every function's isolate before the warm snapshot is
|
|
1517
|
+
// taken (see warmAll in the router). Measured on a real project
|
|
1518
|
+
// (Harmony, 2026-08-28): a test that first touched a few
|
|
1519
|
+
// functions dirtied ~1 GB booting them — memory that then had to
|
|
1520
|
+
// be written into that test's snapshot diff and paid for by every
|
|
1521
|
+
// fork of it. Warm, the isolates are clean shared pages of the
|
|
1522
|
+
// snapshot and a test's diff is what its requests mutate.
|
|
1523
|
+
setup: async () => {
|
|
1524
|
+
const res = await fetch(`http://${g.key("functions")}:9000/_spectest/warm`);
|
|
1525
|
+
if (!res.ok) {
|
|
1526
|
+
console.warn(`supabase(): edge functions warm-up answered ${res.status}; functions boot on first call instead.`);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const { warmed, failed } = (await res.json());
|
|
1530
|
+
console.log(`supabase(): warmed ${warmed.length} edge function${warmed.length === 1 ? "" : "s"}${warmed.length ? ` (${warmed.join(", ")})` : ""}.`);
|
|
1531
|
+
for (const [name, err] of Object.entries(failed)) {
|
|
1532
|
+
console.warn(`supabase(): edge function "${name}" failed to boot at warm-up — it will be retried on first call: ${err}`);
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1475
1535
|
};
|
|
1536
|
+
parts.functions = functionsDef;
|
|
1476
1537
|
}
|
|
1477
1538
|
// ── kong (gateway / primary) — no explicit dependsOn: the group
|
|
1478
1539
|
// expansion makes the primary depend on every member ──────────────
|
package/package.json
CHANGED
|
@@ -1043,12 +1043,60 @@ async function validJwt(token: string): Promise<boolean> {
|
|
|
1043
1043
|
}
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
|
+
/** One isolate per function, reused across requests (the runtime pools by
|
|
1047
|
+
* servicePath). The idle timeout is a day, not upstream's five minutes: a
|
|
1048
|
+
* test environment is a snapshot lineage whose forks inherit these
|
|
1049
|
+
* isolates, and an isolate reaped for idleness is rebooted by the next
|
|
1050
|
+
* test that calls the function — a boot's worth of dirtied memory in that
|
|
1051
|
+
* test's own snapshot diff. */
|
|
1052
|
+
function workerFor(name: string) {
|
|
1053
|
+
return EdgeRuntime.userWorkers.create({
|
|
1054
|
+
servicePath: FUNCTIONS_DIR + "/" + name,
|
|
1055
|
+
memoryLimitMb: 256,
|
|
1056
|
+
workerTimeoutMs: 24 * 60 * 60 * 1000,
|
|
1057
|
+
noModuleCache: false,
|
|
1058
|
+
importMapPath: null,
|
|
1059
|
+
envVars: Object.entries(Deno.env.toObject()),
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** Boot every function's isolate now, without calling it. Creating the
|
|
1064
|
+
* worker loads the module graph and runs the module's top level — for a
|
|
1065
|
+
* function that is its \`Deno.serve(handler)\` registration, no business
|
|
1066
|
+
* logic. Called once at environment setup, before the warm snapshot, so
|
|
1067
|
+
* the isolates are shared, clean pages of the snapshot rather than a boot
|
|
1068
|
+
* in every test that first touches a function. */
|
|
1069
|
+
async function warmAll(): Promise<{ warmed: string[]; failed: Record<string, string> }> {
|
|
1070
|
+
const names: string[] = [];
|
|
1071
|
+
for await (const entry of Deno.readDir(FUNCTIONS_DIR)) {
|
|
1072
|
+
if (!entry.isDirectory || entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
1073
|
+
try {
|
|
1074
|
+
await Deno.stat(FUNCTIONS_DIR + "/" + entry.name + "/index.ts");
|
|
1075
|
+
names.push(entry.name);
|
|
1076
|
+
} catch {
|
|
1077
|
+
// not a function directory
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// All at once: a project can have dozens of functions, and each boot is
|
|
1081
|
+
// mostly module loading — the isolates come up concurrently.
|
|
1082
|
+
const results = await Promise.allSettled(names.map((name) => workerFor(name)));
|
|
1083
|
+
const warmed: string[] = [];
|
|
1084
|
+
const failed: Record<string, string> = {};
|
|
1085
|
+
results.forEach((r, i) => {
|
|
1086
|
+
if (r.status === "fulfilled") warmed.push(names[i]);
|
|
1087
|
+
else failed[names[i]] = String(r.reason);
|
|
1088
|
+
});
|
|
1089
|
+
return { warmed: warmed.sort(), failed };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1046
1092
|
Deno.serve(async (req: Request) => {
|
|
1047
1093
|
const { pathname } = new URL(req.url);
|
|
1048
1094
|
|
|
1049
1095
|
// Ready probe — answered before any auth check, so bring-up never needs a
|
|
1050
1096
|
// token, and never treated as a function name.
|
|
1051
1097
|
if (pathname === "/_spectest/health") return new Response("ok");
|
|
1098
|
+
// Setup-time warm-up (see warmAll); never treated as a function name.
|
|
1099
|
+
if (pathname === "/_spectest/warm") return json(await warmAll());
|
|
1052
1100
|
|
|
1053
1101
|
const name = pathname.split("/")[1] ?? "";
|
|
1054
1102
|
if (name === "") return json({ msg: "missing function name in request" }, 400);
|
|
@@ -1063,14 +1111,7 @@ Deno.serve(async (req: Request) => {
|
|
|
1063
1111
|
|
|
1064
1112
|
const servicePath = FUNCTIONS_DIR + "/" + name;
|
|
1065
1113
|
try {
|
|
1066
|
-
const worker = await
|
|
1067
|
-
servicePath,
|
|
1068
|
-
memoryLimitMb: 256,
|
|
1069
|
-
workerTimeoutMs: 5 * 60 * 1000,
|
|
1070
|
-
noModuleCache: false,
|
|
1071
|
-
importMapPath: null,
|
|
1072
|
-
envVars: Object.entries(Deno.env.toObject()),
|
|
1073
|
-
});
|
|
1114
|
+
const worker = await workerFor(name);
|
|
1074
1115
|
return await worker.fetch(req);
|
|
1075
1116
|
} catch (e) {
|
|
1076
1117
|
// A missing directory lands here too, so say which function was asked
|
|
@@ -1917,7 +1958,7 @@ export function supabase(opts: SupabaseOptions = {}): SupabaseStack {
|
|
|
1917
1958
|
|
|
1918
1959
|
// ── functions (edge runtime) ────────────────────────────────────
|
|
1919
1960
|
if (withFunctions) {
|
|
1920
|
-
|
|
1961
|
+
const functionsDef: ServiceDefinition = {
|
|
1921
1962
|
// Built rather than pulled, so the functions' remote imports are
|
|
1922
1963
|
// fetched by the host builder and cached into the image — see
|
|
1923
1964
|
// `functionsDockerfile`.
|
|
@@ -1976,7 +2017,27 @@ export function supabase(opts: SupabaseOptions = {}): SupabaseStack {
|
|
|
1976
2017
|
path: "/_spectest/health",
|
|
1977
2018
|
timeoutSecs: 120,
|
|
1978
2019
|
},
|
|
2020
|
+
// Boot every function's isolate before the warm snapshot is
|
|
2021
|
+
// taken (see warmAll in the router). Measured on a real project
|
|
2022
|
+
// (Harmony, 2026-08-28): a test that first touched a few
|
|
2023
|
+
// functions dirtied ~1 GB booting them — memory that then had to
|
|
2024
|
+
// be written into that test's snapshot diff and paid for by every
|
|
2025
|
+
// fork of it. Warm, the isolates are clean shared pages of the
|
|
2026
|
+
// snapshot and a test's diff is what its requests mutate.
|
|
2027
|
+
setup: async () => {
|
|
2028
|
+
const res = await fetch(`http://${g.key("functions")}:9000/_spectest/warm`);
|
|
2029
|
+
if (!res.ok) {
|
|
2030
|
+
console.warn(`supabase(): edge functions warm-up answered ${res.status}; functions boot on first call instead.`);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
const { warmed, failed } = (await res.json()) as { warmed: string[]; failed: Record<string, string> };
|
|
2034
|
+
console.log(`supabase(): warmed ${warmed.length} edge function${warmed.length === 1 ? "" : "s"}${warmed.length ? ` (${warmed.join(", ")})` : ""}.`);
|
|
2035
|
+
for (const [name, err] of Object.entries(failed)) {
|
|
2036
|
+
console.warn(`supabase(): edge function "${name}" failed to boot at warm-up — it will be retried on first call: ${err}`);
|
|
2037
|
+
}
|
|
2038
|
+
},
|
|
1979
2039
|
};
|
|
2040
|
+
parts.functions = functionsDef;
|
|
1980
2041
|
}
|
|
1981
2042
|
|
|
1982
2043
|
// ── kong (gateway / primary) — no explicit dependsOn: the group
|