terrascale 0.3.0 → 1.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/CHANGELOG.md +47 -0
- package/README.md +169 -115
- package/package.json +11 -16
- package/sdk-v1-route-manifest.json +2330 -0
- package/src/database-view.js +2 -1
- package/src/database.js +2 -420
- package/src/index.js +58 -75
- package/src/local/index.js +5 -3
- package/src/local/legacy-client.js +411 -0
- package/src/postgres.js +3 -1
- package/src/tanstack/index.js +2 -1
- package/src/v1/decode.js +110 -0
- package/src/v1/errors.js +185 -0
- package/src/v1/filter.js +230 -0
- package/src/v1/ids.js +118 -0
- package/src/v1/json.js +335 -0
- package/src/v1/routes.js +57 -0
- package/src/v1/shapes.js +145 -0
- package/src/v1/terrabase-v1.js +722 -0
- package/src/v1/transport.js +324 -0
- package/types/database-view.d.ts +3 -2
- package/types/database.d.ts +2 -98
- package/types/index.d.ts +42 -51
- package/types/local/index.d.ts +6 -4
- package/types/local/legacy-client.d.ts +94 -0
- package/types/local/test-environment.d.ts +1 -1
- package/types/postgres.d.ts +1 -0
- package/types/tanstack/index.d.ts +3 -2
- package/types/v1/decode.d.ts +105 -0
- package/types/v1/errors.d.ts +107 -0
- package/types/v1/filter.d.ts +148 -0
- package/types/v1/ids.d.ts +44 -0
- package/types/v1/json.d.ts +92 -0
- package/types/v1/routes.d.ts +26 -0
- package/types/v1/shapes.d.ts +78 -0
- package/types/v1/terrabase-v1.d.ts +374 -0
- package/types/v1/transport.d.ts +117 -0
- package/sdk-current-contract.json +0 -27
- package/sdk-route-manifest.json +0 -67
- package/src/discovery.js +0 -374
- package/types/discovery.d.ts +0 -114
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// Deprecated: internal transport for the retired SQLite emulator only.
|
|
2
|
+
import {
|
|
3
|
+
decodeDatabaseCommitReceipt, decodeDatabasePoint, decodeDatabaseQueryPage,
|
|
4
|
+
decodeDatabaseSubscription, decodeDatabaseStatus, encodeDatabaseStatus, encodeDatabaseCommit, encodeDatabaseQuery,
|
|
5
|
+
encodeDatabaseSubscription, encodeDatabaseAuthority, parseDatabaseJson, requireDatabaseU64, requireDatabaseUuid,
|
|
6
|
+
} from "../database-codec.js";
|
|
7
|
+
/**
|
|
8
|
+
* @import {
|
|
9
|
+
* DatabaseAuthority, DatabaseCommit, DatabaseCommitReceipt, DatabasePoint,
|
|
10
|
+
* DatabaseQuery, DatabaseQueryPage, DatabaseResult, DatabaseScope,
|
|
11
|
+
* DatabaseSubscriptionRequest, DatabaseSubscriptionResponse, DatabaseTransactionIdentity,
|
|
12
|
+
* DatabaseTransactionStatus,
|
|
13
|
+
* } from "../database-types.js"
|
|
14
|
+
* @import { FetchLike } from "../config.js"
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const maximumResponseBytes = 4 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internal options for the deprecated local emulator. Remote origins are refused.
|
|
21
|
+
* @typedef {DatabaseScope & {
|
|
22
|
+
* readonly apiKey: string | (() => string | Promise<string>);
|
|
23
|
+
* readonly endpoint?: string;
|
|
24
|
+
* readonly discoveryTimeoutMs?: number;
|
|
25
|
+
* readonly requestTimeoutMs?: number;
|
|
26
|
+
* readonly fetch?: FetchLike;
|
|
27
|
+
* }} TerraBaseClientConfig
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @typedef {{ readonly signal?: AbortSignal }} DatabaseRequestOptions */
|
|
31
|
+
|
|
32
|
+
// Type-level brand only; the symbol is never attached to a value.
|
|
33
|
+
const preparedBrand = Symbol("preparedBrand");
|
|
34
|
+
/** @typedef {DatabaseTransactionIdentity & { readonly [preparedBrand]: true }} PreparedDatabaseCommit */
|
|
35
|
+
/** @type {WeakMap<PreparedDatabaseCommit, { readonly client: TerraBaseClient; readonly scope: string; readonly endpoint: string | undefined; readonly body: string }>} */
|
|
36
|
+
const preparedRequests = new WeakMap();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {number | undefined} value
|
|
40
|
+
* @param {number} defaultValue
|
|
41
|
+
* @param {number} maximum
|
|
42
|
+
* @returns {number}
|
|
43
|
+
*/
|
|
44
|
+
function timeout(value, defaultValue, maximum) {
|
|
45
|
+
const resolved = value ?? defaultValue;
|
|
46
|
+
if (!Number.isInteger(resolved) || resolved < 1 || resolved > maximum) throw new TypeError("Timeout is outside its bounds.");
|
|
47
|
+
return resolved;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {DatabaseAuthority} authority
|
|
52
|
+
* @returns {DatabaseAuthority}
|
|
53
|
+
*/
|
|
54
|
+
function identityAuthority(authority) {
|
|
55
|
+
return authority.mode === "leaderless" ? Object.freeze({ ...authority })
|
|
56
|
+
: Object.freeze({ ...authority, writer: Object.freeze({ ...authority.writer }) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {DatabaseScope} scope
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function scopePath(scope) {
|
|
64
|
+
return `/v1/tenants/${requireDatabaseUuid(scope.tenant, "tenant")}/databases/${requireDatabaseUuid(scope.database, "database")}/shards/${requireDatabaseU64(scope.shard, "shard")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {unknown} raw
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function routeError(raw) {
|
|
72
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw) || Object.keys(raw).some(key => key !== "error")) return "invalid_response";
|
|
73
|
+
const error = /** @type {Record<string, unknown>} */ (raw).error;
|
|
74
|
+
if (typeof error !== "object" || error === null || Array.isArray(error)) return "invalid_response";
|
|
75
|
+
const value = /** @type {Record<string, unknown>} */ (error);
|
|
76
|
+
if (Object.keys(value).some(key => key !== "code")) return "invalid_response";
|
|
77
|
+
return typeof value.code === "string" && /^[a-z][a-z0-9_]{0,63}$/u.test(value.code) ? value.code : "invalid_response";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {Response} response
|
|
82
|
+
* @param {AbortSignal} signal
|
|
83
|
+
* @returns {Promise<unknown>}
|
|
84
|
+
*/
|
|
85
|
+
async function responseJson(response, signal) {
|
|
86
|
+
const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
87
|
+
if (mediaType !== "application/json") {
|
|
88
|
+
void response.body?.cancel().catch(() => undefined);
|
|
89
|
+
throw new TypeError("Expected application/json.");
|
|
90
|
+
}
|
|
91
|
+
const reader = response.body?.getReader();
|
|
92
|
+
if (reader === undefined) throw new TypeError("Response has no JSON body.");
|
|
93
|
+
/** @type {Uint8Array[]} */
|
|
94
|
+
const parts = [];
|
|
95
|
+
let size = 0;
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
/** @type {ReadableStreamReadResult<Uint8Array>} */
|
|
99
|
+
let part;
|
|
100
|
+
try { part = await withCancellation(reader.read(), signal); }
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (signal.aborted) throw error;
|
|
103
|
+
throw new Error("Native response transport failed.", { cause: error });
|
|
104
|
+
}
|
|
105
|
+
if (part.done) break;
|
|
106
|
+
size += part.value.length;
|
|
107
|
+
if (size > maximumResponseBytes) throw new TypeError("Response exceeds its byte limit.");
|
|
108
|
+
parts.push(part.value);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
void reader.cancel().catch(() => undefined);
|
|
112
|
+
throw error;
|
|
113
|
+
} finally { reader.releaseLock(); }
|
|
114
|
+
const bytes = new Uint8Array(size);
|
|
115
|
+
let offset = 0;
|
|
116
|
+
for (const part of parts) { bytes.set(part, offset); offset += part.length; }
|
|
117
|
+
return parseDatabaseJson(bytes);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @template T
|
|
122
|
+
* @param {Promise<T>} promise
|
|
123
|
+
* @param {AbortSignal} signal
|
|
124
|
+
* @param {(value: T) => void} [disposeLateValue]
|
|
125
|
+
* @returns {Promise<T>}
|
|
126
|
+
*/
|
|
127
|
+
function withCancellation(promise, signal, disposeLateValue) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
/** @returns {void} */
|
|
130
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
131
|
+
/** @returns {void} */
|
|
132
|
+
const abort = () => {
|
|
133
|
+
cleanup();
|
|
134
|
+
reject(signal.reason);
|
|
135
|
+
};
|
|
136
|
+
if (signal.aborted) abort();
|
|
137
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
138
|
+
// Observe the started operation even if it aborted synchronously before
|
|
139
|
+
// this wrapper ran; a late fetch result still owns a response body.
|
|
140
|
+
void promise.then(
|
|
141
|
+
value => {
|
|
142
|
+
cleanup();
|
|
143
|
+
if (signal.aborted) {
|
|
144
|
+
try {
|
|
145
|
+
disposeLateValue?.(value);
|
|
146
|
+
} catch {
|
|
147
|
+
// Cleanup cannot replace the terminal cancellation.
|
|
148
|
+
}
|
|
149
|
+
reject(signal.reason);
|
|
150
|
+
} else resolve(value);
|
|
151
|
+
},
|
|
152
|
+
error => {
|
|
153
|
+
cleanup();
|
|
154
|
+
reject(signal.aborted ? signal.reason : error);
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Current scoped document client. All mutations are server commits in one declared shard. */
|
|
161
|
+
export class TerraBaseClient {
|
|
162
|
+
/**
|
|
163
|
+
* @readonly
|
|
164
|
+
* @type {DatabaseScope}
|
|
165
|
+
*/
|
|
166
|
+
#scope;
|
|
167
|
+
/**
|
|
168
|
+
* @readonly
|
|
169
|
+
* @type {TerraBaseClientConfig}
|
|
170
|
+
*/
|
|
171
|
+
#config;
|
|
172
|
+
/**
|
|
173
|
+
* @readonly
|
|
174
|
+
* @type {FetchLike}
|
|
175
|
+
*/
|
|
176
|
+
#fetch;
|
|
177
|
+
/**
|
|
178
|
+
* @readonly
|
|
179
|
+
* @type {string}
|
|
180
|
+
*/
|
|
181
|
+
#path;
|
|
182
|
+
/**
|
|
183
|
+
* @readonly
|
|
184
|
+
* @type {string | undefined}
|
|
185
|
+
*/
|
|
186
|
+
#endpoint;
|
|
187
|
+
/**
|
|
188
|
+
* @readonly
|
|
189
|
+
* @type {number}
|
|
190
|
+
*/
|
|
191
|
+
#requestTimeoutMs;
|
|
192
|
+
|
|
193
|
+
/** @returns {DatabaseScope} */
|
|
194
|
+
get scope() { return this.#scope; }
|
|
195
|
+
|
|
196
|
+
/** @param {TerraBaseClientConfig} config */
|
|
197
|
+
constructor(config) {
|
|
198
|
+
this.#scope = Object.freeze({ tenant: requireDatabaseUuid(config.tenant, "tenant"), database: requireDatabaseUuid(config.database, "database"), shard: requireDatabaseU64(config.shard, "shard") });
|
|
199
|
+
this.#config = { ...config };
|
|
200
|
+
this.#fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
201
|
+
this.#path = scopePath(this.#scope);
|
|
202
|
+
const endpoint = new URL(config.endpoint ?? "http://127.0.0.1");
|
|
203
|
+
if (endpoint.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
|
|
204
|
+
throw new TypeError("The retired SQLite client only supports a local emulator origin.");
|
|
205
|
+
this.#endpoint = endpoint.origin;
|
|
206
|
+
this.#requestTimeoutMs = timeout(config.requestTimeoutMs, 30_000, 300_000);
|
|
207
|
+
if (typeof config.apiKey !== "function") this.#validateKey(config.apiKey);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {string} collection
|
|
212
|
+
* @param {string} document
|
|
213
|
+
* @param {DatabaseRequestOptions} [options]
|
|
214
|
+
* @returns {Promise<DatabaseResult<DatabasePoint>>}
|
|
215
|
+
*/
|
|
216
|
+
read(collection, document, options = {}) {
|
|
217
|
+
const path = `${this.#collectionPath(collection)}/documents/${requireDatabaseUuid(document, "document")}`;
|
|
218
|
+
return this.#request(path, "GET", undefined, raw => this.#scopedPoint(decodeDatabasePoint(raw)), options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string} collection
|
|
223
|
+
* @param {DatabaseQuery} query
|
|
224
|
+
* @param {DatabaseRequestOptions} [options]
|
|
225
|
+
* @returns {Promise<DatabaseResult<DatabaseQueryPage>>}
|
|
226
|
+
*/
|
|
227
|
+
query(collection, query, options = {}) {
|
|
228
|
+
const limit = query.limit;
|
|
229
|
+
const body = encodeDatabaseQuery(query);
|
|
230
|
+
return this.#request(`${this.#collectionPath(collection)}/documents/query`, "POST", body, raw => {
|
|
231
|
+
const page = decodeDatabaseQueryPage(raw);
|
|
232
|
+
if (page.rows.length > limit) throw new TypeError("Query page exceeds the requested limit.");
|
|
233
|
+
page.rows.forEach(row => this.#scopedPoint(row.point));
|
|
234
|
+
return page;
|
|
235
|
+
}, options);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {string} collection
|
|
240
|
+
* @param {DatabaseSubscriptionRequest} request
|
|
241
|
+
* @param {DatabaseRequestOptions} [options]
|
|
242
|
+
* @returns {Promise<DatabaseResult<DatabaseSubscriptionResponse>>}
|
|
243
|
+
*/
|
|
244
|
+
subscription(collection, request, options = {}) {
|
|
245
|
+
const phase = request.phase;
|
|
246
|
+
const limit = request.phase === "snapshot" ? request.limit : undefined;
|
|
247
|
+
const body = encodeDatabaseSubscription(request);
|
|
248
|
+
return this.#request(`${this.#collectionPath(collection)}/documents/subscriptions`, "POST", body, raw => {
|
|
249
|
+
const result = decodeDatabaseSubscription(raw);
|
|
250
|
+
if (result.phase !== phase) throw new TypeError("Subscription response belongs to another phase.");
|
|
251
|
+
if (result.phase === "snapshot" && limit !== undefined && result.rows.length > limit) throw new TypeError("Snapshot page exceeds the requested limit.");
|
|
252
|
+
if (result.phase === "snapshot") result.rows.forEach(row => this.#scopedPoint(row.point));
|
|
253
|
+
else result.groups.forEach(group => group.effects.forEach(effect => { if (effect.point !== null) this.#scopedPoint(effect.point); }));
|
|
254
|
+
return result;
|
|
255
|
+
}, options);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Prepare once and retain this identity for exact retry and durable status resolution.
|
|
260
|
+
* @param {DatabaseCommit} commit
|
|
261
|
+
* @returns {Promise<PreparedDatabaseCommit>}
|
|
262
|
+
*/
|
|
263
|
+
async prepareCommit(commit) {
|
|
264
|
+
const transaction_id = Object.freeze({ ...commit.transaction_id });
|
|
265
|
+
const authority = identityAuthority(commit.authority);
|
|
266
|
+
const encoded = await encodeDatabaseCommit(this.#scope, commit);
|
|
267
|
+
const prepared = /** @type {PreparedDatabaseCommit} */ (Object.freeze({ transaction_id, authority, effect_digest: encoded.effect_digest }));
|
|
268
|
+
preparedRequests.set(prepared, { client: this, scope: this.#path, endpoint: this.#endpoint, body: encoded.body });
|
|
269
|
+
return prepared;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {PreparedDatabaseCommit} prepared
|
|
274
|
+
* @param {DatabaseRequestOptions} [options]
|
|
275
|
+
* @returns {Promise<DatabaseResult<DatabaseCommitReceipt>>}
|
|
276
|
+
*/
|
|
277
|
+
commit(prepared, options = {}) {
|
|
278
|
+
const request = preparedRequests.get(prepared);
|
|
279
|
+
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.");
|
|
280
|
+
if (request.endpoint !== this.#endpoint) throw new TypeError("Prepared commit belongs to another endpoint.");
|
|
281
|
+
return this.#request(`${this.#path}/transactions`, "POST", request.body, raw => {
|
|
282
|
+
const receipt = decodeDatabaseCommitReceipt(raw);
|
|
283
|
+
if (receipt.event.database !== this.#scope.database || receipt.event.shard !== this.#scope.shard ||
|
|
284
|
+
receipt.effect_digest !== prepared.effect_digest || JSON.stringify(encodeDatabaseAuthority(receipt.authority)) !== JSON.stringify(encodeDatabaseAuthority(prepared.authority)) ||
|
|
285
|
+
receipt.transaction_id.nonce !== prepared.transaction_id.nonce || receipt.transaction_id.expires_at_unix_ms !== prepared.transaction_id.expires_at_unix_ms) {
|
|
286
|
+
throw new TypeError("Receipt belongs to another operation or database scope.");
|
|
287
|
+
}
|
|
288
|
+
return receipt;
|
|
289
|
+
}, options, prepared);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Unknown, pending and expired outcomes remain unresolved; an absent document proves no abort.
|
|
294
|
+
* @param {DatabaseTransactionIdentity} identity
|
|
295
|
+
* @param {DatabaseRequestOptions} [options]
|
|
296
|
+
* @returns {Promise<DatabaseResult<DatabaseTransactionStatus>>}
|
|
297
|
+
*/
|
|
298
|
+
transactionStatus(identity, options = {}) {
|
|
299
|
+
const snapshot = Object.freeze({ ...identity, transaction_id: Object.freeze({ ...identity.transaction_id }), authority: identityAuthority(identity.authority) });
|
|
300
|
+
const body = encodeDatabaseStatus(snapshot);
|
|
301
|
+
return this.#request(`${this.#path}/transactions/status`, "POST", body, raw => {
|
|
302
|
+
const status = decodeDatabaseStatus(raw);
|
|
303
|
+
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))) {
|
|
304
|
+
throw new TypeError("Status belongs to another operation.");
|
|
305
|
+
}
|
|
306
|
+
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.");
|
|
307
|
+
return status;
|
|
308
|
+
}, options);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param {string} collection
|
|
313
|
+
* @returns {string}
|
|
314
|
+
*/
|
|
315
|
+
#collectionPath(collection) { return `${this.#path}/collections/${requireDatabaseUuid(collection, "collection")}`; }
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {DatabasePoint} point
|
|
319
|
+
* @returns {DatabasePoint}
|
|
320
|
+
*/
|
|
321
|
+
#scopedPoint(point) {
|
|
322
|
+
if (point.siblings.some(sibling => sibling.event.database !== this.#scope.database || sibling.event.shard !== this.#scope.shard)) {
|
|
323
|
+
throw new TypeError("Point belongs to another database scope.");
|
|
324
|
+
}
|
|
325
|
+
return point;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @param {unknown} key
|
|
330
|
+
* @returns {string}
|
|
331
|
+
*/
|
|
332
|
+
#validateKey(key) {
|
|
333
|
+
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.");
|
|
334
|
+
return key;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @template T
|
|
339
|
+
* @param {string} path
|
|
340
|
+
* @param {"GET" | "POST"} method
|
|
341
|
+
* @param {string | undefined} body
|
|
342
|
+
* @param {(raw: unknown) => T} decode
|
|
343
|
+
* @param {DatabaseRequestOptions} options
|
|
344
|
+
* @param {DatabaseTransactionIdentity} [transaction]
|
|
345
|
+
* @returns {Promise<DatabaseResult<T>>}
|
|
346
|
+
*/
|
|
347
|
+
async #request(path, method, body, decode, options, transaction) {
|
|
348
|
+
let sent = false;
|
|
349
|
+
/** @type {number | undefined} */
|
|
350
|
+
let responseStatus;
|
|
351
|
+
const signal = AbortSignal.any([AbortSignal.timeout(this.#requestTimeoutMs), ...(options.signal === undefined ? [] : [options.signal])]);
|
|
352
|
+
try {
|
|
353
|
+
signal.throwIfAborted();
|
|
354
|
+
const apiKey = this.#validateKey(typeof this.#config.apiKey === "function" ? await withCancellation(Promise.resolve(this.#config.apiKey()), signal) : this.#config.apiKey);
|
|
355
|
+
const origin = /** @type {string} */ (this.#endpoint);
|
|
356
|
+
signal.throwIfAborted();
|
|
357
|
+
const headers = new Headers({ accept: "application/json", authorization: `Bearer ${apiKey}` });
|
|
358
|
+
if (body !== undefined) headers.set("content-type", "application/json");
|
|
359
|
+
const request = new Request(new URL(path, origin), { method, headers, credentials: "omit", redirect: "manual", cache: "no-store", referrerPolicy: "no-referrer", signal, ...(body === undefined ? {} : { body }) });
|
|
360
|
+
sent = true;
|
|
361
|
+
/** @type {Response} */
|
|
362
|
+
let response;
|
|
363
|
+
try {
|
|
364
|
+
response = await withCancellation(this.#fetch(request), signal, late => {
|
|
365
|
+
void late.body?.cancel().catch(() => undefined);
|
|
366
|
+
});
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (signal.aborted) throw error;
|
|
369
|
+
throw new Error("Native request transport failed.", { cause: error });
|
|
370
|
+
}
|
|
371
|
+
responseStatus = response.status;
|
|
372
|
+
if (signal.aborted) {
|
|
373
|
+
void response.body?.cancel().catch(() => undefined);
|
|
374
|
+
signal.throwIfAborted();
|
|
375
|
+
}
|
|
376
|
+
if (response.redirected) {
|
|
377
|
+
void response.body?.cancel().catch(() => undefined);
|
|
378
|
+
throw new TypeError("Native redirects are not admitted.");
|
|
379
|
+
}
|
|
380
|
+
if (response.ok && response.status !== 200) {
|
|
381
|
+
void response.body?.cancel().catch(() => undefined);
|
|
382
|
+
throw new TypeError("The native protocol admits only HTTP 200 success.");
|
|
383
|
+
}
|
|
384
|
+
const raw = await responseJson(response, signal);
|
|
385
|
+
signal.throwIfAborted();
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
const code = routeError(raw);
|
|
388
|
+
/** @type {"uncertain" | "rejected" | "unknown"} */
|
|
389
|
+
let mutationOutcome = "unknown";
|
|
390
|
+
if (code === "commit_uncertain") mutationOutcome = "uncertain";
|
|
391
|
+
else if (code === "precondition_failed" && response.status === 412) mutationOutcome = "rejected";
|
|
392
|
+
// Authentication, cancellation and changed-effect errors do not resolve an earlier attempt.
|
|
393
|
+
// Only the producer's retained precondition rejection is a terminal mutation outcome.
|
|
394
|
+
return { ok: false, error: { code, status: response.status, ...(transaction === undefined ? {} : { transaction, mutation_outcome: mutationOutcome }) } };
|
|
395
|
+
}
|
|
396
|
+
return { ok: true, status: response.status, value: decode(raw) };
|
|
397
|
+
} catch (error) {
|
|
398
|
+
let code = "transport_unavailable";
|
|
399
|
+
if (signal.aborted) code = "cancelled";
|
|
400
|
+
else if (error instanceof TypeError || error instanceof SyntaxError) code = "invalid_response";
|
|
401
|
+
const status = responseStatus;
|
|
402
|
+
return { ok: false, error: { code, ...(status === undefined ? {} : { status }), ...(transaction === undefined ? {} : { transaction, mutation_outcome: sent ? "uncertain" : "unknown" }) } };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @param {TerraBaseClientConfig} config
|
|
409
|
+
* @returns {TerraBaseClient}
|
|
410
|
+
*/
|
|
411
|
+
export function createTerraBaseClient(config) { return new TerraBaseClient(config); }
|
package/src/postgres.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** @deprecated The C# TerraBase has no PostgreSQL listener.
|
|
2
|
+
* Node-only PostgreSQL client for the signed INT8/TEXT point SQL listener. */
|
|
2
3
|
import { lookup } from "node:dns/promises";
|
|
3
4
|
import { isIP } from "node:net";
|
|
4
5
|
import { X509Certificate } from "node:crypto";
|
|
@@ -229,6 +230,7 @@ function columns(fields) {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
/** One driver connection; no automatic reconnect, mutation retry or callback replay. */
|
|
233
|
+
/** @deprecated The C# TerraBase has no PostgreSQL listener. */
|
|
232
234
|
export class PostgresClient {
|
|
233
235
|
/** @type {DriverClient} */
|
|
234
236
|
#driver;
|
package/src/tanstack/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createDatabaseQuery, DatabaseSubscriptionError } from "../database-view.js";
|
|
2
2
|
/** @import { DatabaseQueryOptions, DatabaseViewPosition, DatabaseViewSnapshot } from "../database-view.js" */
|
|
3
|
-
/** @import { PreparedDatabaseCommit, TerraBaseClient } from "../
|
|
3
|
+
/** @import { PreparedDatabaseCommit, TerraBaseClient } from "../local/legacy-client.js" */
|
|
4
4
|
/** @import { DatabaseCommitReceipt, DatabaseQueryRow, DatabaseResult, DatabaseScope } from "../database-types.js" */
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -498,6 +498,7 @@ export class TerraScaleTanStackCollectionSync {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
/**
|
|
501
|
+
* @deprecated Retired SQLite/Rust adapter; no C# shape adapter is provided.
|
|
501
502
|
* Options for `createCollection(terrascaleCollectionOptions(...))`.
|
|
502
503
|
*
|
|
503
504
|
* @template {object} T
|
package/src/v1/decode.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Typed decoding of TerraBase v1 response bodies.
|
|
2
|
+
import { parseU64, requireVersion } from "./ids.js";
|
|
3
|
+
import { isJsonObject } from "./json.js";
|
|
4
|
+
/** @import { JsonObject, JsonValue } from "./json.js" */
|
|
5
|
+
|
|
6
|
+
/** @typedef {"local_durable"} V1Durability */
|
|
7
|
+
/**
|
|
8
|
+
* C# single-writer documents always carry a value, never conflict siblings.
|
|
9
|
+
* @typedef {{ readonly id: string; readonly version: string; readonly state: "value"; readonly document: JsonObject }} V1Document
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The result of a document write.
|
|
14
|
+
* @typedef {object} V1WriteResult
|
|
15
|
+
* @property {string} id
|
|
16
|
+
* @property {string} version For a delete, the tombstone's version.
|
|
17
|
+
* @property {string} offset The database commit offset (decimal u64), usable with shape `waitFor`.
|
|
18
|
+
* @property {V1Durability} durability
|
|
19
|
+
* @property {boolean} created True when a PUT or POST answered 201.
|
|
20
|
+
* @property {string | undefined} location The Location header of a POST create.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} what
|
|
25
|
+
* @returns {never}
|
|
26
|
+
*/
|
|
27
|
+
export function malformed(what) {
|
|
28
|
+
throw new TypeError(`TerraBase returned a malformed ${what}.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {JsonValue | undefined} value
|
|
33
|
+
* @param {string} what
|
|
34
|
+
* @returns {JsonObject}
|
|
35
|
+
*/
|
|
36
|
+
export function object(value, what) {
|
|
37
|
+
return isJsonObject(value) ? value : malformed(what);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {JsonValue | undefined} value
|
|
42
|
+
* @param {string} what
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function string(value, what) {
|
|
46
|
+
return typeof value === "string" ? value : malformed(what);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {JsonValue | undefined} value
|
|
51
|
+
* @param {string} what
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function u64(value, what) {
|
|
55
|
+
return parseU64(value) === undefined ? malformed(what) : /** @type {string} */ (value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {JsonValue | undefined} value
|
|
60
|
+
* @param {string} what
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function boolean(value, what) {
|
|
64
|
+
return typeof value === "boolean" ? value : malformed(what);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {JsonValue | undefined} value
|
|
69
|
+
* @param {string} what
|
|
70
|
+
* @returns {JsonValue[]}
|
|
71
|
+
*/
|
|
72
|
+
export function array(value, what) {
|
|
73
|
+
return Array.isArray(value) ? value : malformed(what);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {JsonValue | undefined} value
|
|
78
|
+
* @returns {V1Durability}
|
|
79
|
+
*/
|
|
80
|
+
export function durability(value) {
|
|
81
|
+
return value === "local_durable" ? value : malformed("durability");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {JsonValue | undefined} value
|
|
86
|
+
* @returns {V1Document}
|
|
87
|
+
*/
|
|
88
|
+
export function decodeDocument(value) {
|
|
89
|
+
const raw = object(value, "document");
|
|
90
|
+
const id = string(raw.id, "document id");
|
|
91
|
+
const version = requireVersion(raw.version);
|
|
92
|
+
if (raw.state !== "value") return malformed("document state");
|
|
93
|
+
return Object.freeze({ id, version, state: "value", document: object(raw.document, "document body") });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {{ status: number; headers: Headers; value: JsonValue | undefined }} response
|
|
98
|
+
* @returns {V1WriteResult}
|
|
99
|
+
*/
|
|
100
|
+
export function decodeWrite(response) {
|
|
101
|
+
const raw = object(response.value, "write result");
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
id: string(raw.id, "write id"),
|
|
104
|
+
version: requireVersion(raw.version),
|
|
105
|
+
offset: u64(raw.offset, "write offset"),
|
|
106
|
+
durability: durability(raw.durability),
|
|
107
|
+
created: response.status === 201,
|
|
108
|
+
location: response.headers.get("location") ?? undefined,
|
|
109
|
+
});
|
|
110
|
+
}
|