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
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {
|
|
3
|
+
* DatabaseAuthority, DatabaseCommit, DatabaseCommitReceipt, DatabaseCondition, DatabaseEvent,
|
|
4
|
+
* DatabaseField, DatabaseObserved, DatabasePoint, DatabaseQuery, DatabaseQueryPage,
|
|
5
|
+
* DatabaseQueryPredicate, DatabaseScope, DatabaseSibling, DatabaseSubscriptionRequest,
|
|
6
|
+
* DatabaseSubscriptionResponse, DatabaseTransactionId, DatabaseValue, DatabaseWrite,
|
|
7
|
+
* DatabaseTransactionIdentity, DatabaseTransactionStatus,
|
|
8
|
+
* } from "./database-types.js"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @typedef {null | boolean | number | string | JsonArray | JsonObject} Json */
|
|
12
|
+
/** @typedef {readonly Json[]} JsonArray */
|
|
13
|
+
/** @typedef {{ readonly [key: string]: Json }} JsonObject */
|
|
14
|
+
/** @typedef {Record<string, unknown>} RecordValue */
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const maxU64 = (1n << 64n) - 1n;
|
|
17
|
+
const minI64 = -(1n << 63n);
|
|
18
|
+
const maxI64 = (1n << 63n) - 1n;
|
|
19
|
+
const maximumBodyBytes = 4 * 1024 * 1024;
|
|
20
|
+
const maximumValueBytes = 1024 * 1024;
|
|
21
|
+
const scalarTypes = new Set(["bool", "i64", "utf8", "bytes"]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {unknown} value
|
|
25
|
+
* @param {string} [name]
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function requireDatabaseUuid(value, name = "UUID") {
|
|
29
|
+
if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(value)) {
|
|
30
|
+
throw new TypeError(`${name} must be a canonical lowercase UUID.`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} value
|
|
37
|
+
* @param {string} [name]
|
|
38
|
+
* @param {boolean} [positive]
|
|
39
|
+
* @returns {bigint}
|
|
40
|
+
*/
|
|
41
|
+
export function requireDatabaseU64(value, name = "u64", positive = false) {
|
|
42
|
+
if (typeof value !== "bigint" || value < (positive ? 1n : 0n) || value > maxU64) {
|
|
43
|
+
throw new TypeError(`${name} must be a ${positive ? "positive " : ""}u64 bigint.`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {unknown} value
|
|
50
|
+
* @param {string} name
|
|
51
|
+
* @param {boolean} [positive]
|
|
52
|
+
* @returns {bigint}
|
|
53
|
+
*/
|
|
54
|
+
function decimal(value, name, positive = false) {
|
|
55
|
+
if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/u.test(value)) throw new TypeError(`${name} must be canonical decimal.`);
|
|
56
|
+
return requireDatabaseU64(BigInt(value), name, positive);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {unknown} value
|
|
61
|
+
* @param {number} bytes
|
|
62
|
+
* @param {string} name
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function hex(value, bytes, name) {
|
|
66
|
+
if (typeof value !== "string" || value.length !== bytes * 2 || !/^[0-9a-f]+$/u.test(value)) {
|
|
67
|
+
throw new TypeError(`${name} must be canonical lowercase hexadecimal.`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {unknown} value
|
|
74
|
+
* @param {readonly string[]} required
|
|
75
|
+
* @param {readonly string[]} [optional]
|
|
76
|
+
* @returns {RecordValue}
|
|
77
|
+
*/
|
|
78
|
+
function object(value, required, optional = []) {
|
|
79
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("Expected a protocol object.");
|
|
80
|
+
const entry = /** @type {RecordValue} */ (value);
|
|
81
|
+
if (required.some(key => !Object.hasOwn(entry, key)) || Object.keys(entry).some(key => !required.includes(key) && !optional.includes(key))) {
|
|
82
|
+
throw new TypeError("Protocol object has missing or unknown fields.");
|
|
83
|
+
}
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {unknown} value
|
|
89
|
+
* @param {number} [maximum]
|
|
90
|
+
* @param {number} [minimum]
|
|
91
|
+
* @returns {readonly unknown[]}
|
|
92
|
+
*/
|
|
93
|
+
function array(value, maximum = 1024, minimum = 0) {
|
|
94
|
+
if (!Array.isArray(value) || value.length < minimum || value.length > maximum) throw new TypeError("Protocol array exceeds its bounds.");
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {unknown} value
|
|
100
|
+
* @returns {number}
|
|
101
|
+
*/
|
|
102
|
+
function fieldId(value) {
|
|
103
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 0xffff_ffff) {
|
|
104
|
+
throw new TypeError("Field ID must be a positive u32.");
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {unknown} value
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
function text(value) {
|
|
114
|
+
if (typeof value !== "string") throw new TypeError("UTF-8 value must be text.");
|
|
115
|
+
for (let index = 0; index < value.length; index++) {
|
|
116
|
+
const code = value.charCodeAt(index);
|
|
117
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
118
|
+
const next = value.charCodeAt(++index);
|
|
119
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) throw new TypeError("Text contains an unpaired surrogate.");
|
|
120
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) throw new TypeError("Text contains an unpaired surrogate.");
|
|
121
|
+
}
|
|
122
|
+
if (encoder.encode(value).length > maximumValueBytes) throw new TypeError("Value exceeds its byte limit.");
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {unknown} value
|
|
128
|
+
* @returns {bigint}
|
|
129
|
+
*/
|
|
130
|
+
function i64(value) {
|
|
131
|
+
if (typeof value !== "bigint" || value < minI64 || value > maxI64) throw new TypeError("i64 value must be an in-range bigint.");
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {unknown} value
|
|
137
|
+
* @returns {bigint}
|
|
138
|
+
*/
|
|
139
|
+
function parseI64(value) {
|
|
140
|
+
if (typeof value !== "string" || !/^(0|-?[1-9][0-9]*)$/u.test(value)) throw new TypeError("i64 must be canonical decimal.");
|
|
141
|
+
return i64(BigInt(value));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {Uint8Array} bytes
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function base64url(bytes) {
|
|
149
|
+
let binary = "";
|
|
150
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
151
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {unknown} value
|
|
156
|
+
* @returns {Uint8Array}
|
|
157
|
+
*/
|
|
158
|
+
function parseBytes(value) {
|
|
159
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]*$/u.test(value) || value.length > 1_398_102) throw new TypeError("Bytes must be bounded canonical base64url.");
|
|
160
|
+
/** @type {string} */
|
|
161
|
+
let binary;
|
|
162
|
+
try { binary = atob(value.replaceAll("-", "+").replaceAll("_", "/")); }
|
|
163
|
+
catch { throw new TypeError("Bytes must be canonical base64url."); }
|
|
164
|
+
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
165
|
+
if (bytes.length > maximumValueBytes || base64url(bytes) !== value) throw new TypeError("Bytes must be bounded canonical base64url.");
|
|
166
|
+
return bytes;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @typedef {{ nodes: number }} Budget */
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {number} depth
|
|
173
|
+
* @param {Budget} budget
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
176
|
+
function takeNode(depth, budget) {
|
|
177
|
+
if (depth > 16 || ++budget.nodes > 4096) throw new TypeError("Document exceeds its depth or node limit.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {readonly DatabaseField[]} fields
|
|
182
|
+
* @param {number} depth
|
|
183
|
+
* @param {Budget} budget
|
|
184
|
+
* @returns {readonly Json[]}
|
|
185
|
+
*/
|
|
186
|
+
function fieldsToWire(fields, depth, budget) {
|
|
187
|
+
const entries = array(fields);
|
|
188
|
+
let previous = 0;
|
|
189
|
+
return entries.map(raw => {
|
|
190
|
+
const field = object(raw, ["id", "value"]);
|
|
191
|
+
const id = fieldId(field.id);
|
|
192
|
+
if (id <= previous) throw new TypeError("Field IDs must be strictly increasing.");
|
|
193
|
+
previous = id;
|
|
194
|
+
return { id, value: valueToWire(/** @type {DatabaseValue} */ (field.value), depth, budget) };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {DatabaseValue} value
|
|
200
|
+
* @param {number} depth
|
|
201
|
+
* @param {Budget} budget
|
|
202
|
+
* @returns {JsonObject}
|
|
203
|
+
*/
|
|
204
|
+
function valueToWire(value, depth, budget) {
|
|
205
|
+
takeNode(depth, budget);
|
|
206
|
+
const initial = object(value, ["type"], ["value", "fields", "items"]);
|
|
207
|
+
switch (initial.type) {
|
|
208
|
+
case "null": object(value, ["type"]); return { type: "null" };
|
|
209
|
+
case "bool": {
|
|
210
|
+
const entry = object(value, ["type", "value"]);
|
|
211
|
+
if (typeof entry.value !== "boolean") throw new TypeError("Boolean value must be boolean.");
|
|
212
|
+
return { type: "bool", value: entry.value };
|
|
213
|
+
}
|
|
214
|
+
case "i64": return { type: "i64", value: i64(object(value, ["type", "value"]).value).toString() };
|
|
215
|
+
case "utf8": return { type: "utf8", value: text(object(value, ["type", "value"]).value) };
|
|
216
|
+
case "bytes": {
|
|
217
|
+
const bytes = object(value, ["type", "value"]).value;
|
|
218
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > maximumValueBytes) throw new TypeError("Bytes value must be a bounded Uint8Array.");
|
|
219
|
+
return { type: "bytes", value: base64url(bytes) };
|
|
220
|
+
}
|
|
221
|
+
case "map": return { type: "map", fields: fieldsToWire(/** @type {readonly DatabaseField[]} */ (object(value, ["type", "fields"]).fields), depth + 1, budget) };
|
|
222
|
+
case "array": return { type: "array", items: array(object(value, ["type", "items"]).items).map(item => valueToWire(/** @type {DatabaseValue} */ (item), depth + 1, budget)) };
|
|
223
|
+
default: throw new TypeError("Unsupported database value type.");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {unknown} raw
|
|
229
|
+
* @param {number} depth
|
|
230
|
+
* @param {Budget} budget
|
|
231
|
+
* @returns {readonly DatabaseField[]}
|
|
232
|
+
*/
|
|
233
|
+
function fieldsFromWire(raw, depth, budget) {
|
|
234
|
+
let previous = 0;
|
|
235
|
+
return array(raw).map(value => {
|
|
236
|
+
const entry = object(value, ["id", "value"]);
|
|
237
|
+
const id = fieldId(entry.id);
|
|
238
|
+
if (id <= previous) throw new TypeError("Field IDs must be strictly increasing.");
|
|
239
|
+
previous = id;
|
|
240
|
+
return { id, value: valueFromWire(entry.value, depth, budget) };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {unknown} raw
|
|
246
|
+
* @param {number} depth
|
|
247
|
+
* @param {Budget} budget
|
|
248
|
+
* @returns {DatabaseValue}
|
|
249
|
+
*/
|
|
250
|
+
function valueFromWire(raw, depth, budget) {
|
|
251
|
+
takeNode(depth, budget);
|
|
252
|
+
const initial = object(raw, ["type"], ["value", "fields", "items"]);
|
|
253
|
+
switch (initial.type) {
|
|
254
|
+
case "null": object(raw, ["type"]); return { type: "null" };
|
|
255
|
+
case "bool": {
|
|
256
|
+
const entry = object(raw, ["type", "value"]);
|
|
257
|
+
if (typeof entry.value !== "boolean") throw new TypeError("Boolean value must be boolean.");
|
|
258
|
+
return { type: "bool", value: entry.value };
|
|
259
|
+
}
|
|
260
|
+
case "i64": return { type: "i64", value: parseI64(object(raw, ["type", "value"]).value) };
|
|
261
|
+
case "utf8": return { type: "utf8", value: text(object(raw, ["type", "value"]).value) };
|
|
262
|
+
case "bytes": return { type: "bytes", value: parseBytes(object(raw, ["type", "value"]).value) };
|
|
263
|
+
case "map": return { type: "map", fields: fieldsFromWire(object(raw, ["type", "fields"]).fields, depth + 1, budget) };
|
|
264
|
+
case "array": return { type: "array", items: array(object(raw, ["type", "items"]).items).map(value => valueFromWire(value, depth + 1, budget)) };
|
|
265
|
+
default: throw new TypeError("Unsupported database value type.");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Typed value encoding preserves numeric field IDs, arrays, bytes, and explicit null.
|
|
271
|
+
* @param {DatabaseValue} value
|
|
272
|
+
* @returns {JsonObject}
|
|
273
|
+
*/
|
|
274
|
+
export function encodeDatabaseValue(value) {
|
|
275
|
+
const wire = valueToWire(value, 1, { nodes: 0 });
|
|
276
|
+
nativeValue(wire);
|
|
277
|
+
return wire;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {unknown} value
|
|
282
|
+
* @returns {DatabaseValue}
|
|
283
|
+
*/
|
|
284
|
+
export function decodeDatabaseValue(value) {
|
|
285
|
+
const decoded = valueFromWire(value, 1, { nodes: 0 });
|
|
286
|
+
nativeValue(valueToWire(decoded, 1, { nodes: 0 }));
|
|
287
|
+
return decoded;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* @param {readonly DatabaseObserved[]} entries
|
|
292
|
+
* @returns {readonly Json[]}
|
|
293
|
+
*/
|
|
294
|
+
function observedToWire(entries) {
|
|
295
|
+
let previous = "";
|
|
296
|
+
return array(entries).map(raw => {
|
|
297
|
+
const entry = object(raw, ["replica", "incarnation", "sequence"]);
|
|
298
|
+
const replica = requireDatabaseUuid(entry.replica, "replica");
|
|
299
|
+
const incarnation = hex(entry.incarnation, 16, "incarnation");
|
|
300
|
+
const order = replica + incarnation;
|
|
301
|
+
if (order <= previous) throw new TypeError("Observed origins must be strictly increasing.");
|
|
302
|
+
previous = order;
|
|
303
|
+
return { replica, incarnation, sequence: requireDatabaseU64(entry.sequence, "sequence", true).toString() };
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @param {unknown} raw
|
|
309
|
+
* @returns {readonly DatabaseObserved[]}
|
|
310
|
+
*/
|
|
311
|
+
function observedFromWire(raw) {
|
|
312
|
+
const entries = array(raw).map(value => {
|
|
313
|
+
const entry = object(value, ["replica", "incarnation", "sequence"]);
|
|
314
|
+
return { replica: requireDatabaseUuid(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"), sequence: decimal(entry.sequence, "sequence", true) };
|
|
315
|
+
});
|
|
316
|
+
observedToWire(entries);
|
|
317
|
+
return entries;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @param {unknown} raw
|
|
322
|
+
* @returns {DatabaseEvent}
|
|
323
|
+
*/
|
|
324
|
+
function eventFromWire(raw) {
|
|
325
|
+
const entry = object(raw, ["database", "shard", "replica", "incarnation", "sequence"]);
|
|
326
|
+
return {
|
|
327
|
+
database: requireDatabaseUuid(entry.database, "database"), shard: decimal(entry.shard, "shard"),
|
|
328
|
+
replica: requireDatabaseUuid(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"),
|
|
329
|
+
sequence: decimal(entry.sequence, "sequence", true),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @param {unknown} raw
|
|
335
|
+
* @returns {DatabasePoint}
|
|
336
|
+
*/
|
|
337
|
+
export function decodeDatabasePoint(raw) {
|
|
338
|
+
const entry = object(raw, ["version", "state", "state_version", "context", "siblings"]);
|
|
339
|
+
if (entry.version !== 1 || !["absent", "deleted", "value", "conflict"].includes(String(entry.state))) throw new TypeError("Unsupported point state or protocol.");
|
|
340
|
+
const context = observedFromWire(entry.context);
|
|
341
|
+
/** @type {DatabaseSibling[]} */
|
|
342
|
+
const siblings = array(entry.siblings).map(rawSibling => {
|
|
343
|
+
const sibling = object(rawSibling, ["event", "schema_generation", "schema_digest", "kind"], ["fields"]);
|
|
344
|
+
const base = { event: eventFromWire(sibling.event), schema_generation: decimal(sibling.schema_generation, "schema_generation", true), schema_digest: hex(sibling.schema_digest, 32, "schema_digest") };
|
|
345
|
+
if (sibling.kind === "tombstone") {
|
|
346
|
+
object(rawSibling, ["event", "schema_generation", "schema_digest", "kind"]);
|
|
347
|
+
return { ...base, kind: "tombstone" };
|
|
348
|
+
}
|
|
349
|
+
if (sibling.kind !== "live") throw new TypeError("Unsupported sibling kind.");
|
|
350
|
+
object(rawSibling, ["event", "schema_generation", "schema_digest", "kind", "fields"]);
|
|
351
|
+
const fields = fieldsFromWire(sibling.fields, 1, { nodes: 0 });
|
|
352
|
+
validateNativeDocument(fieldsToWire(fields, 1, { nodes: 0 }));
|
|
353
|
+
return { ...base, kind: "live", fields };
|
|
354
|
+
});
|
|
355
|
+
const state = /** @type {DatabasePoint["state"]} */ (entry.state);
|
|
356
|
+
const state_version = entry.state_version === null ? null : hex(entry.state_version, 32, "state_version");
|
|
357
|
+
if ((state === "absent" && (context.length !== 0 || siblings.length !== 0 || state_version !== null)) ||
|
|
358
|
+
(state === "deleted" && (siblings.length !== 1 || siblings[0]?.kind !== "tombstone" || state_version === null)) ||
|
|
359
|
+
(state === "value" && (siblings.length !== 1 || siblings[0]?.kind !== "live" || state_version === null)) ||
|
|
360
|
+
(state === "conflict" && (siblings.length < 2 || state_version !== null))) {
|
|
361
|
+
throw new TypeError("Point state contradicts its siblings or condition token.");
|
|
362
|
+
}
|
|
363
|
+
/** @type {Set<string>} */
|
|
364
|
+
const events = new Set();
|
|
365
|
+
for (const sibling of siblings) {
|
|
366
|
+
const event = sibling.event;
|
|
367
|
+
const key = event.replica + event.incarnation + ":" + event.sequence;
|
|
368
|
+
if (events.has(key) || !context.some(origin => origin.replica === event.replica && origin.incarnation === event.incarnation && origin.sequence >= event.sequence)) {
|
|
369
|
+
throw new TypeError("Point causal context is incomplete or contains duplicate siblings.");
|
|
370
|
+
}
|
|
371
|
+
events.add(key);
|
|
372
|
+
}
|
|
373
|
+
return { version: 1, state, state_version, context, siblings };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param {unknown} value
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
380
|
+
function cursor(value) {
|
|
381
|
+
const message = "Cursor must be bounded opaque canonical base64url.";
|
|
382
|
+
if (typeof value !== "string" || value.length > 1024 || !/^[A-Za-z0-9_-]+$/u.test(value)) throw new TypeError(message);
|
|
383
|
+
// Validate only the encoding; the producer-owned cursor contents remain opaque.
|
|
384
|
+
try { parseBytes(value); }
|
|
385
|
+
catch { throw new TypeError(message); }
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {DatabaseQueryPredicate} predicate
|
|
391
|
+
* @returns {JsonObject}
|
|
392
|
+
*/
|
|
393
|
+
function predicateToWire(predicate) {
|
|
394
|
+
const entry = object(predicate, ["op", "path"], ["value"]);
|
|
395
|
+
const path = array(entry.path, 16, 1).map(fieldId);
|
|
396
|
+
if (entry.op === "is_null" || entry.op === "is_missing") {
|
|
397
|
+
object(predicate, ["op", "path"]);
|
|
398
|
+
return { op: entry.op, path };
|
|
399
|
+
}
|
|
400
|
+
if (!["equal", "less_than", "less_than_or_equal", "greater_than", "greater_than_or_equal"].includes(String(entry.op))) throw new TypeError("Unsupported query operator.");
|
|
401
|
+
object(predicate, ["op", "path", "value"]);
|
|
402
|
+
const value = encodeDatabaseValue(/** @type {DatabaseValue} */ (entry.value));
|
|
403
|
+
if (!scalarTypes.has(String(value.type))) throw new TypeError("Query values must be supported scalars.");
|
|
404
|
+
return { op: String(entry.op), path, value };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @param {unknown} value
|
|
409
|
+
* @returns {number}
|
|
410
|
+
*/
|
|
411
|
+
function queryLimit(value) {
|
|
412
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 128) throw new TypeError("Query limit must be between 1 and 128.");
|
|
413
|
+
return value;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {DatabaseQuery} query
|
|
418
|
+
* @returns {string}
|
|
419
|
+
*/
|
|
420
|
+
export function encodeDatabaseQuery(query) {
|
|
421
|
+
const entry = object(query, ["predicate", "limit"], ["cursor"]);
|
|
422
|
+
return boundedJson({ version: 1, predicate: predicateToWire(/** @type {DatabaseQueryPredicate} */ (entry.predicate)), limit: queryLimit(entry.limit), ...(entry.cursor === undefined ? {} : { cursor: cursor(entry.cursor) }) });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {unknown} raw
|
|
427
|
+
* @returns {DatabaseQueryPage["rows"]}
|
|
428
|
+
*/
|
|
429
|
+
function rowsFromWire(raw) {
|
|
430
|
+
let previous = "";
|
|
431
|
+
return array(raw, 128).map(rawRow => {
|
|
432
|
+
const entry = object(rawRow, ["document", "point"]);
|
|
433
|
+
const document = requireDatabaseUuid(entry.document, "document");
|
|
434
|
+
if (document <= previous) throw new TypeError("Query documents must be strictly increasing.");
|
|
435
|
+
previous = document;
|
|
436
|
+
return { document, point: decodeDatabasePoint(entry.point) };
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {unknown} raw
|
|
442
|
+
* @returns {DatabaseQueryPage}
|
|
443
|
+
*/
|
|
444
|
+
export function decodeDatabaseQueryPage(raw) {
|
|
445
|
+
const entry = object(raw, ["version", "rows"], ["next_cursor"]);
|
|
446
|
+
if (entry.version !== 1) throw new TypeError("Unsupported query protocol.");
|
|
447
|
+
return { version: 1, rows: rowsFromWire(entry.rows), ...(entry.next_cursor === undefined ? {} : { next_cursor: cursor(entry.next_cursor) }) };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @param {DatabaseSubscriptionRequest} request
|
|
452
|
+
* @returns {string}
|
|
453
|
+
*/
|
|
454
|
+
export function encodeDatabaseSubscription(request) {
|
|
455
|
+
if (request.phase === "snapshot") {
|
|
456
|
+
const entry = object(request, ["phase", "predicate", "limit"], ["snapshot_cursor"]);
|
|
457
|
+
return boundedJson({ version: 1, phase: "snapshot", predicate: predicateToWire(request.predicate), limit: queryLimit(entry.limit), ...(entry.snapshot_cursor === undefined ? {} : { snapshot_cursor: cursor(entry.snapshot_cursor) }) });
|
|
458
|
+
}
|
|
459
|
+
object(request, ["phase", "predicate", "poll_cursor"]);
|
|
460
|
+
if (request.phase !== "poll") throw new TypeError("Unsupported subscription phase.");
|
|
461
|
+
return boundedJson({ version: 1, phase: "poll", predicate: predicateToWire(request.predicate), poll_cursor: cursor(request.poll_cursor) });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* @param {unknown} raw
|
|
466
|
+
* @returns {DatabaseSubscriptionResponse}
|
|
467
|
+
*/
|
|
468
|
+
export function decodeDatabaseSubscription(raw) {
|
|
469
|
+
const initial = object(raw, ["version", "phase"], ["rows", "next_snapshot_cursor", "poll_cursor", "groups"]);
|
|
470
|
+
if (initial.version !== 1) throw new TypeError("Unsupported subscription protocol.");
|
|
471
|
+
if (initial.phase === "snapshot") {
|
|
472
|
+
const continuation = Object.hasOwn(initial, "next_snapshot_cursor") ? "next_snapshot_cursor" : "poll_cursor";
|
|
473
|
+
const entry = object(raw, ["version", "phase", "rows", continuation]);
|
|
474
|
+
const rows = rowsFromWire(entry.rows);
|
|
475
|
+
return continuation === "next_snapshot_cursor"
|
|
476
|
+
? { version: 1, phase: "snapshot", rows, next_snapshot_cursor: cursor(entry.next_snapshot_cursor) }
|
|
477
|
+
: { version: 1, phase: "snapshot", rows, poll_cursor: cursor(entry.poll_cursor) };
|
|
478
|
+
}
|
|
479
|
+
const entry = object(raw, ["version", "phase", "groups", "poll_cursor"]);
|
|
480
|
+
if (entry.phase !== "poll") throw new TypeError("Unsupported subscription phase.");
|
|
481
|
+
let previous = 0n;
|
|
482
|
+
const groups = array(entry.groups, 8).map(rawGroup => {
|
|
483
|
+
const group = object(rawGroup, ["sequence", "effects"]);
|
|
484
|
+
const sequence = decimal(group.sequence, "sequence", true);
|
|
485
|
+
if (sequence <= previous) throw new TypeError("Subscription sequences must increase.");
|
|
486
|
+
previous = sequence;
|
|
487
|
+
return { sequence, effects: array(group.effects, 128).map(rawEffect => {
|
|
488
|
+
const effect = object(rawEffect, ["document", "point"]);
|
|
489
|
+
return { document: requireDatabaseUuid(effect.document, "document"), point: effect.point === null ? null : decodeDatabasePoint(effect.point) };
|
|
490
|
+
}) };
|
|
491
|
+
});
|
|
492
|
+
return { version: 1, phase: "poll", groups, poll_cursor: cursor(entry.poll_cursor) };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* @param {DatabaseAuthority} authority
|
|
497
|
+
* @returns {JsonObject}
|
|
498
|
+
*/
|
|
499
|
+
export function encodeDatabaseAuthority(authority) {
|
|
500
|
+
if (authority.mode === "leaderless") { object(authority, ["mode"]); return { mode: "leaderless" }; }
|
|
501
|
+
object(authority, ["mode", "generation", "writer"]);
|
|
502
|
+
if (authority.mode !== "single_writer") throw new TypeError("Unsupported authority mode.");
|
|
503
|
+
const writer = object(authority.writer, ["replica", "incarnation"]);
|
|
504
|
+
return { mode: "single_writer", generation: requireDatabaseU64(authority.generation, "authority generation", true).toString(), writer: { replica: requireDatabaseUuid(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* @param {unknown} raw
|
|
509
|
+
* @returns {DatabaseAuthority}
|
|
510
|
+
*/
|
|
511
|
+
export function decodeDatabaseAuthority(raw) {
|
|
512
|
+
const initial = object(raw, ["mode"], ["generation", "writer"]);
|
|
513
|
+
if (initial.mode === "leaderless") { object(raw, ["mode"]); return { mode: "leaderless" }; }
|
|
514
|
+
const entry = object(raw, ["mode", "generation", "writer"]);
|
|
515
|
+
if (entry.mode !== "single_writer") throw new TypeError("Unsupported authority mode.");
|
|
516
|
+
const writer = object(entry.writer, ["replica", "incarnation"]);
|
|
517
|
+
return { mode: "single_writer", generation: decimal(entry.generation, "authority generation", true), writer: { replica: requireDatabaseUuid(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {DatabaseCondition} condition
|
|
522
|
+
* @returns {JsonObject}
|
|
523
|
+
*/
|
|
524
|
+
function conditionToWire(condition) {
|
|
525
|
+
if (condition.kind === "none" || condition.kind === "absent") { object(condition, ["kind"]); return { kind: condition.kind }; }
|
|
526
|
+
object(condition, ["kind", "version"]);
|
|
527
|
+
if (condition.kind !== "version") throw new TypeError("Unsupported condition.");
|
|
528
|
+
return { kind: "version", version: hex(condition.version, 32, "condition version") };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* @param {DatabaseTransactionId} transaction
|
|
533
|
+
* @returns {JsonObject}
|
|
534
|
+
*/
|
|
535
|
+
function transactionToWire(transaction) {
|
|
536
|
+
object(transaction, ["expires_at_unix_ms", "nonce"]);
|
|
537
|
+
return { expires_at_unix_ms: requireDatabaseU64(transaction.expires_at_unix_ms, "expiry").toString(), nonce: hex(transaction.nonce, 16, "nonce") };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* @param {unknown} raw
|
|
542
|
+
* @returns {DatabaseTransactionId}
|
|
543
|
+
*/
|
|
544
|
+
function transactionFromWire(raw) {
|
|
545
|
+
const entry = object(raw, ["expires_at_unix_ms", "nonce"]);
|
|
546
|
+
return { expires_at_unix_ms: decimal(entry.expires_at_unix_ms, "expiry"), nonce: hex(entry.nonce, 16, "nonce") };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* @param {unknown} raw
|
|
551
|
+
* @returns {DatabaseCommitReceipt}
|
|
552
|
+
*/
|
|
553
|
+
export function decodeDatabaseCommitReceipt(raw) {
|
|
554
|
+
const entry = object(raw, ["version", "durability", "transaction_id", "event", "commit_sequence", "effect_digest", "authority"]);
|
|
555
|
+
if (entry.version !== 1 || (entry.durability !== "local_durable" && entry.durability !== "object_durable")) throw new TypeError("Unsupported durability or protocol.");
|
|
556
|
+
return { version: 1, durability: entry.durability, transaction_id: transactionFromWire(entry.transaction_id), event: eventFromWire(entry.event), commit_sequence: decimal(entry.commit_sequence, "commit_sequence", true), effect_digest: hex(entry.effect_digest, 32, "effect digest"), authority: decodeDatabaseAuthority(entry.authority) };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* @param {DatabaseWrite} write
|
|
561
|
+
* @param {DatabaseAuthority} authority
|
|
562
|
+
* @returns {JsonObject}
|
|
563
|
+
*/
|
|
564
|
+
function writeToWire(write, authority) {
|
|
565
|
+
object(write, ["collection", "document", "schema_generation", "schema_digest", "condition", "observed", "value"]);
|
|
566
|
+
if (authority.mode === "single_writer" && write.condition.kind === "none") throw new TypeError("Single-writer mutations require an atomic condition.");
|
|
567
|
+
const value = write.value.kind === "tombstone"
|
|
568
|
+
? (object(write.value, ["kind"]), { kind: "tombstone" })
|
|
569
|
+
: (object(write.value, ["kind", "fields"]), { kind: "live", fields: fieldsToWire(write.value.fields, 1, { nodes: 0 }) });
|
|
570
|
+
if (write.value.kind !== "live" && write.value.kind !== "tombstone") throw new TypeError("Unsupported write kind.");
|
|
571
|
+
if (value.kind === "live") validateNativeDocument(/** @type {readonly Json[]} */ (value.fields));
|
|
572
|
+
return { collection: requireDatabaseUuid(write.collection, "collection"), document: requireDatabaseUuid(write.document, "document"), schema_generation: requireDatabaseU64(write.schema_generation, "schema generation", true).toString(), schema_digest: hex(write.schema_digest, 32, "schema digest"), condition: conditionToWire(write.condition), observed: observedToWire(write.observed), value };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
class Bytes {
|
|
576
|
+
/**
|
|
577
|
+
* @readonly
|
|
578
|
+
* @type {Uint8Array[]}
|
|
579
|
+
*/
|
|
580
|
+
chunks = [];
|
|
581
|
+
length = 0;
|
|
582
|
+
/**
|
|
583
|
+
* @param {Uint8Array} bytes
|
|
584
|
+
* @returns {void}
|
|
585
|
+
*/
|
|
586
|
+
put(bytes) { this.length += bytes.length; if (this.length > maximumBodyBytes * 2) throw new TypeError("Canonical effect exceeds its byte limit."); this.chunks.push(bytes); }
|
|
587
|
+
/**
|
|
588
|
+
* @param {bigint} value
|
|
589
|
+
* @param {number} size
|
|
590
|
+
* @returns {void}
|
|
591
|
+
*/
|
|
592
|
+
integer(value, size) {
|
|
593
|
+
const bytes = new Uint8Array(size);
|
|
594
|
+
for (let index = 0; index < size; index++) { bytes[index] = Number(value & 255n); value >>= 8n; }
|
|
595
|
+
this.put(bytes);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* @param {string} value
|
|
599
|
+
* @returns {void}
|
|
600
|
+
*/
|
|
601
|
+
hex(value) { this.put(Uint8Array.from(value.match(/../gu) ?? [], byte => Number.parseInt(byte, 16))); }
|
|
602
|
+
/**
|
|
603
|
+
* @param {string} value
|
|
604
|
+
* @returns {void}
|
|
605
|
+
*/
|
|
606
|
+
uuid(value) { this.hex(value.replaceAll("-", "")); }
|
|
607
|
+
/** @returns {Uint8Array} */
|
|
608
|
+
finish() { const bytes = new Uint8Array(this.length); let offset = 0; for (const chunk of this.chunks) { bytes.set(chunk, offset); offset += chunk.length; } return bytes; }
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Exact tb-format nested containers: u16 count, optional u32 field ID, u8 type, u32 byte length, bytes.
|
|
613
|
+
* @param {JsonObject} value
|
|
614
|
+
* @param {boolean} [insideArray]
|
|
615
|
+
* @returns {{ type: number; bytes: Uint8Array }}
|
|
616
|
+
*/
|
|
617
|
+
function nativeValue(value, insideArray = false) {
|
|
618
|
+
const bytes = new Bytes();
|
|
619
|
+
/** @type {number} */
|
|
620
|
+
let type;
|
|
621
|
+
switch (value.type) {
|
|
622
|
+
case "null": type = 1; break;
|
|
623
|
+
case "bool": type = 2; bytes.integer(value.value === true ? 1n : 0n, 1); break;
|
|
624
|
+
case "i64": type = 3; bytes.integer(BigInt(/** @type {string} */ (value.value)), 8); break;
|
|
625
|
+
case "utf8": type = 4; bytes.put(encoder.encode(/** @type {string} */ (value.value))); break;
|
|
626
|
+
case "bytes": type = 5; bytes.put(parseBytes(value.value)); break;
|
|
627
|
+
case "map":
|
|
628
|
+
case "array": {
|
|
629
|
+
const map = value.type === "map";
|
|
630
|
+
if (!map && insideArray) throw new TypeError("Arrays cannot contain descendant arrays, including through maps.");
|
|
631
|
+
type = map ? 6 : 7;
|
|
632
|
+
const entries = array(map ? value.fields : value.items);
|
|
633
|
+
bytes.integer(BigInt(entries.length), 2);
|
|
634
|
+
for (const entry of entries) {
|
|
635
|
+
const field = map ? object(entry, ["id", "value"]) : undefined;
|
|
636
|
+
if (field !== undefined) bytes.integer(BigInt(fieldId(field.id)), 4);
|
|
637
|
+
const native = nativeValue(/** @type {JsonObject} */ (field?.value ?? entry), insideArray || !map);
|
|
638
|
+
bytes.integer(BigInt(native.type), 1); bytes.integer(BigInt(native.bytes.length), 4); bytes.put(native.bytes);
|
|
639
|
+
}
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
default: throw new TypeError("Unsupported database value type.");
|
|
643
|
+
}
|
|
644
|
+
if (bytes.length > maximumValueBytes) throw new TypeError("Native value exceeds its byte limit.");
|
|
645
|
+
return { type, bytes: bytes.finish() };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* @param {readonly Json[]} fields
|
|
650
|
+
* @returns {void}
|
|
651
|
+
*/
|
|
652
|
+
function validateNativeDocument(fields) {
|
|
653
|
+
// tb-format document header + checksum, directory entries, and native payloads.
|
|
654
|
+
let size = 98 + 13 * fields.length;
|
|
655
|
+
for (const raw of fields) {
|
|
656
|
+
size += nativeValue(/** @type {JsonObject} */ (object(raw, ["id", "value"]).value)).bytes.length;
|
|
657
|
+
if (size > maximumValueBytes) throw new TypeError("Encoded document exceeds its 1 MiB limit.");
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* @param {DatabaseScope} scope
|
|
663
|
+
* @param {JsonObject} authority
|
|
664
|
+
* @param {readonly JsonObject[]} writes
|
|
665
|
+
* @returns {Uint8Array}
|
|
666
|
+
*/
|
|
667
|
+
function effectBytes(scope, authority, writes) {
|
|
668
|
+
const bytes = new Bytes();
|
|
669
|
+
bytes.put(encoder.encode("TB-DOC-EFFECT-v1\0"));
|
|
670
|
+
bytes.uuid(requireDatabaseUuid(scope.tenant, "tenant")); bytes.uuid(requireDatabaseUuid(scope.database, "database")); bytes.integer(requireDatabaseU64(scope.shard, "shard"), 8);
|
|
671
|
+
const singleWriter = authority.mode === "single_writer";
|
|
672
|
+
bytes.integer(singleWriter ? 1n : 0n, 1);
|
|
673
|
+
if (singleWriter) {
|
|
674
|
+
const writer = object(authority.writer, ["replica", "incarnation"]);
|
|
675
|
+
bytes.integer(BigInt(/** @type {string} */ (authority.generation)), 8); bytes.uuid(/** @type {string} */ (writer.replica)); bytes.hex(/** @type {string} */ (writer.incarnation));
|
|
676
|
+
}
|
|
677
|
+
bytes.integer(BigInt(writes.length), 8);
|
|
678
|
+
for (const write of writes) {
|
|
679
|
+
bytes.uuid(/** @type {string} */ (write.collection)); bytes.uuid(/** @type {string} */ (write.document)); bytes.integer(BigInt(/** @type {string} */ (write.schema_generation)), 8); bytes.hex(/** @type {string} */ (write.schema_digest));
|
|
680
|
+
const condition = object(write.condition, ["kind"], ["version"]);
|
|
681
|
+
bytes.integer(condition.kind === "none" ? 0n : condition.kind === "absent" ? 1n : 2n, 1);
|
|
682
|
+
if (condition.kind === "version") bytes.hex(/** @type {string} */ (condition.version));
|
|
683
|
+
const observed = array(write.observed);
|
|
684
|
+
bytes.integer(BigInt(observed.length), 8);
|
|
685
|
+
for (const raw of observed) { const entry = object(raw, ["replica", "incarnation", "sequence"]); bytes.uuid(/** @type {string} */ (entry.replica)); bytes.hex(/** @type {string} */ (entry.incarnation)); bytes.integer(BigInt(/** @type {string} */ (entry.sequence)), 8); }
|
|
686
|
+
const value = object(write.value, ["kind"], ["fields"]);
|
|
687
|
+
bytes.integer(value.kind === "tombstone" ? 0n : 1n, 1);
|
|
688
|
+
if (value.kind === "live") {
|
|
689
|
+
const fields = array(value.fields); bytes.integer(BigInt(fields.length), 8);
|
|
690
|
+
for (const raw of fields) { const field = object(raw, ["id", "value"]); const native = nativeValue(/** @type {JsonObject} */ (field.value)); bytes.integer(BigInt(/** @type {number} */ (field.id)), 4); bytes.integer(BigInt(native.type), 1); bytes.integer(BigInt(native.bytes.length), 8); bytes.put(native.bytes); }
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return bytes.finish();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* @param {Json} value
|
|
698
|
+
* @returns {string}
|
|
699
|
+
*/
|
|
700
|
+
function boundedJson(value) {
|
|
701
|
+
const encoded = JSON.stringify(value);
|
|
702
|
+
if (encoder.encode(encoded).length > maximumBodyBytes) throw new TypeError("Request exceeds its 4 MiB limit.");
|
|
703
|
+
return encoded;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* @param {DatabaseTransactionIdentity} identity
|
|
708
|
+
* @returns {string}
|
|
709
|
+
*/
|
|
710
|
+
export function encodeDatabaseStatus(identity) {
|
|
711
|
+
const entry = object(identity, ["transaction_id", "authority", "effect_digest"]);
|
|
712
|
+
return boundedJson({ version: 1, transaction_id: transactionToWire(/** @type {DatabaseTransactionId} */ (entry.transaction_id)), authority: encodeDatabaseAuthority(/** @type {DatabaseAuthority} */ (entry.authority)), effect_digest: hex(entry.effect_digest, 32, "effect digest") });
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* @param {unknown} raw
|
|
717
|
+
* @returns {DatabaseTransactionStatus}
|
|
718
|
+
*/
|
|
719
|
+
export function decodeDatabaseStatus(raw) {
|
|
720
|
+
const entry = object(raw, ["version", "state", "transaction_id", "authority", "effect_digest", "outcome"]);
|
|
721
|
+
if (entry.version !== 1) throw new TypeError("Unsupported transaction status protocol.");
|
|
722
|
+
const base = { version: /** @type {const} */ (1), transaction_id: transactionFromWire(entry.transaction_id), authority: decodeDatabaseAuthority(entry.authority), effect_digest: hex(entry.effect_digest, 32, "effect digest") };
|
|
723
|
+
if (entry.state === "pending" || entry.state === "unknown" || entry.state === "expired") {
|
|
724
|
+
if (entry.outcome !== null) throw new TypeError("Unresolved transaction status must have no terminal outcome.");
|
|
725
|
+
return { ...base, state: entry.state, outcome: null };
|
|
726
|
+
}
|
|
727
|
+
if (entry.state === "committed") {
|
|
728
|
+
const outcome = object(entry.outcome, ["receipt"]);
|
|
729
|
+
const receipt = decodeDatabaseCommitReceipt(outcome.receipt);
|
|
730
|
+
if (receipt.transaction_id.nonce !== base.transaction_id.nonce || receipt.transaction_id.expires_at_unix_ms !== base.transaction_id.expires_at_unix_ms || receipt.effect_digest !== base.effect_digest || JSON.stringify(encodeDatabaseAuthority(receipt.authority)) !== JSON.stringify(encodeDatabaseAuthority(base.authority))) {
|
|
731
|
+
throw new TypeError("Status receipt contradicts its operation identity.");
|
|
732
|
+
}
|
|
733
|
+
return { ...base, state: "committed", outcome: { receipt } };
|
|
734
|
+
}
|
|
735
|
+
if (entry.state === "rejected") {
|
|
736
|
+
const outcome = object(entry.outcome, ["error"]);
|
|
737
|
+
const error = object(outcome.error, ["code"]);
|
|
738
|
+
if (error.code !== "precondition_failed") throw new TypeError("Unsupported rejection outcome.");
|
|
739
|
+
return { ...base, state: "rejected", outcome: { error: { code: "precondition_failed" } } };
|
|
740
|
+
}
|
|
741
|
+
throw new TypeError("Unsupported transaction status.");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Reject duplicate JSON members and unbounded syntax before typed response decoding.
|
|
746
|
+
* @param {Uint8Array} bytes
|
|
747
|
+
* @returns {unknown}
|
|
748
|
+
*/
|
|
749
|
+
export function parseDatabaseJson(bytes) {
|
|
750
|
+
if (bytes.length < 1 || bytes.length > maximumBodyBytes) throw new TypeError("JSON response exceeds its byte limit.");
|
|
751
|
+
const input = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
752
|
+
let offset = 0;
|
|
753
|
+
/** @type {() => void} */
|
|
754
|
+
const whitespace = () => { while (/[ \t\r\n]/u.test(input[offset] ?? "x")) offset++; };
|
|
755
|
+
/** @type {() => never} */
|
|
756
|
+
const fail = () => { throw new TypeError("Response contains malformed or duplicate JSON members."); };
|
|
757
|
+
/** @type {() => string} */
|
|
758
|
+
const string = () => {
|
|
759
|
+
const start = offset++;
|
|
760
|
+
while (offset < input.length) {
|
|
761
|
+
const code = input.charCodeAt(offset);
|
|
762
|
+
if (input[offset] === '"') {
|
|
763
|
+
offset++;
|
|
764
|
+
/** @type {unknown} */
|
|
765
|
+
const parsed = JSON.parse(input.slice(start, offset));
|
|
766
|
+
if (typeof parsed !== "string") return fail();
|
|
767
|
+
return parsed;
|
|
768
|
+
}
|
|
769
|
+
if (code < 32) return fail();
|
|
770
|
+
if (input[offset++] === "\\") {
|
|
771
|
+
const escaped = input[offset++];
|
|
772
|
+
if (escaped === "u") {
|
|
773
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(input.slice(offset, offset + 4))) return fail();
|
|
774
|
+
offset += 4;
|
|
775
|
+
} else if (escaped === undefined || !'"\\/bfnrt'.includes(escaped)) return fail();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return fail();
|
|
779
|
+
};
|
|
780
|
+
/** @type {(depth: number) => void} */
|
|
781
|
+
const value = (depth) => {
|
|
782
|
+
if (depth > 64) fail();
|
|
783
|
+
whitespace();
|
|
784
|
+
if (input[offset] === '"') { string(); return; }
|
|
785
|
+
if (input[offset] === "{" || input[offset] === "[") {
|
|
786
|
+
const map = input[offset++] === "{";
|
|
787
|
+
const end = map ? "}" : "]";
|
|
788
|
+
/** @type {Set<string>} */
|
|
789
|
+
const keys = new Set();
|
|
790
|
+
whitespace();
|
|
791
|
+
if (input[offset] === end) { offset++; return; }
|
|
792
|
+
while (offset < input.length) {
|
|
793
|
+
if (map) {
|
|
794
|
+
whitespace();
|
|
795
|
+
if (input[offset] !== '"') fail();
|
|
796
|
+
const key = string();
|
|
797
|
+
if (keys.has(key)) fail();
|
|
798
|
+
keys.add(key);
|
|
799
|
+
whitespace();
|
|
800
|
+
if (input[offset++] !== ":") fail();
|
|
801
|
+
}
|
|
802
|
+
value(depth + 1);
|
|
803
|
+
whitespace();
|
|
804
|
+
if (input[offset] === end) { offset++; return; }
|
|
805
|
+
if (input[offset++] !== ",") fail();
|
|
806
|
+
}
|
|
807
|
+
fail();
|
|
808
|
+
}
|
|
809
|
+
for (const literal of ["true", "false", "null"]) {
|
|
810
|
+
if (input.slice(offset, offset + literal.length) === literal) { offset += literal.length; return; }
|
|
811
|
+
}
|
|
812
|
+
const number = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/uy;
|
|
813
|
+
number.lastIndex = offset;
|
|
814
|
+
if (number.exec(input) === null) fail();
|
|
815
|
+
offset = number.lastIndex;
|
|
816
|
+
};
|
|
817
|
+
value(0); whitespace();
|
|
818
|
+
if (offset !== input.length) fail();
|
|
819
|
+
return /** @type {unknown} */ (JSON.parse(input));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Build and snapshot the exact logical effect before awaiting WebCrypto.
|
|
824
|
+
* @param {DatabaseScope} scope
|
|
825
|
+
* @param {DatabaseCommit} commit
|
|
826
|
+
* @returns {Promise<{ readonly body: string; readonly effect_digest: string }>}
|
|
827
|
+
*/
|
|
828
|
+
export async function encodeDatabaseCommit(scope, commit) {
|
|
829
|
+
object(scope, ["tenant", "database", "shard"]);
|
|
830
|
+
object(commit, ["transaction_id", "authority", "writes"]);
|
|
831
|
+
const authority = encodeDatabaseAuthority(commit.authority);
|
|
832
|
+
const transaction = transactionToWire(commit.transaction_id);
|
|
833
|
+
let previous = "";
|
|
834
|
+
const writes = array(commit.writes, 1024, 1).map(raw => {
|
|
835
|
+
const write = writeToWire(/** @type {DatabaseWrite} */ (raw), commit.authority);
|
|
836
|
+
const order = String(write.collection) + String(write.document);
|
|
837
|
+
if (order <= previous) throw new TypeError("Writes must be sorted by collection and document.");
|
|
838
|
+
previous = order;
|
|
839
|
+
return write;
|
|
840
|
+
});
|
|
841
|
+
const canonical = effectBytes(scope, authority, writes);
|
|
842
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(canonical).buffer));
|
|
843
|
+
const effect_digest = Array.from(digest, byte => byte.toString(16).padStart(2, "0")).join("");
|
|
844
|
+
return { body: boundedJson({ version: 1, transaction_id: transaction, authority, effect_digest, writes }), effect_digest };
|
|
845
|
+
}
|