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.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +143 -0
  3. package/package.json +159 -0
  4. package/sdk-current-contract.json +27 -0
  5. package/sdk-route-manifest.json +67 -0
  6. package/src/admin.js +9 -0
  7. package/src/better-auth.js +14 -0
  8. package/src/config.js +121 -0
  9. package/src/database-codec.js +845 -0
  10. package/src/database-types.js +237 -0
  11. package/src/database-view.js +422 -0
  12. package/src/database.js +420 -0
  13. package/src/discovery.js +374 -0
  14. package/src/http.js +887 -0
  15. package/src/index.js +79 -0
  16. package/src/local/authentication.js +47 -0
  17. package/src/local/better-auth.js +517 -0
  18. package/src/local/cli.js +51 -0
  19. package/src/local/context.js +23 -0
  20. package/src/local/environment.js +109 -0
  21. package/src/local/index.js +204 -0
  22. package/src/local/router.js +999 -0
  23. package/src/local/server.js +664 -0
  24. package/src/local/store.js +530 -0
  25. package/src/local/test-environment.js +74 -0
  26. package/src/management-contracts.js +72 -0
  27. package/src/management.js +12 -0
  28. package/src/native-origin.js +75 -0
  29. package/src/postgres.js +494 -0
  30. package/src/react/core.js +743 -0
  31. package/src/react/index.js +99 -0
  32. package/src/result.js +251 -0
  33. package/src/schema.js +366 -0
  34. package/src/sql.js +996 -0
  35. package/src/svelte/index.js +129 -0
  36. package/src/tanstack/index.js +511 -0
  37. package/src/ts-auth-discovery.js +190 -0
  38. package/src/ts-auth.js +3497 -0
  39. package/types/admin.d.ts +6 -0
  40. package/types/better-auth.d.ts +8 -0
  41. package/types/config.d.ts +58 -0
  42. package/types/database-codec.d.ts +111 -0
  43. package/types/database-types.d.ts +213 -0
  44. package/types/database-view.d.ts +183 -0
  45. package/types/database.d.ts +98 -0
  46. package/types/discovery.d.ts +114 -0
  47. package/types/http.d.ts +46 -0
  48. package/types/index.d.ts +52 -0
  49. package/types/local/authentication.d.ts +11 -0
  50. package/types/local/better-auth.d.ts +33 -0
  51. package/types/local/cli.d.ts +2 -0
  52. package/types/local/context.d.ts +14 -0
  53. package/types/local/environment.d.ts +23 -0
  54. package/types/local/index.d.ts +94 -0
  55. package/types/local/router.d.ts +66 -0
  56. package/types/local/server.d.ts +54 -0
  57. package/types/local/store.d.ts +106 -0
  58. package/types/local/test-environment.d.ts +25 -0
  59. package/types/management-contracts.d.ts +44 -0
  60. package/types/management.d.ts +6 -0
  61. package/types/native-origin.d.ts +23 -0
  62. package/types/postgres.d.ts +123 -0
  63. package/types/react/core.d.ts +366 -0
  64. package/types/react/index.d.ts +54 -0
  65. package/types/result.d.ts +161 -0
  66. package/types/schema.d.ts +145 -0
  67. package/types/sql.d.ts +288 -0
  68. package/types/svelte/index.d.ts +81 -0
  69. package/types/tanstack/index.d.ts +165 -0
  70. package/types/ts-auth-discovery.d.ts +11 -0
  71. package/types/ts-auth.d.ts +1826 -0
@@ -0,0 +1,420 @@
1
+ import {
2
+ decodeDatabaseCommitReceipt, decodeDatabasePoint, decodeDatabaseQueryPage,
3
+ decodeDatabaseSubscription, decodeDatabaseStatus, encodeDatabaseStatus, encodeDatabaseCommit, encodeDatabaseQuery,
4
+ encodeDatabaseSubscription, encodeDatabaseAuthority, parseDatabaseJson, requireDatabaseU64, requireDatabaseUuid,
5
+ } from "./database-codec.js";
6
+ import { TerraBaseDiscoveryError, TerraBaseDiscoveryRouter } from "./discovery.js";
7
+ import { nativeServiceOrigin } from "./native-origin.js";
8
+ /**
9
+ * @import {
10
+ * DatabaseAuthority, DatabaseCommit, DatabaseCommitReceipt, DatabasePoint,
11
+ * DatabaseQuery, DatabaseQueryPage, DatabaseResult, DatabaseScope,
12
+ * DatabaseSubscriptionRequest, DatabaseSubscriptionResponse, DatabaseTransactionIdentity,
13
+ * DatabaseTransactionStatus,
14
+ * } from "./database-types.js"
15
+ * @import { FetchLike } from "./config.js"
16
+ * @import { TerraBaseRoutingCapability } from "./discovery.js"
17
+ */
18
+
19
+ const maximumResponseBytes = 4 * 1024 * 1024;
20
+
21
+ /**
22
+ * - `apiKey`: native service key used for authenticated Atlas discovery and the selected regional listener.
23
+ * - `endpoint`: a provisioned direct origin. Omit to use authenticated native Atlas discovery.
24
+ * - `discoveryTimeoutMs`: bounds the canonical Atlas request within the complete operation deadline.
25
+ * @typedef {DatabaseScope & {
26
+ * readonly apiKey: string | (() => string | Promise<string>);
27
+ * readonly endpoint?: string;
28
+ * readonly discoveryTimeoutMs?: number;
29
+ * readonly requestTimeoutMs?: number;
30
+ * readonly fetch?: FetchLike;
31
+ * }} TerraBaseClientConfig
32
+ */
33
+
34
+ /** @typedef {{ readonly signal?: AbortSignal }} DatabaseRequestOptions */
35
+
36
+ // Type-level brand only; the symbol is never attached to a value.
37
+ const preparedBrand = Symbol("preparedBrand");
38
+ /** @typedef {DatabaseTransactionIdentity & { readonly [preparedBrand]: true }} PreparedDatabaseCommit */
39
+ /** @type {WeakMap<PreparedDatabaseCommit, { readonly client: TerraBaseClient; readonly scope: string; readonly endpoint: string | undefined; readonly body: string; readonly capabilities: readonly TerraBaseRoutingCapability[] }>} */
40
+ const preparedRequests = new WeakMap();
41
+
42
+ /**
43
+ * @param {number | undefined} value
44
+ * @param {number} defaultValue
45
+ * @param {number} maximum
46
+ * @returns {number}
47
+ */
48
+ function timeout(value, defaultValue, maximum) {
49
+ const resolved = value ?? defaultValue;
50
+ if (!Number.isInteger(resolved) || resolved < 1 || resolved > maximum) throw new TypeError("Timeout is outside its bounds.");
51
+ return resolved;
52
+ }
53
+
54
+ /**
55
+ * @param {DatabaseAuthority} authority
56
+ * @returns {DatabaseAuthority}
57
+ */
58
+ function identityAuthority(authority) {
59
+ return authority.mode === "leaderless" ? Object.freeze({ ...authority })
60
+ : Object.freeze({ ...authority, writer: Object.freeze({ ...authority.writer }) });
61
+ }
62
+
63
+ /**
64
+ * @param {DatabaseScope} scope
65
+ * @returns {string}
66
+ */
67
+ function scopePath(scope) {
68
+ return `/v1/tenants/${requireDatabaseUuid(scope.tenant, "tenant")}/databases/${requireDatabaseUuid(scope.database, "database")}/shards/${requireDatabaseU64(scope.shard, "shard")}`;
69
+ }
70
+
71
+ /**
72
+ * @param {unknown} raw
73
+ * @returns {string}
74
+ */
75
+ function routeError(raw) {
76
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw) || Object.keys(raw).some(key => key !== "error")) return "invalid_response";
77
+ const error = /** @type {Record<string, unknown>} */ (raw).error;
78
+ if (typeof error !== "object" || error === null || Array.isArray(error)) return "invalid_response";
79
+ const value = /** @type {Record<string, unknown>} */ (error);
80
+ if (Object.keys(value).some(key => key !== "code")) return "invalid_response";
81
+ return typeof value.code === "string" && /^[a-z][a-z0-9_]{0,63}$/u.test(value.code) ? value.code : "invalid_response";
82
+ }
83
+
84
+ /**
85
+ * @param {Response} response
86
+ * @param {AbortSignal} signal
87
+ * @returns {Promise<unknown>}
88
+ */
89
+ async function responseJson(response, signal) {
90
+ const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
91
+ if (mediaType !== "application/json") {
92
+ void response.body?.cancel().catch(() => undefined);
93
+ throw new TypeError("Expected application/json.");
94
+ }
95
+ const reader = response.body?.getReader();
96
+ if (reader === undefined) throw new TypeError("Response has no JSON body.");
97
+ /** @type {Uint8Array[]} */
98
+ const parts = [];
99
+ let size = 0;
100
+ try {
101
+ while (true) {
102
+ /** @type {ReadableStreamReadResult<Uint8Array>} */
103
+ let part;
104
+ try { part = await withCancellation(reader.read(), signal); }
105
+ catch (error) {
106
+ if (signal.aborted) throw error;
107
+ throw new Error("Native response transport failed.", { cause: error });
108
+ }
109
+ if (part.done) break;
110
+ size += part.value.length;
111
+ if (size > maximumResponseBytes) throw new TypeError("Response exceeds its byte limit.");
112
+ parts.push(part.value);
113
+ }
114
+ } catch (error) {
115
+ void reader.cancel().catch(() => undefined);
116
+ throw error;
117
+ } finally { reader.releaseLock(); }
118
+ const bytes = new Uint8Array(size);
119
+ let offset = 0;
120
+ for (const part of parts) { bytes.set(part, offset); offset += part.length; }
121
+ return parseDatabaseJson(bytes);
122
+ }
123
+
124
+ /**
125
+ * @template T
126
+ * @param {Promise<T>} promise
127
+ * @param {AbortSignal} signal
128
+ * @param {(value: T) => void} [disposeLateValue]
129
+ * @returns {Promise<T>}
130
+ */
131
+ function withCancellation(promise, signal, disposeLateValue) {
132
+ return new Promise((resolve, reject) => {
133
+ /** @returns {void} */
134
+ const cleanup = () => signal.removeEventListener("abort", abort);
135
+ /** @returns {void} */
136
+ const abort = () => {
137
+ cleanup();
138
+ reject(signal.reason);
139
+ };
140
+ if (signal.aborted) abort();
141
+ else signal.addEventListener("abort", abort, { once: true });
142
+ // Observe the started operation even if it aborted synchronously before
143
+ // this wrapper ran; a late fetch result still owns a response body.
144
+ void promise.then(
145
+ value => {
146
+ cleanup();
147
+ if (signal.aborted) {
148
+ try {
149
+ disposeLateValue?.(value);
150
+ } catch {
151
+ // Cleanup cannot replace the terminal cancellation.
152
+ }
153
+ reject(signal.reason);
154
+ } else resolve(value);
155
+ },
156
+ error => {
157
+ cleanup();
158
+ reject(signal.aborted ? signal.reason : error);
159
+ },
160
+ );
161
+ });
162
+ }
163
+
164
+ /** Current scoped document client. All mutations are server commits in one declared shard. */
165
+ export class TerraBaseClient {
166
+ /**
167
+ * @readonly
168
+ * @type {DatabaseScope}
169
+ */
170
+ #scope;
171
+ /**
172
+ * @readonly
173
+ * @type {TerraBaseClientConfig}
174
+ */
175
+ #config;
176
+ /**
177
+ * @readonly
178
+ * @type {FetchLike}
179
+ */
180
+ #fetch;
181
+ /**
182
+ * @readonly
183
+ * @type {string}
184
+ */
185
+ #path;
186
+ /**
187
+ * @readonly
188
+ * @type {string | undefined}
189
+ */
190
+ #endpoint;
191
+ /**
192
+ * @readonly
193
+ * @type {TerraBaseDiscoveryRouter | undefined}
194
+ */
195
+ #discovery;
196
+ /**
197
+ * @readonly
198
+ * @type {number}
199
+ */
200
+ #requestTimeoutMs;
201
+
202
+ /** @returns {DatabaseScope} */
203
+ get scope() { return this.#scope; }
204
+
205
+ /** @param {TerraBaseClientConfig} config */
206
+ constructor(config) {
207
+ this.#scope = Object.freeze({ tenant: requireDatabaseUuid(config.tenant, "tenant"), database: requireDatabaseUuid(config.database, "database"), shard: requireDatabaseU64(config.shard, "shard") });
208
+ this.#config = { ...config };
209
+ this.#fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
210
+ this.#path = scopePath(this.#scope);
211
+ this.#endpoint = config.endpoint === undefined ? undefined : nativeServiceOrigin(config.endpoint);
212
+ this.#requestTimeoutMs = timeout(config.requestTimeoutMs, 30_000, 300_000);
213
+ this.#discovery = this.#endpoint === undefined ? new TerraBaseDiscoveryRouter(this.#scope, this.#fetch, config.discoveryTimeoutMs) : undefined;
214
+ if (typeof config.apiKey !== "function") this.#validateKey(config.apiKey);
215
+ }
216
+
217
+ /**
218
+ * @param {string} collection
219
+ * @param {string} document
220
+ * @param {DatabaseRequestOptions} [options]
221
+ * @returns {Promise<DatabaseResult<DatabasePoint>>}
222
+ */
223
+ read(collection, document, options = {}) {
224
+ const path = `${this.#collectionPath(collection)}/documents/${requireDatabaseUuid(document, "document")}`;
225
+ return this.#request(path, "GET", undefined, raw => this.#scopedPoint(decodeDatabasePoint(raw)), options, ["client_v1", "read"]);
226
+ }
227
+
228
+ /**
229
+ * @param {string} collection
230
+ * @param {DatabaseQuery} query
231
+ * @param {DatabaseRequestOptions} [options]
232
+ * @returns {Promise<DatabaseResult<DatabaseQueryPage>>}
233
+ */
234
+ query(collection, query, options = {}) {
235
+ const limit = query.limit;
236
+ const body = encodeDatabaseQuery(query);
237
+ return this.#request(`${this.#collectionPath(collection)}/documents/query`, "POST", body, raw => {
238
+ const page = decodeDatabaseQueryPage(raw);
239
+ if (page.rows.length > limit) throw new TypeError("Query page exceeds the requested limit.");
240
+ page.rows.forEach(row => this.#scopedPoint(row.point));
241
+ return page;
242
+ }, options, ["client_v1", "read", "query"]);
243
+ }
244
+
245
+ /**
246
+ * @param {string} collection
247
+ * @param {DatabaseSubscriptionRequest} request
248
+ * @param {DatabaseRequestOptions} [options]
249
+ * @returns {Promise<DatabaseResult<DatabaseSubscriptionResponse>>}
250
+ */
251
+ subscription(collection, request, options = {}) {
252
+ const phase = request.phase;
253
+ const limit = request.phase === "snapshot" ? request.limit : undefined;
254
+ const body = encodeDatabaseSubscription(request);
255
+ return this.#request(`${this.#collectionPath(collection)}/documents/subscriptions`, "POST", body, raw => {
256
+ const result = decodeDatabaseSubscription(raw);
257
+ if (result.phase !== phase) throw new TypeError("Subscription response belongs to another phase.");
258
+ if (result.phase === "snapshot" && limit !== undefined && result.rows.length > limit) throw new TypeError("Snapshot page exceeds the requested limit.");
259
+ if (result.phase === "snapshot") result.rows.forEach(row => this.#scopedPoint(row.point));
260
+ else result.groups.forEach(group => group.effects.forEach(effect => { if (effect.point !== null) this.#scopedPoint(effect.point); }));
261
+ return result;
262
+ }, options, ["client_v1", "read", "subscriptions"]);
263
+ }
264
+
265
+ /**
266
+ * Prepare once and retain this identity for exact retry and durable status resolution.
267
+ * @param {DatabaseCommit} commit
268
+ * @returns {Promise<PreparedDatabaseCommit>}
269
+ */
270
+ async prepareCommit(commit) {
271
+ const transaction_id = Object.freeze({ ...commit.transaction_id });
272
+ const authority = identityAuthority(commit.authority);
273
+ /** @type {TerraBaseRoutingCapability[]} */
274
+ const capabilities = ["client_v1", "write"];
275
+ if (commit.writes.some(write => write.condition.kind !== "none")) capabilities.push("conditional_mutation");
276
+ if (commit.writes.length > 1) capabilities.push("atomic_groups");
277
+ if (authority.mode === "single_writer") capabilities.push("security_state");
278
+ const encoded = await encodeDatabaseCommit(this.#scope, commit);
279
+ const prepared = /** @type {PreparedDatabaseCommit} */ (Object.freeze({ transaction_id, authority, effect_digest: encoded.effect_digest }));
280
+ preparedRequests.set(prepared, { client: this, scope: this.#path, endpoint: this.#endpoint, body: encoded.body, capabilities: Object.freeze(capabilities) });
281
+ return prepared;
282
+ }
283
+
284
+ /**
285
+ * @param {PreparedDatabaseCommit} prepared
286
+ * @param {DatabaseRequestOptions} [options]
287
+ * @returns {Promise<DatabaseResult<DatabaseCommitReceipt>>}
288
+ */
289
+ commit(prepared, options = {}) {
290
+ const request = preparedRequests.get(prepared);
291
+ if (request === undefined || request.client !== this || request.scope !== this.#path) throw new TypeError("Prepared commit belongs to another client or scope, or was not prepared by this SDK.");
292
+ if (request.endpoint !== this.#endpoint) throw new TypeError("Prepared commit belongs to another endpoint.");
293
+ return this.#request(`${this.#path}/transactions`, "POST", request.body, raw => {
294
+ const receipt = decodeDatabaseCommitReceipt(raw);
295
+ if (receipt.event.database !== this.#scope.database || receipt.event.shard !== this.#scope.shard ||
296
+ receipt.effect_digest !== prepared.effect_digest || JSON.stringify(encodeDatabaseAuthority(receipt.authority)) !== JSON.stringify(encodeDatabaseAuthority(prepared.authority)) ||
297
+ receipt.transaction_id.nonce !== prepared.transaction_id.nonce || receipt.transaction_id.expires_at_unix_ms !== prepared.transaction_id.expires_at_unix_ms) {
298
+ throw new TypeError("Receipt belongs to another operation or database scope.");
299
+ }
300
+ return receipt;
301
+ }, options, request.capabilities, prepared);
302
+ }
303
+
304
+ /**
305
+ * Unknown, pending and expired outcomes remain unresolved; an absent document proves no abort.
306
+ * @param {DatabaseTransactionIdentity} identity
307
+ * @param {DatabaseRequestOptions} [options]
308
+ * @returns {Promise<DatabaseResult<DatabaseTransactionStatus>>}
309
+ */
310
+ transactionStatus(identity, options = {}) {
311
+ const snapshot = Object.freeze({ ...identity, transaction_id: Object.freeze({ ...identity.transaction_id }), authority: identityAuthority(identity.authority) });
312
+ const body = encodeDatabaseStatus(snapshot);
313
+ return this.#request(`${this.#path}/transactions/status`, "POST", body, raw => {
314
+ const status = decodeDatabaseStatus(raw);
315
+ if (status.effect_digest !== snapshot.effect_digest || status.transaction_id.nonce !== snapshot.transaction_id.nonce || status.transaction_id.expires_at_unix_ms !== snapshot.transaction_id.expires_at_unix_ms || JSON.stringify(encodeDatabaseAuthority(status.authority)) !== JSON.stringify(encodeDatabaseAuthority(snapshot.authority))) {
316
+ throw new TypeError("Status belongs to another operation.");
317
+ }
318
+ if (status.state === "committed" && (status.outcome.receipt.event.database !== this.#scope.database || status.outcome.receipt.event.shard !== this.#scope.shard)) throw new TypeError("Status receipt belongs to another database scope.");
319
+ return status;
320
+ }, options, snapshot.authority.mode === "single_writer" ? ["client_v1", "write", "security_state"] : ["client_v1", "write"]);
321
+ }
322
+
323
+ /**
324
+ * @param {string} collection
325
+ * @returns {string}
326
+ */
327
+ #collectionPath(collection) { return `${this.#path}/collections/${requireDatabaseUuid(collection, "collection")}`; }
328
+
329
+ /**
330
+ * @param {DatabasePoint} point
331
+ * @returns {DatabasePoint}
332
+ */
333
+ #scopedPoint(point) {
334
+ if (point.siblings.some(sibling => sibling.event.database !== this.#scope.database || sibling.event.shard !== this.#scope.shard)) {
335
+ throw new TypeError("Point belongs to another database scope.");
336
+ }
337
+ return point;
338
+ }
339
+
340
+ /**
341
+ * @param {unknown} key
342
+ * @returns {string}
343
+ */
344
+ #validateKey(key) {
345
+ if (typeof key !== "string" || key.length === 0 || key.length > 4096 || /[\s\p{Cc}]/u.test(key)) throw new TypeError("Native API key must be nonblank and bounded.");
346
+ return key;
347
+ }
348
+
349
+ /**
350
+ * @template T
351
+ * @param {string} path
352
+ * @param {"GET" | "POST"} method
353
+ * @param {string | undefined} body
354
+ * @param {(raw: unknown) => T} decode
355
+ * @param {DatabaseRequestOptions} options
356
+ * @param {readonly TerraBaseRoutingCapability[]} capabilities
357
+ * @param {DatabaseTransactionIdentity} [transaction]
358
+ * @returns {Promise<DatabaseResult<T>>}
359
+ */
360
+ async #request(path, method, body, decode, options, capabilities, transaction) {
361
+ let sent = false;
362
+ /** @type {number | undefined} */
363
+ let responseStatus;
364
+ const signal = AbortSignal.any([AbortSignal.timeout(this.#requestTimeoutMs), ...(options.signal === undefined ? [] : [options.signal])]);
365
+ try {
366
+ signal.throwIfAborted();
367
+ const apiKey = this.#validateKey(typeof this.#config.apiKey === "function" ? await withCancellation(Promise.resolve(this.#config.apiKey()), signal) : this.#config.apiKey);
368
+ const endpoint = this.#endpoint === undefined ? await /** @type {TerraBaseDiscoveryRouter} */ (this.#discovery).resolve(apiKey, capabilities, signal) : undefined;
369
+ const origin = this.#endpoint ?? /** @type {NonNullable<typeof endpoint>} */ (endpoint).url;
370
+ signal.throwIfAborted();
371
+ const headers = new Headers({ accept: "application/json", authorization: `Bearer ${apiKey}` });
372
+ if (body !== undefined) headers.set("content-type", "application/json");
373
+ const request = new Request(new URL(path, origin), { method, headers, credentials: "omit", redirect: "manual", cache: "no-store", referrerPolicy: "no-referrer", signal, ...(body === undefined ? {} : { body }) });
374
+ if (endpoint !== undefined) /** @type {TerraBaseDiscoveryRouter} */ (this.#discovery).assertCurrent(endpoint);
375
+ sent = true;
376
+ /** @type {Response} */
377
+ let response;
378
+ try {
379
+ response = await withCancellation(this.#fetch(request), signal, late => {
380
+ void late.body?.cancel().catch(() => undefined);
381
+ });
382
+ } catch (error) {
383
+ if (signal.aborted) throw error;
384
+ throw new Error("Native request transport failed.", { cause: error });
385
+ }
386
+ responseStatus = response.status;
387
+ if (signal.aborted) {
388
+ void response.body?.cancel().catch(() => undefined);
389
+ signal.throwIfAborted();
390
+ }
391
+ if (response.redirected) {
392
+ void response.body?.cancel().catch(() => undefined);
393
+ throw new TypeError("Native redirects are not admitted.");
394
+ }
395
+ if (response.ok && response.status !== 200) {
396
+ void response.body?.cancel().catch(() => undefined);
397
+ throw new TypeError("The native protocol admits only HTTP 200 success.");
398
+ }
399
+ const raw = await responseJson(response, signal);
400
+ signal.throwIfAborted();
401
+ if (!response.ok) {
402
+ const code = routeError(raw);
403
+ // Authentication, cancellation and changed-effect errors do not resolve an earlier attempt.
404
+ // Only the producer's retained precondition rejection is a terminal mutation outcome.
405
+ return { ok: false, error: { code, status: response.status, ...(transaction === undefined ? {} : { transaction, mutation_outcome: code === "commit_uncertain" ? "uncertain" : code === "precondition_failed" && response.status === 412 ? "rejected" : "unknown" }) } };
406
+ }
407
+ return { ok: true, status: response.status, value: decode(raw) };
408
+ } catch (error) {
409
+ const code = signal.aborted ? "cancelled" : error instanceof TerraBaseDiscoveryError ? error.code : error instanceof TypeError || error instanceof SyntaxError ? "invalid_response" : "transport_unavailable";
410
+ const status = responseStatus ?? (error instanceof TerraBaseDiscoveryError ? error.status : undefined);
411
+ return { ok: false, error: { code, ...(status === undefined ? {} : { status }), ...(transaction === undefined ? {} : { transaction, mutation_outcome: sent ? "uncertain" : "unknown" }) } };
412
+ }
413
+ }
414
+ }
415
+
416
+ /**
417
+ * @param {TerraBaseClientConfig} config
418
+ * @returns {TerraBaseClient}
419
+ */
420
+ export function createTerraBaseClient(config) { return new TerraBaseClient(config); }