terrascale 0.3.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/LICENSE +202 -0
- package/README.md +143 -0
- package/package.json +159 -0
- package/sdk-current-contract.json +27 -0
- package/sdk-route-manifest.json +67 -0
- package/src/admin.js +9 -0
- package/src/better-auth.js +14 -0
- package/src/config.js +121 -0
- package/src/database-codec.js +845 -0
- package/src/database-types.js +237 -0
- package/src/database-view.js +422 -0
- package/src/database.js +420 -0
- package/src/discovery.js +374 -0
- package/src/http.js +887 -0
- package/src/index.js +79 -0
- package/src/local/authentication.js +47 -0
- package/src/local/better-auth.js +517 -0
- package/src/local/cli.js +51 -0
- package/src/local/context.js +23 -0
- package/src/local/environment.js +109 -0
- package/src/local/index.js +204 -0
- package/src/local/router.js +999 -0
- package/src/local/server.js +664 -0
- package/src/local/store.js +530 -0
- package/src/local/test-environment.js +74 -0
- package/src/management-contracts.js +72 -0
- package/src/management.js +12 -0
- package/src/native-origin.js +75 -0
- package/src/postgres.js +494 -0
- package/src/react/core.js +743 -0
- package/src/react/index.js +99 -0
- package/src/result.js +251 -0
- package/src/schema.js +366 -0
- package/src/sql.js +996 -0
- package/src/svelte/index.js +129 -0
- package/src/tanstack/index.js +511 -0
- package/src/ts-auth-discovery.js +190 -0
- package/src/ts-auth.js +3497 -0
- package/types/admin.d.ts +6 -0
- package/types/better-auth.d.ts +8 -0
- package/types/config.d.ts +58 -0
- package/types/database-codec.d.ts +111 -0
- package/types/database-types.d.ts +213 -0
- package/types/database-view.d.ts +183 -0
- package/types/database.d.ts +98 -0
- package/types/discovery.d.ts +114 -0
- package/types/http.d.ts +46 -0
- package/types/index.d.ts +52 -0
- package/types/local/authentication.d.ts +11 -0
- package/types/local/better-auth.d.ts +33 -0
- package/types/local/cli.d.ts +2 -0
- package/types/local/context.d.ts +14 -0
- package/types/local/environment.d.ts +23 -0
- package/types/local/index.d.ts +94 -0
- package/types/local/router.d.ts +66 -0
- package/types/local/server.d.ts +54 -0
- package/types/local/store.d.ts +106 -0
- package/types/local/test-environment.d.ts +25 -0
- package/types/management-contracts.d.ts +44 -0
- package/types/management.d.ts +6 -0
- package/types/native-origin.d.ts +23 -0
- package/types/postgres.d.ts +123 -0
- package/types/react/core.d.ts +366 -0
- package/types/react/index.d.ts +54 -0
- package/types/result.d.ts +161 -0
- package/types/schema.d.ts +145 -0
- package/types/sql.d.ts +288 -0
- package/types/svelte/index.d.ts +81 -0
- package/types/tanstack/index.d.ts +165 -0
- package/types/ts-auth-discovery.d.ts +11 -0
- package/types/ts-auth.d.ts +1826 -0
package/src/discovery.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { parseDatabaseJson, requireDatabaseU64, requireDatabaseUuid } from "./database-codec.js";
|
|
2
|
+
import { nativeRegionalOrigin, requireNativeRegion, terraScaleServiceDomain } from "./native-origin.js";
|
|
3
|
+
/** @import { FetchLike } from "./config.js" */
|
|
4
|
+
/** @import { DatabaseScope } from "./database-types.js" */
|
|
5
|
+
|
|
6
|
+
/** Frozen authenticated native receiver; no alternate metadata origin is admitted. */
|
|
7
|
+
export const terraBaseDiscoveryOrigin = /** @type {const} */ (`https://atlas.${terraScaleServiceDomain}`);
|
|
8
|
+
const maximumResponseBytes = 256 * 1024;
|
|
9
|
+
const maximumEndpoints = 20;
|
|
10
|
+
const nilUuid = "00000000-0000-0000-0000-000000000000";
|
|
11
|
+
const capabilityNames = /** @type {const} */ (["client_v1", "read", "query", "subscriptions", "write", "conditional_mutation", "atomic_groups", "security_state"]);
|
|
12
|
+
|
|
13
|
+
/** @typedef {typeof capabilityNames[number]} TerraBaseRoutingCapability */
|
|
14
|
+
/** @typedef {Readonly<Record<TerraBaseRoutingCapability, boolean>>} TerraBaseRoutingCapabilities */
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* readonly region: string;
|
|
18
|
+
* readonly url: string;
|
|
19
|
+
* readonly priority: number;
|
|
20
|
+
* readonly catalog: { readonly digest: string };
|
|
21
|
+
* readonly capabilities: TerraBaseRoutingCapabilities;
|
|
22
|
+
* readonly security_writer: null;
|
|
23
|
+
* }} TerraBaseRoutingEndpoint
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* readonly schema_version: 1;
|
|
28
|
+
* readonly scope: DatabaseScope;
|
|
29
|
+
* readonly authority: {
|
|
30
|
+
* readonly deployment_id: string;
|
|
31
|
+
* readonly cell: string;
|
|
32
|
+
* readonly publisher_id: string;
|
|
33
|
+
* readonly publisher_generation: bigint;
|
|
34
|
+
* };
|
|
35
|
+
* readonly revision: bigint;
|
|
36
|
+
* readonly published_at: number;
|
|
37
|
+
* readonly expires_at: number;
|
|
38
|
+
* readonly refreshSeconds: number;
|
|
39
|
+
* readonly residency: readonly string[] | null;
|
|
40
|
+
* readonly endpoints: readonly TerraBaseRoutingEndpoint[];
|
|
41
|
+
* }} TerraBaseRoutingDocument
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export class TerraBaseDiscoveryError extends Error {
|
|
45
|
+
/**
|
|
46
|
+
* @readonly
|
|
47
|
+
* @type {string}
|
|
48
|
+
*/
|
|
49
|
+
code;
|
|
50
|
+
/**
|
|
51
|
+
* @readonly
|
|
52
|
+
* @type {number | undefined}
|
|
53
|
+
*/
|
|
54
|
+
status;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} code
|
|
58
|
+
* @param {number} [status]
|
|
59
|
+
*/
|
|
60
|
+
constructor(code, status) {
|
|
61
|
+
super(code);
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.status = status;
|
|
64
|
+
this.name = "TerraBaseDiscoveryError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {unknown} value
|
|
70
|
+
* @param {readonly string[]} fields
|
|
71
|
+
* @returns {Record<string, unknown>}
|
|
72
|
+
*/
|
|
73
|
+
function closed(value, fields) {
|
|
74
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Expected a discovery object.");
|
|
75
|
+
const entry = /** @type {Record<string, unknown>} */ (value);
|
|
76
|
+
if (fields.some(key => !Object.hasOwn(entry, key)) || Object.keys(entry).some(key => !fields.includes(key))) {
|
|
77
|
+
throw new TypeError("Discovery object has missing or unknown fields.");
|
|
78
|
+
}
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {unknown} value
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function uuid(value) {
|
|
87
|
+
const id = requireDatabaseUuid(value);
|
|
88
|
+
if (id === nilUuid) throw new TypeError("Discovery UUID must be nonnil.");
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {unknown} value
|
|
94
|
+
* @returns {bigint}
|
|
95
|
+
*/
|
|
96
|
+
function decimal(value) {
|
|
97
|
+
if (typeof value !== "string" || !/^(0|[1-9][0-9]{0,19})$/u.test(value)) throw new TypeError("Discovery u64 must be canonical decimal.");
|
|
98
|
+
return requireDatabaseU64(BigInt(value), "discovery u64", true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {unknown} value
|
|
103
|
+
* @param {boolean} [token]
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
function identity(value, token = false) {
|
|
107
|
+
if (typeof value !== "string" || !(token ? /^[a-z0-9][a-z0-9-]{0,62}$/u : /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u).test(value)) {
|
|
108
|
+
throw new TypeError("Invalid discovery authority identity.");
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {unknown} value
|
|
115
|
+
* @returns {number}
|
|
116
|
+
*/
|
|
117
|
+
function unix(value) {
|
|
118
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 253402300799) throw new TypeError("Invalid discovery timestamp.");
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse only the current closed native8 descriptor, without widening its lease or capabilities.
|
|
124
|
+
* @param {unknown} payload
|
|
125
|
+
* @param {DatabaseScope} expected
|
|
126
|
+
* @param {number} [nowMs]
|
|
127
|
+
* @returns {TerraBaseRoutingDocument}
|
|
128
|
+
*/
|
|
129
|
+
export function parseTerraBaseRoutingDocument(payload, expected, nowMs = Date.now()) {
|
|
130
|
+
const doc = closed(payload, ["schema_version", "scope", "authority", "revision", "published_at", "expires_at", "refreshSeconds", "residency", "endpoints"]);
|
|
131
|
+
if (doc.schema_version !== 1) throw new TypeError("Unsupported discovery schema.");
|
|
132
|
+
const target = closed(doc.scope, ["tenant_id", "database_id", "shard_id"]);
|
|
133
|
+
const scope = Object.freeze({ tenant: uuid(target.tenant_id), database: uuid(target.database_id), shard: decimal(target.shard_id) });
|
|
134
|
+
if (scope.tenant !== expected.tenant || scope.database !== expected.database || scope.shard !== expected.shard) throw new TypeError("Discovery belongs to another native scope.");
|
|
135
|
+
const source = closed(doc.authority, ["deployment_id", "cell", "publisher_id", "publisher_generation"]);
|
|
136
|
+
const authority = Object.freeze({ deployment_id: identity(source.deployment_id), cell: identity(source.cell, true), publisher_id: identity(source.publisher_id, true), publisher_generation: decimal(source.publisher_generation) });
|
|
137
|
+
const published_at = unix(doc.published_at), expires_at = unix(doc.expires_at);
|
|
138
|
+
if (!Number.isFinite(nowMs) || published_at * 1000 > nowMs || expires_at * 1000 <= nowMs || expires_at <= published_at || expires_at - published_at > 10) {
|
|
139
|
+
throw new TypeError("Discovery interval is expired, future, or outside its bound.");
|
|
140
|
+
}
|
|
141
|
+
const refreshSeconds = doc.refreshSeconds;
|
|
142
|
+
if (typeof refreshSeconds !== "number" || !Number.isInteger(refreshSeconds) || refreshSeconds < 1 || refreshSeconds > 10) throw new TypeError("Invalid discovery refresh interval.");
|
|
143
|
+
/** @type {readonly string[] | null} */
|
|
144
|
+
let residency = null;
|
|
145
|
+
if (doc.residency !== null) {
|
|
146
|
+
if (!Array.isArray(doc.residency) || doc.residency.length < 1 || doc.residency.length > maximumEndpoints) throw new TypeError("Invalid native residency.");
|
|
147
|
+
const codes = Array.from(doc.residency).map(requireNativeRegion);
|
|
148
|
+
if (new Set(codes).size !== codes.length) throw new TypeError("Duplicate residency region.");
|
|
149
|
+
residency = Object.freeze(codes);
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(doc.endpoints) || doc.endpoints.length < 1 || doc.endpoints.length > maximumEndpoints) throw new TypeError("Invalid discovery endpoint list.");
|
|
152
|
+
/** @type {Set<string>} */
|
|
153
|
+
const regions = new Set();
|
|
154
|
+
/** @type {Set<number>} */
|
|
155
|
+
const priorities = new Set();
|
|
156
|
+
const count = doc.endpoints.length;
|
|
157
|
+
const endpoints = Array.from(doc.endpoints).map(raw => {
|
|
158
|
+
const endpoint = closed(raw, ["region", "url", "priority", "catalog", "capabilities", "security_writer"]);
|
|
159
|
+
const code = requireNativeRegion(endpoint.region), priority = endpoint.priority;
|
|
160
|
+
if (regions.has(code) || typeof priority !== "number" || !Number.isInteger(priority) || priority < 1 || priority > count || priorities.has(priority)) throw new TypeError("Invalid or duplicate endpoint rank.");
|
|
161
|
+
regions.add(code); priorities.add(priority);
|
|
162
|
+
if (endpoint.url !== nativeRegionalOrigin(code) || (residency !== null && !residency.includes(code))) throw new TypeError("Endpoint is not an eligible canonical native origin.");
|
|
163
|
+
const catalog = closed(endpoint.catalog, ["digest"]);
|
|
164
|
+
if (typeof catalog.digest !== "string" || !/^[0-9a-f]{64}$/u.test(catalog.digest)) throw new TypeError("Invalid catalog digest.");
|
|
165
|
+
const caps = closed(endpoint.capabilities, capabilityNames);
|
|
166
|
+
for (const name of capabilityNames) if (typeof caps[name] !== "boolean") throw new TypeError("Discovery capabilities must be explicit booleans.");
|
|
167
|
+
if (caps.client_v1 !== true || (caps.read !== true && caps.write !== true) || caps.conditional_mutation !== false || caps.atomic_groups !== false || caps.security_state !== false || endpoint.security_writer !== null) {
|
|
168
|
+
throw new TypeError("Unsupported or ineligible native capability descriptor.");
|
|
169
|
+
}
|
|
170
|
+
return /** @type {TerraBaseRoutingEndpoint} */ (Object.freeze({ region: code, url: endpoint.url, priority, catalog: Object.freeze({ digest: catalog.digest }), capabilities: /** @type {TerraBaseRoutingCapabilities} */ (Object.freeze({ ...caps })), security_writer: null }));
|
|
171
|
+
});
|
|
172
|
+
return Object.freeze({ schema_version: 1, scope, authority, revision: decimal(doc.revision), published_at, expires_at, refreshSeconds, residency, endpoints: Object.freeze(endpoints) });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} key
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
function credential(key) {
|
|
180
|
+
// The unused trailing bits must be zero in unpadded base64url16/base64url32.
|
|
181
|
+
if (!/^tbk1\.[A-Za-z0-9_-]{21}[AQgw]\.[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/u.test(key)) {
|
|
182
|
+
throw new TerraBaseDiscoveryError("invalid_discovery_credential");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @template T
|
|
188
|
+
* @param {Promise<T>} promise
|
|
189
|
+
* @param {AbortSignal} signal
|
|
190
|
+
* @returns {Promise<T>}
|
|
191
|
+
*/
|
|
192
|
+
async function cancellable(promise, signal) {
|
|
193
|
+
signal.throwIfAborted();
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
/** @returns {void} */
|
|
196
|
+
const abort = () => reject(signal.reason);
|
|
197
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
198
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {Response} response
|
|
204
|
+
* @param {AbortSignal} signal
|
|
205
|
+
* @returns {Promise<unknown>}
|
|
206
|
+
*/
|
|
207
|
+
async function responseJson(response, signal) {
|
|
208
|
+
if (response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") throw new TypeError("Expected discovery JSON.");
|
|
209
|
+
const cache = response.headers.get("cache-control")?.toLowerCase().split(",").map(part => part.trim());
|
|
210
|
+
if (!cache?.includes("private") || !cache.includes("no-store") || cache.includes("public")) throw new TypeError("Discovery response must be private, no-store.");
|
|
211
|
+
const reader = response.body?.getReader();
|
|
212
|
+
if (reader === undefined) throw new TypeError("Discovery response has no body.");
|
|
213
|
+
/** @type {Uint8Array[]} */
|
|
214
|
+
const parts = []; let size = 0;
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
const part = await cancellable(reader.read(), signal);
|
|
218
|
+
if (part.done) break;
|
|
219
|
+
size += part.value.length;
|
|
220
|
+
if (size > maximumResponseBytes) throw new TypeError("Discovery body exceeds its bound.");
|
|
221
|
+
parts.push(part.value);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) { void reader.cancel().catch(() => undefined); throw error; }
|
|
224
|
+
finally { reader.releaseLock(); }
|
|
225
|
+
const bytes = new Uint8Array(size); let offset = 0;
|
|
226
|
+
for (const part of parts) { bytes.set(part, offset); offset += part.length; }
|
|
227
|
+
return parseDatabaseJson(bytes);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {unknown} payload
|
|
232
|
+
* @param {number} status
|
|
233
|
+
* @returns {TerraBaseDiscoveryError}
|
|
234
|
+
*/
|
|
235
|
+
function failure(payload, status) {
|
|
236
|
+
const error = closed(closed(payload, ["error"]).error, ["code", "message", "fix"]);
|
|
237
|
+
/** @type {Readonly<Record<number, readonly string[]>>} */
|
|
238
|
+
const admitted = { 400: ["invalid_request", "invalid_scope", "invalid_cursor"], 403: ["discovery_forbidden"], 503: ["discovery_unconfigured", "no_eligible_region", "routing_observation_regressed"] };
|
|
239
|
+
if (typeof error.code !== "string" || !admitted[status]?.includes(error.code) || typeof error.message !== "string" || typeof error.fix !== "string") throw new TypeError("Invalid discovery error envelope.");
|
|
240
|
+
return new TerraBaseDiscoveryError(error.code, status);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {TerraBaseRoutingDocument} document
|
|
245
|
+
* @param {TerraBaseRoutingDocument} floor
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
function observationRegressed(document, floor) {
|
|
249
|
+
const current = document.authority, previous = floor.authority;
|
|
250
|
+
return current.deployment_id !== previous.deployment_id || current.cell !== previous.cell ||
|
|
251
|
+
current.publisher_generation < previous.publisher_generation || document.revision < floor.revision ||
|
|
252
|
+
(current.publisher_generation === previous.publisher_generation && document.revision === floor.revision && current.publisher_id !== previous.publisher_id);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Per-scope observation floors survive lease expiry and credential rotation for this client. */
|
|
256
|
+
export class TerraBaseDiscoveryRouter {
|
|
257
|
+
/** @readonly @type {DatabaseScope} */
|
|
258
|
+
#scope;
|
|
259
|
+
/** @readonly @type {FetchLike} */
|
|
260
|
+
#fetch;
|
|
261
|
+
/** @readonly @type {number} */
|
|
262
|
+
#timeoutMs;
|
|
263
|
+
/** @type {TerraBaseRoutingDocument | undefined} */
|
|
264
|
+
#observation;
|
|
265
|
+
/** @type {{ readonly fingerprint: string; readonly document: TerraBaseRoutingDocument; readonly wallDeadline: number; readonly monotonicDeadline: number } | undefined} */
|
|
266
|
+
#cache;
|
|
267
|
+
/** @readonly @type {WeakMap<TerraBaseRoutingEndpoint, { readonly document: TerraBaseRoutingDocument; readonly wallDeadline: number; readonly monotonicDeadline: number }>} */
|
|
268
|
+
#leases = new WeakMap();
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {DatabaseScope} scope
|
|
272
|
+
* @param {FetchLike} [fetch]
|
|
273
|
+
* @param {number} [timeoutMs]
|
|
274
|
+
*/
|
|
275
|
+
constructor(scope, fetch = globalThis.fetch.bind(globalThis), timeoutMs = 3000) {
|
|
276
|
+
this.#scope = Object.freeze({ tenant: uuid(scope.tenant), database: uuid(scope.database), shard: requireDatabaseU64(scope.shard, "shard") });
|
|
277
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw new TypeError("Discovery timeout is outside its bounds.");
|
|
278
|
+
this.#fetch = fetch; this.#timeoutMs = timeoutMs;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {string} apiKey
|
|
283
|
+
* @param {readonly TerraBaseRoutingCapability[]} required
|
|
284
|
+
* @param {AbortSignal} [callerSignal]
|
|
285
|
+
* @returns {Promise<TerraBaseRoutingEndpoint>}
|
|
286
|
+
*/
|
|
287
|
+
async resolve(apiKey, required, callerSignal) {
|
|
288
|
+
const signal = AbortSignal.any([AbortSignal.timeout(this.#timeoutMs), ...(callerSignal === undefined ? [] : [callerSignal])]);
|
|
289
|
+
signal.throwIfAborted(); credential(apiKey);
|
|
290
|
+
if (required.some(name => !capabilityNames.includes(name))) throw new TypeError("Unknown native routing capability.");
|
|
291
|
+
const hash = await cancellable(crypto.subtle.digest("SHA-256", new TextEncoder().encode(apiKey)), signal);
|
|
292
|
+
const fingerprint = Array.from(new Uint8Array(hash), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
293
|
+
const cached = this.#cache, wallNow = Date.now(), monotonicNow = performance.now();
|
|
294
|
+
let doc = cached?.fingerprint === fingerprint && cached.document.published_at * 1000 <= wallNow && wallNow < cached.wallDeadline && monotonicNow < cached.monotonicDeadline ? cached.document : undefined;
|
|
295
|
+
if (doc === undefined) {
|
|
296
|
+
this.#cache = undefined;
|
|
297
|
+
const startedWall = Date.now(), startedMonotonic = performance.now();
|
|
298
|
+
const url = new URL("/api/v1/discovery", terraBaseDiscoveryOrigin);
|
|
299
|
+
url.search = new URLSearchParams({ tenant_id: this.#scope.tenant, database_id: this.#scope.database, shard_id: this.#scope.shard.toString() }).toString();
|
|
300
|
+
if (this.#observation !== undefined) {
|
|
301
|
+
url.searchParams.set("observed_deployment_id", this.#observation.authority.deployment_id);
|
|
302
|
+
url.searchParams.set("observed_cell", this.#observation.authority.cell);
|
|
303
|
+
url.searchParams.set("observed_publisher_generation", this.#observation.authority.publisher_generation.toString());
|
|
304
|
+
url.searchParams.set("rev", this.#observation.revision.toString());
|
|
305
|
+
}
|
|
306
|
+
const request = new Request(url, { method: "GET", headers: { accept: "application/json", authorization: `Bearer ${apiKey}` }, credentials: "omit", redirect: "manual", cache: "no-store", referrerPolicy: "no-referrer", signal });
|
|
307
|
+
/** @type {Response | undefined} */
|
|
308
|
+
let response;
|
|
309
|
+
/** @type {Promise<Response> | undefined} */
|
|
310
|
+
let pendingResponse;
|
|
311
|
+
try {
|
|
312
|
+
pendingResponse = this.#fetch(request);
|
|
313
|
+
response = await cancellable(pendingResponse, signal);
|
|
314
|
+
if (response.redirected) throw new TypeError("Discovery redirects are not admitted.");
|
|
315
|
+
if (response.status !== 200 && ![400, 403, 503].includes(response.status)) throw new TypeError("Unadmitted discovery HTTP status.");
|
|
316
|
+
const payload = await responseJson(response, signal);
|
|
317
|
+
if (response.status !== 200) throw failure(payload, response.status);
|
|
318
|
+
doc = parseTerraBaseRoutingDocument(payload, this.#scope);
|
|
319
|
+
const previous = this.#observation;
|
|
320
|
+
if (previous !== undefined && observationRegressed(doc, previous)) {
|
|
321
|
+
throw new TerraBaseDiscoveryError("routing_observation_regressed");
|
|
322
|
+
}
|
|
323
|
+
const wallDeadline = Math.min(doc.expires_at * 1000, startedWall + doc.refreshSeconds * 1000);
|
|
324
|
+
const monotonicDeadline = Math.min(startedMonotonic + doc.refreshSeconds * 1000, performance.now() + doc.expires_at * 1000 - Date.now());
|
|
325
|
+
if (Date.now() >= wallDeadline || performance.now() >= monotonicDeadline) throw new TerraBaseDiscoveryError("routing_expired");
|
|
326
|
+
this.#observation = doc;
|
|
327
|
+
this.#cache = { fingerprint, document: doc, wallDeadline, monotonicDeadline };
|
|
328
|
+
} catch (error) {
|
|
329
|
+
/**
|
|
330
|
+
* @param {Response} unread
|
|
331
|
+
* @returns {void}
|
|
332
|
+
*/
|
|
333
|
+
const discard = (unread) => {
|
|
334
|
+
try {
|
|
335
|
+
if (!unread.bodyUsed) void unread.body?.cancel().catch(() => undefined);
|
|
336
|
+
} catch {
|
|
337
|
+
// Custom response properties and cancel methods may throw; cleanup is best effort.
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
if (response !== undefined) discard(response);
|
|
341
|
+
// A custom fetch can ignore abort and return later; cleanup must not delay the caller.
|
|
342
|
+
else void pendingResponse?.then(discard, () => undefined).catch(() => undefined);
|
|
343
|
+
if (signal.aborted) {
|
|
344
|
+
if (callerSignal?.aborted) throw callerSignal.reason;
|
|
345
|
+
throw new TerraBaseDiscoveryError("discovery_unavailable");
|
|
346
|
+
}
|
|
347
|
+
if (error instanceof TerraBaseDiscoveryError) throw error;
|
|
348
|
+
if (response !== undefined && (error instanceof TypeError || error instanceof SyntaxError)) throw new TerraBaseDiscoveryError("invalid_response", response.status);
|
|
349
|
+
throw new TerraBaseDiscoveryError("discovery_unavailable");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
signal.throwIfAborted();
|
|
353
|
+
const endpoint = [...doc.endpoints].sort((a, b) => a.priority - b.priority).find(candidate => required.every(name => candidate.capabilities[name]));
|
|
354
|
+
if (endpoint === undefined) throw new TerraBaseDiscoveryError("capability_unavailable");
|
|
355
|
+
// Both the cached and freshly fetched paths leave the admitted lease in #cache.
|
|
356
|
+
const cache = /** @type {{ readonly wallDeadline: number; readonly monotonicDeadline: number }} */ (this.#cache);
|
|
357
|
+
this.#leases.set(endpoint, { document: doc, wallDeadline: cache.wallDeadline, monotonicDeadline: cache.monotonicDeadline });
|
|
358
|
+
return endpoint;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Recheck immediately before native dispatch, including concurrently advanced floors.
|
|
363
|
+
* @param {TerraBaseRoutingEndpoint} endpoint
|
|
364
|
+
* @returns {void}
|
|
365
|
+
*/
|
|
366
|
+
assertCurrent(endpoint) {
|
|
367
|
+
const lease = this.#leases.get(endpoint), observed = this.#observation;
|
|
368
|
+
const wallNow = Date.now();
|
|
369
|
+
if (lease === undefined || lease.document.published_at * 1000 > wallNow || wallNow >= lease.wallDeadline || performance.now() >= lease.monotonicDeadline) throw new TerraBaseDiscoveryError("routing_expired");
|
|
370
|
+
if (observed === undefined || observationRegressed(lease.document, observed)) {
|
|
371
|
+
throw new TerraBaseDiscoveryError("routing_observation_regressed");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|