@palbase/backend 35.0.0 → 36.0.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/dist/bin/palbase-backend.cjs +556 -487
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +6 -6
- package/dist/{chunk-L4R7WXLH.js → chunk-7WAGQ3VR.js} +4 -171
- package/dist/chunk-7WAGQ3VR.js.map +1 -0
- package/dist/{chunk-UMESJJRS.js → chunk-CS6NQ6PO.js} +2 -1
- package/dist/chunk-CS6NQ6PO.js.map +1 -0
- package/dist/{chunk-BOD3DT6D.js → chunk-EHKEMHB4.js} +560 -430
- package/dist/chunk-EHKEMHB4.js.map +1 -0
- package/dist/{chunk-PRH45NZU.js → chunk-FFO36MQQ.js} +2 -45
- package/dist/chunk-FFO36MQQ.js.map +1 -0
- package/dist/{chunk-P7MGSAFT.js → chunk-LFUNRWUE.js} +1 -40
- package/dist/chunk-LFUNRWUE.js.map +1 -0
- package/dist/{chunk-RZLY4BOY.js → chunk-PDD55QWN.js} +2 -2
- package/dist/{chunk-YO5GYM73.js → chunk-X7UR3VXA.js} +5 -6
- package/dist/chunk-X7UR3VXA.js.map +1 -0
- package/dist/{chunk-6FEHCEB7.js → chunk-YANGWAIM.js} +3 -3
- package/dist/db/index.cjs +1 -232
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +3 -46
- package/dist/db/index.d.ts +3 -46
- package/dist/db/index.js +4 -12
- package/dist/engine/index.cjs +556 -487
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +4 -4
- package/dist/engine/index.d.ts +4 -4
- package/dist/engine/index.js +6 -6
- package/dist/{index-DllNjpTl.d.cts → index-CI7S0Wqv.d.cts} +61 -8
- package/dist/{endpoint-BI0yGuEL.d.ts → index-CUy50OLU.d.ts} +6 -82
- package/dist/{endpoint-DaGnjt42.d.cts → index-DRx880KY.d.cts} +6 -82
- package/dist/{index-E8Sd6NFy.d.ts → index-Ogi30dbt.d.ts} +61 -8
- package/dist/index.cjs +5 -237
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -7
- package/dist/index.d.ts +6 -7
- package/dist/index.js +7 -15
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +1 -0
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +3 -3
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +2 -2
- package/dist/{registry-BnKvIyve.d.cts → registry-C7UCkQf0.d.cts} +1 -1
- package/dist/{registry-zDCj0eOU.d.ts → registry-DJcvbomD.d.ts} +1 -1
- package/dist/test/index.cjs +3 -11
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +2 -2
- package/dist/test/index.d.ts +2 -2
- package/dist/test/index.js +2 -9
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/database.md +84 -67
- package/docs/examples/database-dx.ts +12 -17
- package/docs/llms-full.txt +82 -65
- package/package.json +1 -1
- package/stager/inferred_returns.js +153 -0
- package/stager/return_types.js +9 -4
- package/stager/stage.js +3 -1
- package/template/AGENTS.md +5 -5
- package/template/package.json +1 -1
- package/dist/chunk-BOD3DT6D.js.map +0 -1
- package/dist/chunk-L4R7WXLH.js.map +0 -1
- package/dist/chunk-P7MGSAFT.js.map +0 -1
- package/dist/chunk-PRH45NZU.js.map +0 -1
- package/dist/chunk-UMESJJRS.js.map +0 -1
- package/dist/chunk-YO5GYM73.js.map +0 -1
- package/docs/examples/check-query-contracts.ts +0 -5
- package/docs/examples/database-dx.contract.json +0 -65
- /package/dist/{chunk-RZLY4BOY.js.map → chunk-PDD55QWN.js.map} +0 -0
- /package/dist/{chunk-6FEHCEB7.js.map → chunk-YANGWAIM.js.map} +0 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
buildContainer,
|
|
6
6
|
buildRelations,
|
|
7
7
|
uniqueKeysOf
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-YANGWAIM.js";
|
|
9
9
|
import {
|
|
10
10
|
validateInsertManyOptions
|
|
11
11
|
} from "./chunk-GHNC2UHS.js";
|
|
@@ -13,11 +13,10 @@ import {
|
|
|
13
13
|
__requestALS,
|
|
14
14
|
__runStartHooks,
|
|
15
15
|
__runWithRuntime
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-X7UR3VXA.js";
|
|
17
17
|
import {
|
|
18
|
-
CODECS
|
|
19
|
-
|
|
20
|
-
} from "./chunk-PRH45NZU.js";
|
|
18
|
+
CODECS
|
|
19
|
+
} from "./chunk-FFO36MQQ.js";
|
|
21
20
|
import {
|
|
22
21
|
assertNoExpressionHandles,
|
|
23
22
|
assertUsableFilter,
|
|
@@ -32,9 +31,8 @@ import {
|
|
|
32
31
|
looksLikeUnbrandedRef,
|
|
33
32
|
qualifiedTableKey,
|
|
34
33
|
retryBudget,
|
|
35
|
-
validateAtomicOptions
|
|
36
|
-
|
|
37
|
-
} from "./chunk-P7MGSAFT.js";
|
|
34
|
+
validateAtomicOptions
|
|
35
|
+
} from "./chunk-LFUNRWUE.js";
|
|
38
36
|
import {
|
|
39
37
|
getRoutes,
|
|
40
38
|
resolveEffectiveAuth
|
|
@@ -51,7 +49,7 @@ import {
|
|
|
51
49
|
isHttpError,
|
|
52
50
|
isRetryable,
|
|
53
51
|
markEngineRaised
|
|
54
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-CS6NQ6PO.js";
|
|
55
53
|
import {
|
|
56
54
|
__name
|
|
57
55
|
} from "./chunk-VXPNPVAG.js";
|
|
@@ -93,6 +91,16 @@ ${detail}`);
|
|
|
93
91
|
if (!Number.isInteger(poolMax) || poolMax < 1) {
|
|
94
92
|
throw new BootRefused([], `boot refused: DB_POOL_MAX must be a positive integer (got ${env.DB_POOL_MAX}).`);
|
|
95
93
|
}
|
|
94
|
+
const capacityNumber = /* @__PURE__ */ __name((key2, minimum, maximum) => {
|
|
95
|
+
if (env[key2] === void 0) return void 0;
|
|
96
|
+
const value = Number(env[key2]);
|
|
97
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new BootRefused([], `boot refused: ${key2} must be between ${minimum} and ${maximum}.`);
|
|
98
|
+
return value;
|
|
99
|
+
}, "capacityNumber");
|
|
100
|
+
const queueLimit = capacityNumber("PALBASE_DB_QUEUE_LIMIT", 0, 1e4);
|
|
101
|
+
const queueTimeoutMs = capacityNumber("PALBASE_DB_QUEUE_TIMEOUT_MS", 1, 6e4);
|
|
102
|
+
const tenantClaim = env.PALBASE_DB_TENANT_CLAIM;
|
|
103
|
+
if (tenantClaim !== void 0 && !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(tenantClaim)) throw new BootRefused([], "boot refused: PALBASE_DB_TENANT_CLAIM must be a top-level trusted claim name.");
|
|
96
104
|
return {
|
|
97
105
|
databaseUrl: env.DATABASE_URL.trim(),
|
|
98
106
|
authJwksUrl: env.AUTH_JWKS_URL.trim(),
|
|
@@ -117,6 +125,17 @@ ${detail}`);
|
|
|
117
125
|
// and the live database agrees (pg_roles.rolbypassrls = true).
|
|
118
126
|
dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
|
|
119
127
|
poolMax,
|
|
128
|
+
databaseAdmission: {
|
|
129
|
+
...queueLimit === void 0 ? {} : {
|
|
130
|
+
queueLimit
|
|
131
|
+
},
|
|
132
|
+
...queueTimeoutMs === void 0 ? {} : {
|
|
133
|
+
queueTimeoutMs
|
|
134
|
+
},
|
|
135
|
+
...tenantClaim === void 0 ? {} : {
|
|
136
|
+
tenantClaim
|
|
137
|
+
}
|
|
138
|
+
},
|
|
120
139
|
...env.PALBASE_DB_DIAGNOSTICS === "true" ? {
|
|
121
140
|
databaseDiagnostics: {
|
|
122
141
|
onComplete: /* @__PURE__ */ __name((diagnostics) => {
|
|
@@ -737,142 +756,167 @@ function parameterChunks(sql) {
|
|
|
737
756
|
__name(parameterChunks, "parameterChunks");
|
|
738
757
|
|
|
739
758
|
// src/engine/admission.ts
|
|
740
|
-
var
|
|
759
|
+
var DatabaseAdmission = class {
|
|
741
760
|
static {
|
|
742
|
-
__name(this, "
|
|
761
|
+
__name(this, "DatabaseAdmission");
|
|
743
762
|
}
|
|
744
|
-
|
|
745
|
-
pending = [];
|
|
763
|
+
options;
|
|
746
764
|
active = 0;
|
|
747
|
-
|
|
765
|
+
queued = 0;
|
|
748
766
|
tenants = /* @__PURE__ */ new Map();
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
767
|
+
// Map insertion order is the round-robin ring; each Set is FIFO. Grant and
|
|
768
|
+
// cancellation remove nodes directly, without scanning a 10,000-item queue.
|
|
769
|
+
pending = /* @__PURE__ */ new Map();
|
|
770
|
+
lastTenant;
|
|
771
|
+
accepted = 0;
|
|
772
|
+
rejected = {
|
|
773
|
+
full: 0,
|
|
774
|
+
timeout: 0,
|
|
775
|
+
cancelled: 0
|
|
776
|
+
};
|
|
777
|
+
limit;
|
|
778
|
+
timeout;
|
|
779
|
+
constructor(options) {
|
|
780
|
+
this.options = options;
|
|
781
|
+
this.limit = options.queueLimit ?? Math.min(512, Math.max(32, options.concurrency * 8));
|
|
782
|
+
this.timeout = options.queueTimeoutMs ?? 1e3;
|
|
783
|
+
if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1 || !Number.isSafeInteger(this.limit) || this.limit < 0 || this.limit > 1e4 || !Number.isSafeInteger(this.timeout) || this.timeout < 1 || this.timeout > 6e4) {
|
|
784
|
+
throw new Error("Invalid database pool or queue deployment settings");
|
|
785
|
+
}
|
|
753
786
|
}
|
|
754
787
|
get snapshot() {
|
|
755
788
|
return {
|
|
756
789
|
active: this.active,
|
|
757
|
-
queued: this.
|
|
790
|
+
queued: this.queued,
|
|
758
791
|
tenants: this.tenants.size,
|
|
759
|
-
|
|
792
|
+
accepted: this.accepted,
|
|
793
|
+
rejected: {
|
|
794
|
+
...this.rejected
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
rejection(reason) {
|
|
799
|
+
this.rejected[reason]++;
|
|
800
|
+
try {
|
|
801
|
+
this.options.onReject?.({
|
|
802
|
+
reason,
|
|
803
|
+
active: this.active,
|
|
804
|
+
queued: this.queued
|
|
805
|
+
});
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
take(tenant, entered) {
|
|
810
|
+
this.active++;
|
|
811
|
+
this.accepted++;
|
|
812
|
+
this.lastTenant = tenant;
|
|
813
|
+
this.tenants.set(tenant, (this.tenants.get(tenant) ?? 0) + 1);
|
|
814
|
+
let released = false;
|
|
815
|
+
return {
|
|
816
|
+
queueMs: performance.now() - entered,
|
|
817
|
+
release: /* @__PURE__ */ __name(() => {
|
|
818
|
+
if (released) return;
|
|
819
|
+
released = true;
|
|
820
|
+
this.active--;
|
|
821
|
+
const count = this.tenants.get(tenant) - 1;
|
|
822
|
+
if (count) this.tenants.set(tenant, count);
|
|
823
|
+
else this.tenants.delete(tenant);
|
|
824
|
+
this.drain();
|
|
825
|
+
}, "release")
|
|
760
826
|
};
|
|
761
827
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
828
|
+
drain() {
|
|
829
|
+
while (this.active < this.options.concurrency && this.queued) {
|
|
830
|
+
let tenant = this.pending.keys().next().value;
|
|
831
|
+
if (this.pending.size > 1 && tenant === this.lastTenant) {
|
|
832
|
+
const queue2 = this.pending.get(tenant);
|
|
833
|
+
this.pending.delete(tenant);
|
|
834
|
+
this.pending.set(tenant, queue2);
|
|
835
|
+
tenant = this.pending.keys().next().value;
|
|
836
|
+
}
|
|
837
|
+
const queue = this.pending.get(tenant), waiter = queue.values().next().value;
|
|
838
|
+
queue.delete(waiter);
|
|
839
|
+
this.pending.delete(tenant);
|
|
840
|
+
if (queue.size) this.pending.set(tenant, queue);
|
|
841
|
+
this.queued--;
|
|
842
|
+
waiter.cleanup();
|
|
843
|
+
waiter.grant(this.take(tenant, waiter.entered));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async acquire(claimsJson, signal) {
|
|
847
|
+
if (signal?.aborted) {
|
|
848
|
+
this.rejection("cancelled");
|
|
849
|
+
throw signal.reason ?? new Error("Database admission cancelled");
|
|
850
|
+
}
|
|
851
|
+
const claims = JSON.parse(claimsJson);
|
|
852
|
+
const value = this.options.tenantClaim ? claims[this.options.tenantClaim] : claims.sub;
|
|
853
|
+
const tenant = typeof value === "string" && value.length ? value : "__anonymous__";
|
|
854
|
+
const entered = performance.now();
|
|
855
|
+
if (this.active < this.options.concurrency && !this.queued) return this.take(tenant, entered);
|
|
856
|
+
if (this.queued >= this.limit) {
|
|
857
|
+
this.rejection("full");
|
|
858
|
+
throw new TooManyRequests({
|
|
774
859
|
retryAfter: 1
|
|
775
|
-
},
|
|
860
|
+
}, "Database queue is full");
|
|
776
861
|
}
|
|
777
862
|
return new Promise((grant, reject) => {
|
|
863
|
+
const queue = this.pending.get(tenant) ?? /* @__PURE__ */ new Set();
|
|
864
|
+
if (!this.pending.has(tenant)) this.pending.set(tenant, queue);
|
|
778
865
|
const waiter = {
|
|
779
|
-
tenant,
|
|
780
|
-
keys: scoped,
|
|
781
|
-
entered: performance.now(),
|
|
782
866
|
grant,
|
|
783
867
|
reject,
|
|
784
|
-
|
|
785
|
-
|
|
868
|
+
entered,
|
|
869
|
+
cleanup() {
|
|
870
|
+
}
|
|
786
871
|
};
|
|
787
|
-
const remove = /* @__PURE__ */ __name((reason) => {
|
|
788
|
-
|
|
789
|
-
if (
|
|
790
|
-
this.
|
|
872
|
+
const remove = /* @__PURE__ */ __name((reason, error) => {
|
|
873
|
+
if (!queue.delete(waiter)) return;
|
|
874
|
+
if (!queue.size) this.pending.delete(tenant);
|
|
875
|
+
this.queued--;
|
|
791
876
|
waiter.cleanup();
|
|
792
|
-
|
|
877
|
+
this.rejection(reason);
|
|
878
|
+
reject(error);
|
|
793
879
|
this.drain();
|
|
794
880
|
}, "remove");
|
|
795
|
-
const abort = /* @__PURE__ */ __name(() => remove(signal?.reason ?? new Error("Database admission cancelled")), "abort");
|
|
796
|
-
const timer = setTimeout(() => remove(new TooManyRequests({
|
|
881
|
+
const abort = /* @__PURE__ */ __name(() => remove("cancelled", signal?.reason ?? new Error("Database admission cancelled")), "abort");
|
|
882
|
+
const timer = setTimeout(() => remove("timeout", new TooManyRequests({
|
|
797
883
|
retryAfter: 1
|
|
798
|
-
},
|
|
884
|
+
}, "Database queue deadline exceeded")), this.timeout);
|
|
799
885
|
waiter.cleanup = () => {
|
|
800
886
|
clearTimeout(timer);
|
|
801
887
|
signal?.removeEventListener("abort", abort);
|
|
802
888
|
};
|
|
889
|
+
queue.add(waiter);
|
|
890
|
+
this.queued++;
|
|
803
891
|
signal?.addEventListener("abort", abort, {
|
|
804
892
|
once: true
|
|
805
893
|
});
|
|
806
|
-
this.pending.push(waiter);
|
|
807
894
|
if (signal?.aborted) abort();
|
|
808
895
|
else this.drain();
|
|
809
896
|
});
|
|
810
897
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
let released = false;
|
|
829
|
-
waiter.grant({
|
|
830
|
-
queueMs: performance.now() - waiter.entered,
|
|
831
|
-
release: /* @__PURE__ */ __name(() => {
|
|
832
|
-
if (released) return;
|
|
833
|
-
released = true;
|
|
834
|
-
this.active--;
|
|
835
|
-
const left = this.tenants.get(waiter.tenant) - 1;
|
|
836
|
-
if (left === 0) this.tenants.delete(waiter.tenant);
|
|
837
|
-
else this.tenants.set(waiter.tenant, left);
|
|
838
|
-
for (const key2 of waiter.keys) this.activeKeys.delete(key2);
|
|
839
|
-
this.drain();
|
|
840
|
-
}, "release")
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
} finally {
|
|
844
|
-
this.draining = false;
|
|
898
|
+
};
|
|
899
|
+
var databases = /* @__PURE__ */ new WeakMap();
|
|
900
|
+
function databaseAdmission(owner, options) {
|
|
901
|
+
let admission = databases.get(owner);
|
|
902
|
+
if (!admission) {
|
|
903
|
+
admission = new DatabaseAdmission(options ?? {
|
|
904
|
+
concurrency: 10
|
|
905
|
+
});
|
|
906
|
+
databases.set(owner, admission);
|
|
907
|
+
} else if (options) {
|
|
908
|
+
for (const key2 of [
|
|
909
|
+
"concurrency",
|
|
910
|
+
"queueLimit",
|
|
911
|
+
"queueTimeoutMs",
|
|
912
|
+
"tenantClaim"
|
|
913
|
+
]) {
|
|
914
|
+
if (options[key2] !== admission.options[key2]) throw new Error("Database admission cannot be reconfigured while its pool is shared");
|
|
845
915
|
}
|
|
846
916
|
}
|
|
847
|
-
};
|
|
848
|
-
var registries = /* @__PURE__ */ new WeakMap();
|
|
849
|
-
function workloadAdmission(owner, definition) {
|
|
850
|
-
validateWorkload(definition);
|
|
851
|
-
let registry = registries.get(owner);
|
|
852
|
-
if (!registry) {
|
|
853
|
-
registry = /* @__PURE__ */ new Map();
|
|
854
|
-
registries.set(owner, registry);
|
|
855
|
-
}
|
|
856
|
-
const existing = registry.get(definition.name);
|
|
857
|
-
if (existing) {
|
|
858
|
-
const d = existing.definition;
|
|
859
|
-
if (Object.keys(d).some((k) => d[k] !== definition[k])) throw new Error(`Workload ${definition.name} was already registered with different limits`);
|
|
860
|
-
return existing;
|
|
861
|
-
}
|
|
862
|
-
if (registry.size >= 64) throw new Error("At most 64 named database workloads may be registered per engine; declare and reuse them at module scope");
|
|
863
|
-
const admission = new WorkloadAdmission(Object.freeze({
|
|
864
|
-
...definition
|
|
865
|
-
}));
|
|
866
|
-
registry.set(definition.name, admission);
|
|
867
917
|
return admission;
|
|
868
918
|
}
|
|
869
|
-
__name(
|
|
870
|
-
function workloadTenant(definition, claimsJson) {
|
|
871
|
-
const claims = JSON.parse(claimsJson);
|
|
872
|
-
const value = claims[definition.tenantClaim] ?? claims.sub;
|
|
873
|
-
return typeof value === "string" && value.length > 0 ? value : "__anonymous__";
|
|
874
|
-
}
|
|
875
|
-
__name(workloadTenant, "workloadTenant");
|
|
919
|
+
__name(databaseAdmission, "databaseAdmission");
|
|
876
920
|
|
|
877
921
|
// src/engine/transaction-control.ts
|
|
878
922
|
var TransactionControl = class {
|
|
@@ -995,6 +1039,7 @@ var DatabaseTrace = class {
|
|
|
995
1039
|
sqlMs: 0,
|
|
996
1040
|
commitMs: 0,
|
|
997
1041
|
rollbackMs: 0,
|
|
1042
|
+
connectionMs: 0,
|
|
998
1043
|
commits: 0,
|
|
999
1044
|
commitFailures: 0,
|
|
1000
1045
|
rollbackRequests: 0,
|
|
@@ -1006,7 +1051,7 @@ var DatabaseTrace = class {
|
|
|
1006
1051
|
if (this.failure) throw this.failure;
|
|
1007
1052
|
}
|
|
1008
1053
|
time(key2, started) {
|
|
1009
|
-
this.state[key2]
|
|
1054
|
+
this.state[key2] = (this.state[key2] ?? 0) + performance.now() - started;
|
|
1010
1055
|
}
|
|
1011
1056
|
retry() {
|
|
1012
1057
|
this.state.retries++;
|
|
@@ -1212,31 +1257,53 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
1212
1257
|
fail = rej;
|
|
1213
1258
|
});
|
|
1214
1259
|
void parked.catch(() => void 0);
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
validateAtomicOptions({
|
|
1223
|
-
isolation,
|
|
1224
|
-
readOnly: options.readOnly
|
|
1225
|
-
});
|
|
1226
|
-
await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
|
|
1260
|
+
settled = (async () => {
|
|
1261
|
+
const queuedAt = performance.now();
|
|
1262
|
+
let lease;
|
|
1263
|
+
try {
|
|
1264
|
+
lease = await (sql.admission ?? databaseAdmission(sql)).acquire(claimsJson, options.control?.signal);
|
|
1265
|
+
} finally {
|
|
1266
|
+
options.trace?.time("queueMs", queuedAt);
|
|
1227
1267
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
options.control
|
|
1232
|
-
|
|
1268
|
+
const openingAt = performance.now();
|
|
1269
|
+
let connectedAt;
|
|
1270
|
+
try {
|
|
1271
|
+
options.control?.check();
|
|
1272
|
+
if (closed) throw new Error("Database transaction closed while waiting for admission");
|
|
1273
|
+
return await sql.begin(async (tx) => {
|
|
1274
|
+
connectedAt = performance.now();
|
|
1275
|
+
options.trace?.time("poolMs", openingAt);
|
|
1276
|
+
const setupAt = options.trace ? performance.now() : 0;
|
|
1277
|
+
options.control?.check();
|
|
1278
|
+
if (closed) throw new Error("Database transaction closed while waiting for its connection");
|
|
1279
|
+
if (options.isolation !== void 0 || options.readOnly !== void 0) {
|
|
1280
|
+
const isolation = options.isolation ?? "read committed";
|
|
1281
|
+
validateAtomicOptions({
|
|
1282
|
+
isolation,
|
|
1283
|
+
readOnly: options.readOnly
|
|
1284
|
+
});
|
|
1285
|
+
await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
|
|
1286
|
+
}
|
|
1287
|
+
if (options.control && sql.cancelBackend) {
|
|
1288
|
+
const ticket = await tx.unsafe(`WITH backend AS MATERIALIZED (
|
|
1289
|
+
SELECT pid, backend_start::text AS started FROM pg_catalog.pg_stat_get_activity(pg_catalog.pg_backend_pid())
|
|
1290
|
+
) ${bindSql}, backend.pid, backend.started FROM backend`, bindParams);
|
|
1291
|
+
if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
|
|
1292
|
+
options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
|
|
1293
|
+
options.control.check();
|
|
1294
|
+
} else {
|
|
1295
|
+
const binding = tx.unsafe(bindSql, bindParams);
|
|
1296
|
+
await (options.control ? options.control.watch(binding) : binding);
|
|
1297
|
+
}
|
|
1298
|
+
options.trace?.time("setupMs", setupAt);
|
|
1299
|
+
resolveTx2(scoped(tx));
|
|
1300
|
+
await parked;
|
|
1301
|
+
});
|
|
1302
|
+
} finally {
|
|
1303
|
+
if (connectedAt !== void 0) options.trace?.time("connectionMs", connectedAt);
|
|
1304
|
+
lease.release();
|
|
1233
1305
|
}
|
|
1234
|
-
|
|
1235
|
-
await (options.control ? options.control.watch(binding) : binding);
|
|
1236
|
-
options.trace?.time("setupMs", setupAt);
|
|
1237
|
-
resolveTx2(scoped(tx));
|
|
1238
|
-
await parked;
|
|
1239
|
-
}).catch((e) => {
|
|
1306
|
+
})().catch((e) => {
|
|
1240
1307
|
rejectTx(e);
|
|
1241
1308
|
throw e;
|
|
1242
1309
|
});
|
|
@@ -1253,10 +1320,13 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
1253
1320
|
if (pendingSavepoints.size) await Promise.allSettled([
|
|
1254
1321
|
...pendingSavepoints
|
|
1255
1322
|
]);
|
|
1256
|
-
options.control?.check();
|
|
1257
1323
|
options.trace?.check();
|
|
1324
|
+
if (!opening) {
|
|
1325
|
+
closed = true;
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
options.control?.check();
|
|
1258
1329
|
closed = true;
|
|
1259
|
-
if (!opening) return;
|
|
1260
1330
|
const started = options.trace ? performance.now() : 0;
|
|
1261
1331
|
release();
|
|
1262
1332
|
try {
|
|
@@ -1383,10 +1453,11 @@ function asTableRows(table, rows, withSpec, partial = false) {
|
|
|
1383
1453
|
const nested = typeof spec === "object" ? spec.with : void 0;
|
|
1384
1454
|
for (const row of result) {
|
|
1385
1455
|
const value = row[rel.name];
|
|
1386
|
-
|
|
1456
|
+
const selected = typeof spec === "object" && "select" in spec && spec.select !== void 0;
|
|
1457
|
+
if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested, selected);
|
|
1387
1458
|
else if (value && typeof value === "object") row[rel.name] = asTableRows(rel.to, [
|
|
1388
1459
|
value
|
|
1389
|
-
], nested)[0];
|
|
1460
|
+
], nested, selected)[0];
|
|
1390
1461
|
}
|
|
1391
1462
|
}
|
|
1392
1463
|
for (const row of result) foldCan(table, row, partial);
|
|
@@ -2330,6 +2401,9 @@ function withClauses(table, spec, add, caller, parentAlias = "t", depth = 0, cou
|
|
|
2330
2401
|
const i = ++counter.value;
|
|
2331
2402
|
const childAlias = `w${i}`;
|
|
2332
2403
|
const childKnown = schemaColumns(rel.to);
|
|
2404
|
+
if (o["select"] !== void 0 && (!Array.isArray(o["select"]) || o["select"].length === 0)) {
|
|
2405
|
+
throw new Error(`${caller}(${table}): with.${name}.select must be a non-empty array of columns`);
|
|
2406
|
+
}
|
|
2333
2407
|
const childCols = Array.isArray(o["select"]) && o["select"].length > 0 ? o["select"] : childKnown !== null ? [
|
|
2334
2408
|
...childKnown
|
|
2335
2409
|
] : [];
|
|
@@ -2433,7 +2507,10 @@ function pageSchema(table) {
|
|
|
2433
2507
|
}
|
|
2434
2508
|
__name(pageSchema, "pageSchema");
|
|
2435
2509
|
function createOps(tx, context = {}) {
|
|
2436
|
-
const at = /* @__PURE__ */ __name(() =>
|
|
2510
|
+
const at = /* @__PURE__ */ __name(() => {
|
|
2511
|
+
const target = typeof tx === "function" ? tx() : tx;
|
|
2512
|
+
return target instanceof Promise ? target.then(resolveTx) : resolveTx(target);
|
|
2513
|
+
}, "at");
|
|
2437
2514
|
const ops = {
|
|
2438
2515
|
diagnostics() {
|
|
2439
2516
|
return context.trace?.snapshot ?? null;
|
|
@@ -2884,22 +2961,6 @@ function createOps(tx, context = {}) {
|
|
|
2884
2961
|
]);
|
|
2885
2962
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
2886
2963
|
},
|
|
2887
|
-
compileQuery(query) {
|
|
2888
|
-
assertReadContract(query, tableDefOf(query.table)?.columns);
|
|
2889
|
-
return compileFindMany(query.table, query.where, query.options);
|
|
2890
|
-
},
|
|
2891
|
-
async readQuery(query) {
|
|
2892
|
-
const compiled = ops.compileQuery(query);
|
|
2893
|
-
return asTableRows(query.table, await (await at()).unsafe(compiled.sql, compiled.params, {
|
|
2894
|
-
name: query.name
|
|
2895
|
-
}), void 0, true);
|
|
2896
|
-
},
|
|
2897
|
-
async explainQuery(query) {
|
|
2898
|
-
const compiled = ops.compileQuery(query);
|
|
2899
|
-
return await (await at()).unsafe(`EXPLAIN (FORMAT JSON) ${compiled.sql}`, compiled.params, {
|
|
2900
|
-
name: `${query.name}.explain`
|
|
2901
|
-
});
|
|
2902
|
-
},
|
|
2903
2964
|
async findMany(table, query = {}, opts = {}) {
|
|
2904
2965
|
const compiled = compileFindMany(table, query, opts);
|
|
2905
2966
|
return asTableRows(table, await (await at()).unsafe(compiled.sql, compiled.params), opts.with, opts.select !== void 0);
|
|
@@ -3434,7 +3495,7 @@ function createOps(tx, context = {}) {
|
|
|
3434
3495
|
if (options.mode === "compiled") throw new Error(`Command cannot compile: ${compiled.reason}. Use auto/sequential mode or installCommandExecutor at boot.`);
|
|
3435
3496
|
return ops.txPlan(plan);
|
|
3436
3497
|
}
|
|
3437
|
-
if (compiled.steps.length === 0) return {
|
|
3498
|
+
if (compiled.steps.length === 0 && !compiled.pipeline?.length) return {
|
|
3438
3499
|
results: []
|
|
3439
3500
|
};
|
|
3440
3501
|
if (compiled.pipeline) {
|
|
@@ -3707,6 +3768,7 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
3707
3768
|
const state = sqlstateOf(e);
|
|
3708
3769
|
if (state === SQLSTATE.serializationFailure) return markEngineRaised(new SerializationFailure(state));
|
|
3709
3770
|
if (state === SQLSTATE.deadlockDetected) return markEngineRaised(new DeadlockDetected(state));
|
|
3771
|
+
if (state === SQLSTATE.queryCancelled) return markEngineRaised(new HttpError(503, "database_query_cancelled", "Database statement was cancelled"));
|
|
3710
3772
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
3711
3773
|
return new Error(`Database.$asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`);
|
|
3712
3774
|
}
|
|
@@ -3733,6 +3795,7 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
3733
3795
|
}
|
|
3734
3796
|
}), "wrapTx");
|
|
3735
3797
|
return {
|
|
3798
|
+
admission: sql.admission ?? databaseAdmission(sql),
|
|
3736
3799
|
unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
|
|
3737
3800
|
cancelBackend: sql.cancelBackend?.bind(sql),
|
|
3738
3801
|
begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))).catch((e) => {
|
|
@@ -3741,9 +3804,12 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
3741
3804
|
};
|
|
3742
3805
|
}
|
|
3743
3806
|
__name(diagnosingDriver, "diagnosingDriver");
|
|
3744
|
-
function createRequestDatabase(sql, identity, diagnostics) {
|
|
3807
|
+
function createRequestDatabase(sql, identity, diagnostics, signal) {
|
|
3745
3808
|
const trace = diagnostics === void 0 ? void 0 : new DatabaseTrace(diagnostics);
|
|
3809
|
+
const requestControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
|
|
3746
3810
|
let requestClosed = false;
|
|
3811
|
+
let authorized = false, applicationTouched = false;
|
|
3812
|
+
let authorizationRelease;
|
|
3747
3813
|
const contextFor = /* @__PURE__ */ __name((role) => {
|
|
3748
3814
|
let scope;
|
|
3749
3815
|
return {
|
|
@@ -3762,19 +3828,21 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
3762
3828
|
validateAtomicOptions(options);
|
|
3763
3829
|
if (requestClosed) throw new Error("Database request has settled; it cannot open another transaction");
|
|
3764
3830
|
if (atomicScope.getStore() !== void 0) throw new Error("Nested Database.$atomic is not supported; use tx.$attempt for a savepoint");
|
|
3831
|
+
if (authorized && !applicationTouched && tx.opened) {
|
|
3832
|
+
authorizationRelease ??= (async () => {
|
|
3833
|
+
await tx.commit();
|
|
3834
|
+
tx = requestTransaction();
|
|
3835
|
+
})();
|
|
3836
|
+
}
|
|
3837
|
+
if (authorizationRelease) await authorizationRelease;
|
|
3765
3838
|
const retries = retryBudget(options.retry);
|
|
3766
|
-
const
|
|
3767
|
-
|
|
3839
|
+
const atomicSignal = options.signal && signal ? AbortSignal.any([
|
|
3840
|
+
options.signal,
|
|
3841
|
+
signal
|
|
3842
|
+
]) : options.signal ?? signal;
|
|
3843
|
+
const control = options.timeoutMs !== void 0 || atomicSignal !== void 0 ? new TransactionControl(options.timeoutMs, atomicSignal) : void 0;
|
|
3768
3844
|
try {
|
|
3769
3845
|
control?.check();
|
|
3770
|
-
if (options.workload) {
|
|
3771
|
-
const queuedAt = trace ? performance.now() : 0;
|
|
3772
|
-
try {
|
|
3773
|
-
lease = await workloadAdmission(sql, options.workload).acquire(workloadTenant(options.workload, identity.claimsJson), options.conflictKeys ?? [], control?.signal);
|
|
3774
|
-
} finally {
|
|
3775
|
-
trace?.time("queueMs", queuedAt);
|
|
3776
|
-
}
|
|
3777
|
-
}
|
|
3778
3846
|
for (let attempt = 0; ; attempt++) {
|
|
3779
3847
|
control?.check();
|
|
3780
3848
|
const timeouts = [
|
|
@@ -3807,19 +3875,23 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
3807
3875
|
}
|
|
3808
3876
|
} finally {
|
|
3809
3877
|
control?.dispose();
|
|
3810
|
-
lease?.release();
|
|
3811
3878
|
}
|
|
3812
3879
|
}, "atomic");
|
|
3813
|
-
const
|
|
3814
|
-
trace
|
|
3815
|
-
|
|
3880
|
+
const requestTransaction = /* @__PURE__ */ __name(() => createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
|
|
3881
|
+
trace,
|
|
3882
|
+
control: requestControl
|
|
3883
|
+
}), "requestTransaction");
|
|
3884
|
+
let tx = requestTransaction();
|
|
3816
3885
|
let serviceTx = null;
|
|
3886
|
+
let serviceControl;
|
|
3817
3887
|
let serviceClient = null;
|
|
3818
3888
|
const asService = /* @__PURE__ */ __name(() => {
|
|
3819
3889
|
if (serviceClient === null) {
|
|
3890
|
+
serviceControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
|
|
3820
3891
|
serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
|
|
3821
3892
|
lockTimeout: SERVICE_LOCK_TIMEOUT,
|
|
3822
|
-
trace
|
|
3893
|
+
trace,
|
|
3894
|
+
control: serviceControl
|
|
3823
3895
|
});
|
|
3824
3896
|
serviceClient = Object.assign(withTables(createOps(serviceTx, serviceContext)), {
|
|
3825
3897
|
atomic: atomic(identity.serviceRole, "service")
|
|
@@ -3828,16 +3900,24 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
3828
3900
|
return serviceClient;
|
|
3829
3901
|
}, "asService");
|
|
3830
3902
|
return {
|
|
3831
|
-
client: Object.assign(withTables(createOps(
|
|
3903
|
+
client: Object.assign(withTables(createOps(() => {
|
|
3904
|
+
if (authorized) applicationTouched = true;
|
|
3905
|
+
return authorizationRelease ? authorizationRelease.then(() => tx) : tx;
|
|
3906
|
+
}, userContext)), {
|
|
3832
3907
|
asService,
|
|
3833
3908
|
atomic: atomic(identity.role, "user")
|
|
3834
3909
|
}),
|
|
3910
|
+
authorizationComplete() {
|
|
3911
|
+
authorized = true;
|
|
3912
|
+
},
|
|
3835
3913
|
async commit() {
|
|
3836
3914
|
requestClosed = true;
|
|
3837
3915
|
try {
|
|
3838
3916
|
await tx.commit();
|
|
3839
3917
|
await serviceTx?.commit();
|
|
3840
3918
|
trace?.complete("committed");
|
|
3919
|
+
requestControl?.dispose();
|
|
3920
|
+
serviceControl?.dispose();
|
|
3841
3921
|
} catch (error) {
|
|
3842
3922
|
trace?.failed();
|
|
3843
3923
|
throw error;
|
|
@@ -3847,6 +3927,8 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
3847
3927
|
requestClosed = true;
|
|
3848
3928
|
await tx.rollback(reason);
|
|
3849
3929
|
await serviceTx?.rollback(reason);
|
|
3930
|
+
requestControl?.dispose();
|
|
3931
|
+
serviceControl?.dispose();
|
|
3850
3932
|
trace?.complete("failed");
|
|
3851
3933
|
}
|
|
3852
3934
|
};
|
|
@@ -4154,7 +4236,7 @@ function compileCommand(plan) {
|
|
|
4154
4236
|
};
|
|
4155
4237
|
});
|
|
4156
4238
|
steps.push({
|
|
4157
|
-
|
|
4239
|
+
sql,
|
|
4158
4240
|
bindings,
|
|
4159
4241
|
guard: op.guard
|
|
4160
4242
|
});
|
|
@@ -4177,11 +4259,15 @@ function compileCommand(plan) {
|
|
|
4177
4259
|
rows_affected: 1
|
|
4178
4260
|
});
|
|
4179
4261
|
}
|
|
4262
|
+
if (!hasRefs) return {
|
|
4263
|
+
steps: [],
|
|
4264
|
+
pipeline
|
|
4265
|
+
};
|
|
4180
4266
|
return {
|
|
4181
|
-
steps,
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
}
|
|
4267
|
+
steps: steps.map(({ sql, ...step }) => sql === void 0 ? step : {
|
|
4268
|
+
...step,
|
|
4269
|
+
chunks: parameterChunks(sql)
|
|
4270
|
+
})
|
|
4185
4271
|
};
|
|
4186
4272
|
}
|
|
4187
4273
|
__name(compileCommand, "compileCommand");
|
|
@@ -4589,9 +4675,11 @@ function makeSseWriter(opts) {
|
|
|
4589
4675
|
if (!begun) {
|
|
4590
4676
|
begun = true;
|
|
4591
4677
|
chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
|
|
4678
|
+
void chain.catch(() => void 0);
|
|
4592
4679
|
return;
|
|
4593
4680
|
}
|
|
4594
4681
|
chain = chain.then(() => opts.enqueue(frame));
|
|
4682
|
+
void chain.catch(() => void 0);
|
|
4595
4683
|
},
|
|
4596
4684
|
started() {
|
|
4597
4685
|
return begun;
|
|
@@ -4804,6 +4892,23 @@ async function createApp(opts) {
|
|
|
4804
4892
|
throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
4805
4893
|
}
|
|
4806
4894
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
4895
|
+
const admission = sql.admission ?? databaseAdmission(sql, {
|
|
4896
|
+
concurrency: config.poolMax,
|
|
4897
|
+
...config.databaseAdmission
|
|
4898
|
+
});
|
|
4899
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
4900
|
+
let shuttingDown;
|
|
4901
|
+
let accepting = true;
|
|
4902
|
+
async function serving(run) {
|
|
4903
|
+
const work = run();
|
|
4904
|
+
inFlight.add(work);
|
|
4905
|
+
try {
|
|
4906
|
+
return await work;
|
|
4907
|
+
} finally {
|
|
4908
|
+
inFlight.delete(work);
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
__name(serving, "serving");
|
|
4807
4912
|
await sql.unsafe("select 1");
|
|
4808
4913
|
setSchema(opts.schemas ?? []);
|
|
4809
4914
|
setCursorKey(config.cursorSecret || config.serviceRoleKey || null);
|
|
@@ -4870,6 +4975,19 @@ async function createApp(opts) {
|
|
|
4870
4975
|
}
|
|
4871
4976
|
}
|
|
4872
4977
|
__name(runInServiceScope, "runInServiceScope");
|
|
4978
|
+
function errorResponse(err, endpoint, requestId) {
|
|
4979
|
+
if (isHttpError(err)) {
|
|
4980
|
+
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
4981
|
+
log.error(`[engine] unhandled ${err.error} in ${endpoint}`, err);
|
|
4982
|
+
}
|
|
4983
|
+
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
4984
|
+
data: err.data
|
|
4985
|
+
} : void 0);
|
|
4986
|
+
}
|
|
4987
|
+
log.error(`[engine] unhandled error in ${endpoint}`, err);
|
|
4988
|
+
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
4989
|
+
}
|
|
4990
|
+
__name(errorResponse, "errorResponse");
|
|
4873
4991
|
async function handleAuthorize(req, requestId) {
|
|
4874
4992
|
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
4875
4993
|
return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
|
|
@@ -4892,7 +5010,7 @@ async function createApp(opts) {
|
|
|
4892
5010
|
...config.databaseDiagnostics,
|
|
4893
5011
|
endpoint: "upload authorization",
|
|
4894
5012
|
requestId
|
|
4895
|
-
});
|
|
5013
|
+
}, req.signal);
|
|
4896
5014
|
try {
|
|
4897
5015
|
const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
|
|
4898
5016
|
if (refusal) {
|
|
@@ -4902,7 +5020,7 @@ async function createApp(opts) {
|
|
|
4902
5020
|
await uploadDb.commit();
|
|
4903
5021
|
} catch (err) {
|
|
4904
5022
|
await uploadDb.rollback(err);
|
|
4905
|
-
|
|
5023
|
+
return errorResponse(err, "upload authorization", requestId);
|
|
4906
5024
|
}
|
|
4907
5025
|
const grant = grantFor(target?.entry, {
|
|
4908
5026
|
userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
|
|
@@ -4944,256 +5062,257 @@ async function createApp(opts) {
|
|
|
4944
5062
|
...meta.options.databaseBudget,
|
|
4945
5063
|
endpoint: hit.entry.id,
|
|
4946
5064
|
requestId
|
|
4947
|
-
});
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
5065
|
+
}, req.signal);
|
|
5066
|
+
try {
|
|
5067
|
+
const rolesOfCaller = makeRoleResolver(db.client);
|
|
5068
|
+
let dispatched = false;
|
|
5069
|
+
const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
|
|
5070
|
+
if (dispatched) return;
|
|
5071
|
+
await db.rollback(new Error("request refused before dispatch"));
|
|
5072
|
+
}, "settleBeforeRefusal");
|
|
5073
|
+
const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
|
|
5074
|
+
if (refusal) {
|
|
5075
|
+
await settleBeforeRefusal();
|
|
5076
|
+
return refusal;
|
|
5077
|
+
}
|
|
5078
|
+
if (claims && spec.verifiedEmail && claims.email_verified !== true) {
|
|
5079
|
+
await settleBeforeRefusal();
|
|
5080
|
+
return envelope("email_not_verified", "A verified email address is required", 403, requestId);
|
|
5081
|
+
}
|
|
5082
|
+
let parsedBody;
|
|
5083
|
+
let bodyRead = false;
|
|
5084
|
+
let rawBody = null;
|
|
5085
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
5086
|
+
const declared = Number(req.headers.get("content-length") ?? "");
|
|
5087
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
5088
|
+
await settleBeforeRefusal();
|
|
5089
|
+
return new Response(JSON.stringify({
|
|
5090
|
+
error: "payload_too_large",
|
|
5091
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
5092
|
+
status: 413,
|
|
5093
|
+
request_id: requestId
|
|
5094
|
+
}), {
|
|
5095
|
+
status: 413,
|
|
5096
|
+
headers: JSON_HEADERS
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
const buf = await req.arrayBuffer().catch(() => null);
|
|
5100
|
+
if (buf && buf.byteLength > MAX_BODY_BYTES) {
|
|
5101
|
+
await settleBeforeRefusal();
|
|
5102
|
+
return new Response(JSON.stringify({
|
|
5103
|
+
error: "payload_too_large",
|
|
5104
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
5105
|
+
status: 413,
|
|
5106
|
+
request_id: requestId
|
|
5107
|
+
}), {
|
|
5108
|
+
status: 413,
|
|
5109
|
+
headers: JSON_HEADERS
|
|
5110
|
+
});
|
|
5111
|
+
}
|
|
5112
|
+
if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
|
|
5113
|
+
}
|
|
5114
|
+
const readParsedBody = /* @__PURE__ */ __name(() => {
|
|
5115
|
+
if (bodyRead) return parsedBody;
|
|
5116
|
+
bodyRead = true;
|
|
5117
|
+
if (!rawBody) {
|
|
5118
|
+
parsedBody = {};
|
|
5119
|
+
return parsedBody;
|
|
5120
|
+
}
|
|
5121
|
+
try {
|
|
5122
|
+
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
5123
|
+
} catch {
|
|
5124
|
+
parsedBody = {};
|
|
5125
|
+
}
|
|
5126
|
+
return parsedBody;
|
|
5127
|
+
}, "readParsedBody");
|
|
5128
|
+
const attest = await attestGate(req, rawBody, {
|
|
5129
|
+
userId
|
|
5130
|
+
});
|
|
5131
|
+
if (attest.observation) observations.record(attest.observation);
|
|
5132
|
+
if (attest.outcome === "reject") {
|
|
5133
|
+
const headers = {
|
|
5134
|
+
...JSON_HEADERS
|
|
5135
|
+
};
|
|
5136
|
+
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
4969
5137
|
await settleBeforeRefusal();
|
|
4970
5138
|
return new Response(JSON.stringify({
|
|
4971
|
-
error:
|
|
4972
|
-
error_description:
|
|
4973
|
-
status:
|
|
5139
|
+
error: attest.reason,
|
|
5140
|
+
error_description: "This endpoint requires a verified app instance",
|
|
5141
|
+
status: 401,
|
|
4974
5142
|
request_id: requestId
|
|
4975
5143
|
}), {
|
|
4976
|
-
status:
|
|
4977
|
-
headers
|
|
5144
|
+
status: 401,
|
|
5145
|
+
headers
|
|
4978
5146
|
});
|
|
4979
5147
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
5148
|
+
if (attest.device && claims) attestedDevice = attest.device;
|
|
5149
|
+
attestChallengeOut = attest.nextChallenge;
|
|
5150
|
+
const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
|
|
5151
|
+
if (retryAfter !== null) {
|
|
4982
5152
|
await settleBeforeRefusal();
|
|
4983
5153
|
return new Response(JSON.stringify({
|
|
4984
|
-
error: "
|
|
4985
|
-
error_description:
|
|
4986
|
-
status:
|
|
4987
|
-
request_id: requestId
|
|
5154
|
+
error: "too_many_requests",
|
|
5155
|
+
error_description: "Rate limit exceeded for this endpoint",
|
|
5156
|
+
status: 429,
|
|
5157
|
+
request_id: requestId,
|
|
5158
|
+
data: {
|
|
5159
|
+
retryAfter
|
|
5160
|
+
}
|
|
4988
5161
|
}), {
|
|
4989
|
-
status:
|
|
4990
|
-
headers:
|
|
5162
|
+
status: 429,
|
|
5163
|
+
headers: {
|
|
5164
|
+
...JSON_HEADERS,
|
|
5165
|
+
"retry-after": String(retryAfter)
|
|
5166
|
+
}
|
|
4991
5167
|
});
|
|
4992
5168
|
}
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
}
|
|
5002
|
-
try {
|
|
5003
|
-
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
5004
|
-
} catch {
|
|
5005
|
-
parsedBody = {};
|
|
5006
|
-
}
|
|
5007
|
-
return parsedBody;
|
|
5008
|
-
}, "readParsedBody");
|
|
5009
|
-
const attest = await attestGate(req, rawBody, {
|
|
5010
|
-
userId
|
|
5011
|
-
});
|
|
5012
|
-
if (attest.observation) observations.record(attest.observation);
|
|
5013
|
-
if (attest.outcome === "reject") {
|
|
5014
|
-
const headers = {
|
|
5015
|
-
...JSON_HEADERS
|
|
5016
|
-
};
|
|
5017
|
-
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
5018
|
-
await settleBeforeRefusal();
|
|
5019
|
-
return new Response(JSON.stringify({
|
|
5020
|
-
error: attest.reason,
|
|
5021
|
-
error_description: "This endpoint requires a verified app instance",
|
|
5022
|
-
status: 401,
|
|
5023
|
-
request_id: requestId
|
|
5024
|
-
}), {
|
|
5025
|
-
status: 401,
|
|
5026
|
-
headers
|
|
5027
|
-
});
|
|
5028
|
-
}
|
|
5029
|
-
if (attest.device && claims) attestedDevice = attest.device;
|
|
5030
|
-
attestChallengeOut = attest.nextChallenge;
|
|
5031
|
-
const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
|
|
5032
|
-
if (retryAfter !== null) {
|
|
5033
|
-
await settleBeforeRefusal();
|
|
5034
|
-
return new Response(JSON.stringify({
|
|
5035
|
-
error: "too_many_requests",
|
|
5036
|
-
error_description: "Rate limit exceeded for this endpoint",
|
|
5037
|
-
status: 429,
|
|
5038
|
-
request_id: requestId,
|
|
5039
|
-
data: {
|
|
5040
|
-
retryAfter
|
|
5041
|
-
}
|
|
5042
|
-
}), {
|
|
5043
|
-
status: 429,
|
|
5044
|
-
headers: {
|
|
5045
|
-
...JSON_HEADERS,
|
|
5046
|
-
"retry-after": String(retryAfter)
|
|
5047
|
-
}
|
|
5169
|
+
let completionUploadId = null;
|
|
5170
|
+
const args = [];
|
|
5171
|
+
let sseEnqueue = null;
|
|
5172
|
+
let sseSettle = /* @__PURE__ */ __name(async () => {
|
|
5173
|
+
}, "sseSettle");
|
|
5174
|
+
const sseWriter = makeSseWriter({
|
|
5175
|
+
enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
|
|
5176
|
+
onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
|
|
5048
5177
|
});
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
const r = p.schema.safeParse(parsedBody);
|
|
5064
|
-
if (!r.success) {
|
|
5065
|
-
await settleBeforeRefusal();
|
|
5066
|
-
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
5067
|
-
data: {
|
|
5068
|
-
fields: fieldErrors(r.error)
|
|
5069
|
-
}
|
|
5070
|
-
});
|
|
5071
|
-
}
|
|
5072
|
-
args[p.index] = r.data;
|
|
5073
|
-
break;
|
|
5074
|
-
}
|
|
5075
|
-
case "query": {
|
|
5076
|
-
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
5077
|
-
if (!r.success) {
|
|
5078
|
-
await settleBeforeRefusal();
|
|
5079
|
-
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
5080
|
-
data: {
|
|
5081
|
-
fields: fieldErrors(r.error)
|
|
5082
|
-
}
|
|
5083
|
-
});
|
|
5084
|
-
}
|
|
5085
|
-
args[p.index] = r.data;
|
|
5086
|
-
break;
|
|
5087
|
-
}
|
|
5088
|
-
case "param":
|
|
5089
|
-
args[p.index] = hit.params[p.name];
|
|
5090
|
-
break;
|
|
5091
|
-
case "headers": {
|
|
5092
|
-
const raw = Object.fromEntries(req.headers);
|
|
5093
|
-
if (!p.schema) {
|
|
5094
|
-
args[p.index] = raw;
|
|
5178
|
+
for (const p of meta.params ?? []) {
|
|
5179
|
+
switch (p.kind) {
|
|
5180
|
+
case "body": {
|
|
5181
|
+
readParsedBody();
|
|
5182
|
+
const r = p.schema.safeParse(parsedBody);
|
|
5183
|
+
if (!r.success) {
|
|
5184
|
+
await settleBeforeRefusal();
|
|
5185
|
+
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
5186
|
+
data: {
|
|
5187
|
+
fields: fieldErrors(r.error)
|
|
5188
|
+
}
|
|
5189
|
+
});
|
|
5190
|
+
}
|
|
5191
|
+
args[p.index] = r.data;
|
|
5095
5192
|
break;
|
|
5096
5193
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
case "user":
|
|
5110
|
-
case "optionalUser":
|
|
5111
|
-
args[p.index] = claims ? {
|
|
5112
|
-
id: userId,
|
|
5113
|
-
email: claims.email,
|
|
5114
|
-
role: claims.role,
|
|
5115
|
-
emailVerified: claims.email_verified === true,
|
|
5116
|
-
metadata: claims.metadata ?? {},
|
|
5117
|
-
// The caller's APPLICATION roles, from `auth.user_roles` — the
|
|
5118
|
-
// same resolution the gate used, so the handler cannot read a
|
|
5119
|
-
// different set than the one it was admitted on. Never from a
|
|
5120
|
-
// claim: a revoked role has to bite on the next request, and a
|
|
5121
|
-
// token says only what was true when it was minted.
|
|
5122
|
-
roles: await rolesOfCaller(),
|
|
5123
|
-
// Server-owned and only ever filled from a verdict palauth
|
|
5124
|
-
// returned. Never from a raw header, never from JWT metadata —
|
|
5125
|
-
// the type has said so since it was written and this is the
|
|
5126
|
-
// first writer.
|
|
5127
|
-
device: attestedDevice
|
|
5128
|
-
} : null;
|
|
5129
|
-
break;
|
|
5130
|
-
case "uploadedObject": {
|
|
5131
|
-
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
5132
|
-
await settleBeforeRefusal();
|
|
5133
|
-
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
5134
|
-
}
|
|
5135
|
-
readParsedBody();
|
|
5136
|
-
const envelopeIn = parsedBody;
|
|
5137
|
-
if (!envelopeIn?.uploadedObject) {
|
|
5138
|
-
await settleBeforeRefusal();
|
|
5139
|
-
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
5194
|
+
case "query": {
|
|
5195
|
+
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
5196
|
+
if (!r.success) {
|
|
5197
|
+
await settleBeforeRefusal();
|
|
5198
|
+
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
5199
|
+
data: {
|
|
5200
|
+
fields: fieldErrors(r.error)
|
|
5201
|
+
}
|
|
5202
|
+
});
|
|
5203
|
+
}
|
|
5204
|
+
args[p.index] = r.data;
|
|
5205
|
+
break;
|
|
5140
5206
|
}
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5207
|
+
case "param":
|
|
5208
|
+
args[p.index] = hit.params[p.name];
|
|
5209
|
+
break;
|
|
5210
|
+
case "headers": {
|
|
5211
|
+
const raw = Object.fromEntries(req.headers);
|
|
5212
|
+
if (!p.schema) {
|
|
5213
|
+
args[p.index] = raw;
|
|
5214
|
+
break;
|
|
5215
|
+
}
|
|
5216
|
+
const r = p.schema.safeParse(headersFor(raw, p.schema));
|
|
5217
|
+
if (!r.success) {
|
|
5145
5218
|
await settleBeforeRefusal();
|
|
5146
|
-
return
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
} : void 0
|
|
5219
|
+
return envelope("bad_request", "Request headers failed validation", 400, requestId, {
|
|
5220
|
+
data: {
|
|
5221
|
+
fields: fieldErrors(r.error)
|
|
5222
|
+
}
|
|
5151
5223
|
});
|
|
5152
5224
|
}
|
|
5153
|
-
|
|
5225
|
+
args[p.index] = r.data;
|
|
5226
|
+
break;
|
|
5227
|
+
}
|
|
5228
|
+
case "user":
|
|
5229
|
+
case "optionalUser":
|
|
5230
|
+
args[p.index] = claims ? {
|
|
5231
|
+
id: userId,
|
|
5232
|
+
email: claims.email,
|
|
5233
|
+
role: claims.role,
|
|
5234
|
+
emailVerified: claims.email_verified === true,
|
|
5235
|
+
metadata: claims.metadata ?? {},
|
|
5236
|
+
// The caller's APPLICATION roles, from `auth.user_roles` — the
|
|
5237
|
+
// same resolution the gate used, so the handler cannot read a
|
|
5238
|
+
// different set than the one it was admitted on. Never from a
|
|
5239
|
+
// claim: a revoked role has to bite on the next request, and a
|
|
5240
|
+
// token says only what was true when it was minted.
|
|
5241
|
+
roles: await rolesOfCaller(),
|
|
5242
|
+
// Server-owned and only ever filled from a verdict palauth
|
|
5243
|
+
// returned. Never from a raw header, never from JWT metadata —
|
|
5244
|
+
// the type has said so since it was written and this is the
|
|
5245
|
+
// first writer.
|
|
5246
|
+
device: attestedDevice
|
|
5247
|
+
} : null;
|
|
5248
|
+
break;
|
|
5249
|
+
case "uploadedObject": {
|
|
5250
|
+
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
5251
|
+
await settleBeforeRefusal();
|
|
5252
|
+
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
5253
|
+
}
|
|
5254
|
+
readParsedBody();
|
|
5255
|
+
const envelopeIn = parsedBody;
|
|
5256
|
+
if (!envelopeIn?.uploadedObject) {
|
|
5257
|
+
await settleBeforeRefusal();
|
|
5258
|
+
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
5259
|
+
}
|
|
5260
|
+
const uploadId = envelopeIn.uploadedObject.uploadId;
|
|
5261
|
+
if (typeof uploadId === "string" && uploadId !== "") {
|
|
5262
|
+
const already = completions.recall(uploadId);
|
|
5263
|
+
if (already) {
|
|
5264
|
+
await settleBeforeRefusal();
|
|
5265
|
+
return new Response(already.body, {
|
|
5266
|
+
status: already.status,
|
|
5267
|
+
headers: already.contentType ? {
|
|
5268
|
+
"content-type": already.contentType
|
|
5269
|
+
} : void 0
|
|
5270
|
+
});
|
|
5271
|
+
}
|
|
5272
|
+
completionUploadId = uploadId;
|
|
5273
|
+
}
|
|
5274
|
+
args[p.index] = envelopeIn.uploadedObject;
|
|
5275
|
+
parsedBody = envelopeIn.body ?? {};
|
|
5276
|
+
break;
|
|
5154
5277
|
}
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5278
|
+
case "requestId":
|
|
5279
|
+
args[p.index] = requestId;
|
|
5280
|
+
break;
|
|
5281
|
+
case "traceId":
|
|
5282
|
+
args[p.index] = requestId;
|
|
5283
|
+
break;
|
|
5284
|
+
case "client":
|
|
5285
|
+
args[p.index] = {
|
|
5286
|
+
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
5287
|
+
appVersion: req.headers.get("x-palbase-client-version"),
|
|
5288
|
+
platform: req.headers.get("x-platform"),
|
|
5289
|
+
osVersion: req.headers.get("x-os-version")
|
|
5290
|
+
};
|
|
5291
|
+
break;
|
|
5292
|
+
case "req":
|
|
5293
|
+
args[p.index] = req;
|
|
5294
|
+
break;
|
|
5295
|
+
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
5296
|
+
// state when the client disconnects. Measured on Bun: an infinite
|
|
5297
|
+
// producer guarded by it stops within a few frames of the client dying.
|
|
5298
|
+
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
5299
|
+
// request-scoped data and no signal.
|
|
5300
|
+
case "signal":
|
|
5301
|
+
args[p.index] = req.signal;
|
|
5302
|
+
break;
|
|
5303
|
+
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
5304
|
+
// on any other route it is an inert writer whose frames go nowhere,
|
|
5305
|
+
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
5306
|
+
// route.
|
|
5307
|
+
case "sseOut":
|
|
5308
|
+
args[p.index] = sseWriter;
|
|
5309
|
+
break;
|
|
5310
|
+
default:
|
|
5311
|
+
args[p.index] = void 0;
|
|
5158
5312
|
}
|
|
5159
|
-
case "requestId":
|
|
5160
|
-
args[p.index] = requestId;
|
|
5161
|
-
break;
|
|
5162
|
-
case "traceId":
|
|
5163
|
-
args[p.index] = requestId;
|
|
5164
|
-
break;
|
|
5165
|
-
case "client":
|
|
5166
|
-
args[p.index] = {
|
|
5167
|
-
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
5168
|
-
appVersion: req.headers.get("x-palbase-client-version"),
|
|
5169
|
-
platform: req.headers.get("x-platform"),
|
|
5170
|
-
osVersion: req.headers.get("x-os-version")
|
|
5171
|
-
};
|
|
5172
|
-
break;
|
|
5173
|
-
case "req":
|
|
5174
|
-
args[p.index] = req;
|
|
5175
|
-
break;
|
|
5176
|
-
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
5177
|
-
// state when the client disconnects. Measured on Bun: an infinite
|
|
5178
|
-
// producer guarded by it stops within a few frames of the client dying.
|
|
5179
|
-
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
5180
|
-
// request-scoped data and no signal.
|
|
5181
|
-
case "signal":
|
|
5182
|
-
args[p.index] = req.signal;
|
|
5183
|
-
break;
|
|
5184
|
-
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
5185
|
-
// on any other route it is an inert writer whose frames go nowhere,
|
|
5186
|
-
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
5187
|
-
// route.
|
|
5188
|
-
case "sseOut":
|
|
5189
|
-
args[p.index] = sseWriter;
|
|
5190
|
-
break;
|
|
5191
|
-
default:
|
|
5192
|
-
args[p.index] = void 0;
|
|
5193
5313
|
}
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
try {
|
|
5314
|
+
db.authorizationComplete?.();
|
|
5315
|
+
dispatched = true;
|
|
5197
5316
|
const services = buildServices(db);
|
|
5198
5317
|
if (meta.options?.sseConfig !== void 0) {
|
|
5199
5318
|
let sseController;
|
|
@@ -5209,11 +5328,19 @@ async function createApp(opts) {
|
|
|
5209
5328
|
} catch {
|
|
5210
5329
|
}
|
|
5211
5330
|
}, "sseEnqueue");
|
|
5212
|
-
let sawFirstFrame;
|
|
5213
|
-
const firstFrame = new Promise((
|
|
5331
|
+
let sawFirstFrame, refuseFirstFrame;
|
|
5332
|
+
const firstFrame = new Promise((resolve, reject) => {
|
|
5333
|
+
sawFirstFrame = resolve;
|
|
5334
|
+
refuseFirstFrame = reject;
|
|
5335
|
+
});
|
|
5214
5336
|
sseSettle = /* @__PURE__ */ __name(async () => {
|
|
5215
|
-
|
|
5216
|
-
|
|
5337
|
+
try {
|
|
5338
|
+
await db.commit();
|
|
5339
|
+
sawFirstFrame();
|
|
5340
|
+
} catch (error) {
|
|
5341
|
+
refuseFirstFrame(error);
|
|
5342
|
+
throw error;
|
|
5343
|
+
}
|
|
5217
5344
|
}, "sseSettle");
|
|
5218
5345
|
const guarded = {
|
|
5219
5346
|
...services,
|
|
@@ -5254,6 +5381,7 @@ async function createApp(opts) {
|
|
|
5254
5381
|
status: 204
|
|
5255
5382
|
});
|
|
5256
5383
|
}
|
|
5384
|
+
await firstFrame;
|
|
5257
5385
|
void (async () => {
|
|
5258
5386
|
await ran;
|
|
5259
5387
|
await sseWriter.drained();
|
|
@@ -5287,15 +5415,16 @@ async function createApp(opts) {
|
|
|
5287
5415
|
}
|
|
5288
5416
|
return method.apply(hit.entry.instance, args);
|
|
5289
5417
|
});
|
|
5290
|
-
await db.commit();
|
|
5291
5418
|
if (meta.returnSchema) {
|
|
5292
5419
|
const v = meta.returnSchema.safeParse(result);
|
|
5293
5420
|
if (!v.success) {
|
|
5294
5421
|
log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
|
|
5422
|
+
await db.rollback(new Error("output_invalid"));
|
|
5295
5423
|
return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
|
|
5296
5424
|
}
|
|
5297
5425
|
}
|
|
5298
|
-
if (result === void 0 || result === null) {
|
|
5426
|
+
if (result === void 0 || result === null && !meta.returnSchema) {
|
|
5427
|
+
await db.commit();
|
|
5299
5428
|
if (completionUploadId) {
|
|
5300
5429
|
completions.remember(completionUploadId, {
|
|
5301
5430
|
status: 204,
|
|
@@ -5308,6 +5437,7 @@ async function createApp(opts) {
|
|
|
5308
5437
|
});
|
|
5309
5438
|
}
|
|
5310
5439
|
const payload = JSON.stringify(result);
|
|
5440
|
+
await db.commit();
|
|
5311
5441
|
if (completionUploadId) {
|
|
5312
5442
|
completions.remember(completionUploadId, {
|
|
5313
5443
|
status: 200,
|
|
@@ -5324,16 +5454,7 @@ async function createApp(opts) {
|
|
|
5324
5454
|
});
|
|
5325
5455
|
} catch (err) {
|
|
5326
5456
|
await db.rollback(err);
|
|
5327
|
-
|
|
5328
|
-
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
5329
|
-
log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
|
|
5330
|
-
}
|
|
5331
|
-
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
5332
|
-
data: err.data
|
|
5333
|
-
} : void 0);
|
|
5334
|
-
}
|
|
5335
|
-
log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
|
|
5336
|
-
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
5457
|
+
return errorResponse(err, hit.entry.id, requestId);
|
|
5337
5458
|
}
|
|
5338
5459
|
}
|
|
5339
5460
|
__name(handle, "handle");
|
|
@@ -5346,15 +5467,24 @@ async function createApp(opts) {
|
|
|
5346
5467
|
throw err;
|
|
5347
5468
|
}
|
|
5348
5469
|
return {
|
|
5349
|
-
handle,
|
|
5470
|
+
handle: /* @__PURE__ */ __name((req) => !accepting ? Promise.resolve(envelope("runtime_draining", "This application is shutting down", 503, `req_${crypto.randomUUID()}`)) : serving(() => handle(req)), "handle"),
|
|
5471
|
+
get databasePressure() {
|
|
5472
|
+
return admission.snapshot;
|
|
5473
|
+
},
|
|
5350
5474
|
routes,
|
|
5351
5475
|
config,
|
|
5352
5476
|
container: liveContainer,
|
|
5353
|
-
runInServiceScope,
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5477
|
+
runInServiceScope: /* @__PURE__ */ __name((fn) => !accepting ? Promise.reject(new Error("This application is shutting down; no new database scope can start")) : serving(() => runInServiceScope(fn)), "runInServiceScope"),
|
|
5478
|
+
shutdown() {
|
|
5479
|
+
accepting = false;
|
|
5480
|
+
return shuttingDown ??= (async () => {
|
|
5481
|
+
while (inFlight.size) await Promise.allSettled([
|
|
5482
|
+
...inFlight
|
|
5483
|
+
]);
|
|
5484
|
+
await runShutdownHooks();
|
|
5485
|
+
await closeDriver(sql);
|
|
5486
|
+
released = true;
|
|
5487
|
+
})();
|
|
5358
5488
|
}
|
|
5359
5489
|
};
|
|
5360
5490
|
}
|
|
@@ -5389,4 +5519,4 @@ export {
|
|
|
5389
5519
|
installEgressFence,
|
|
5390
5520
|
createApp
|
|
5391
5521
|
};
|
|
5392
|
-
//# sourceMappingURL=chunk-
|
|
5522
|
+
//# sourceMappingURL=chunk-EHKEMHB4.js.map
|