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,237 @@
1
+ /**
2
+ * The current TerraBase document protocol uses canonical UUID entity IDs.
3
+ * @typedef {string} CanonicalUuid
4
+ */
5
+
6
+ /**
7
+ * All 64-bit quantities stay bigint in JavaScript and decimal strings on the wire.
8
+ * @typedef {{
9
+ * readonly tenant: CanonicalUuid;
10
+ * readonly database: CanonicalUuid;
11
+ * readonly shard: bigint;
12
+ * }} DatabaseScope
13
+ */
14
+
15
+ /**
16
+ * @typedef {{
17
+ * readonly collection: CanonicalUuid;
18
+ * readonly schema_generation: bigint;
19
+ * readonly schema_digest: string;
20
+ * }} CollectionBinding
21
+ */
22
+
23
+ /**
24
+ * @typedef {(
25
+ * | { readonly type: "null" }
26
+ * | { readonly type: "bool"; readonly value: boolean }
27
+ * | { readonly type: "i64"; readonly value: bigint }
28
+ * | { readonly type: "utf8"; readonly value: string }
29
+ * | { readonly type: "bytes"; readonly value: Uint8Array }
30
+ * | { readonly type: "map"; readonly fields: readonly DatabaseField[] }
31
+ * | { readonly type: "array"; readonly items: readonly DatabaseValue[] }
32
+ * )} DatabaseValue
33
+ */
34
+
35
+ /**
36
+ * Missing fields are omitted; an explicit null remains a typed null value.
37
+ * @typedef {{ readonly id: number; readonly value: DatabaseValue }} DatabaseField
38
+ */
39
+
40
+ /**
41
+ * @typedef {{
42
+ * readonly replica: CanonicalUuid;
43
+ * readonly incarnation: string;
44
+ * readonly sequence: bigint;
45
+ * }} DatabaseObserved
46
+ */
47
+
48
+ /**
49
+ * @typedef {DatabaseObserved & {
50
+ * readonly database: CanonicalUuid;
51
+ * readonly shard: bigint;
52
+ * }} DatabaseEvent
53
+ */
54
+
55
+ /**
56
+ * @typedef {(
57
+ * | { readonly mode: "leaderless" }
58
+ * | {
59
+ * readonly mode: "single_writer";
60
+ * readonly generation: bigint;
61
+ * readonly writer: { readonly replica: CanonicalUuid; readonly incarnation: string };
62
+ * }
63
+ * )} DatabaseAuthority
64
+ */
65
+
66
+ /**
67
+ * @typedef {(
68
+ * | { readonly kind: "none" }
69
+ * | { readonly kind: "absent" }
70
+ * | { readonly kind: "version"; readonly version: string }
71
+ * )} DatabaseCondition
72
+ */
73
+
74
+ /**
75
+ * @typedef {(
76
+ * | { readonly kind: "live"; readonly fields: readonly DatabaseField[] }
77
+ * | { readonly kind: "tombstone" }
78
+ * )} DatabaseWriteValue
79
+ */
80
+
81
+ /**
82
+ * @typedef {CollectionBinding & {
83
+ * readonly document: CanonicalUuid;
84
+ * readonly condition: DatabaseCondition;
85
+ * readonly observed: readonly DatabaseObserved[];
86
+ * readonly value: DatabaseWriteValue;
87
+ * }} DatabaseWrite
88
+ */
89
+
90
+ /** @typedef {{ readonly expires_at_unix_ms: bigint; readonly nonce: string }} DatabaseTransactionId */
91
+
92
+ /**
93
+ * @typedef {{
94
+ * readonly transaction_id: DatabaseTransactionId;
95
+ * readonly authority: DatabaseAuthority;
96
+ * readonly writes: readonly DatabaseWrite[];
97
+ * }} DatabaseCommit
98
+ */
99
+
100
+ /**
101
+ * @typedef {Omit<CollectionBinding, "collection"> & {
102
+ * readonly event: DatabaseEvent;
103
+ * } & DatabaseWriteValue} DatabaseSibling
104
+ */
105
+
106
+ /**
107
+ * `state_version`: only a unique live or deleted state carries an atomic condition token.
108
+ * @typedef {{
109
+ * readonly version: 1;
110
+ * readonly state: "absent" | "deleted" | "value" | "conflict";
111
+ * readonly state_version: string | null;
112
+ * readonly context: readonly DatabaseObserved[];
113
+ * readonly siblings: readonly DatabaseSibling[];
114
+ * }} DatabasePoint
115
+ */
116
+
117
+ /** @typedef {Extract<DatabaseValue, { readonly type: "bool" | "i64" | "utf8" | "bytes" }>} DatabaseQueryValue */
118
+
119
+ /**
120
+ * @typedef {(
121
+ * | {
122
+ * readonly op: "equal" | "less_than" | "less_than_or_equal" | "greater_than" | "greater_than_or_equal";
123
+ * readonly path: readonly number[];
124
+ * readonly value: DatabaseQueryValue;
125
+ * }
126
+ * | { readonly op: "is_null" | "is_missing"; readonly path: readonly number[] }
127
+ * )} DatabaseQueryPredicate
128
+ */
129
+
130
+ /**
131
+ * @typedef {{
132
+ * readonly predicate: DatabaseQueryPredicate;
133
+ * readonly limit: number;
134
+ * readonly cursor?: string;
135
+ * }} DatabaseQuery
136
+ */
137
+
138
+ /** @typedef {{ readonly document: CanonicalUuid; readonly point: DatabasePoint }} DatabaseQueryRow */
139
+
140
+ /**
141
+ * @typedef {{
142
+ * readonly version: 1;
143
+ * readonly rows: readonly DatabaseQueryRow[];
144
+ * readonly next_cursor?: string;
145
+ * }} DatabaseQueryPage
146
+ */
147
+
148
+ /**
149
+ * @typedef {{
150
+ * readonly version: 1;
151
+ * readonly durability: "local_durable" | "object_durable";
152
+ * readonly transaction_id: DatabaseTransactionId;
153
+ * readonly event: DatabaseEvent;
154
+ * readonly commit_sequence: bigint;
155
+ * readonly effect_digest: string;
156
+ * readonly authority: DatabaseAuthority;
157
+ * }} DatabaseCommitReceipt
158
+ */
159
+
160
+ /**
161
+ * @typedef {{
162
+ * readonly transaction_id: DatabaseTransactionId;
163
+ * readonly authority: DatabaseAuthority;
164
+ * readonly effect_digest: string;
165
+ * }} DatabaseTransactionIdentity
166
+ */
167
+
168
+ /**
169
+ * @typedef {DatabaseTransactionIdentity & { readonly version: 1 } & (
170
+ * | { readonly state: "committed"; readonly outcome: { readonly receipt: DatabaseCommitReceipt } }
171
+ * | { readonly state: "rejected"; readonly outcome: { readonly error: { readonly code: "precondition_failed" } } }
172
+ * | { readonly state: "pending" | "unknown" | "expired"; readonly outcome: null }
173
+ * )} DatabaseTransactionStatus
174
+ */
175
+
176
+ /**
177
+ * An uncertain request needs status resolution; it is never an aborted transaction.
178
+ * @template T
179
+ * @typedef {(
180
+ * | { readonly ok: true; readonly status: number; readonly value: T }
181
+ * | {
182
+ * readonly ok: false;
183
+ * readonly error: {
184
+ * readonly code: string;
185
+ * readonly status?: number;
186
+ * readonly mutation_outcome?: "rejected" | "uncertain" | "unknown";
187
+ * readonly transaction?: DatabaseTransactionIdentity;
188
+ * };
189
+ * }
190
+ * )} DatabaseResult
191
+ */
192
+
193
+ /**
194
+ * @typedef {(
195
+ * | {
196
+ * readonly phase: "snapshot";
197
+ * readonly predicate: DatabaseQueryPredicate;
198
+ * readonly limit: number;
199
+ * readonly snapshot_cursor?: string;
200
+ * }
201
+ * | { readonly phase: "poll"; readonly predicate: DatabaseQueryPredicate; readonly poll_cursor: string }
202
+ * )} DatabaseSubscriptionRequest
203
+ */
204
+
205
+ /** @typedef {{ readonly document: CanonicalUuid; readonly point: DatabasePoint | null }} DatabaseSubscriptionEffect */
206
+
207
+ /**
208
+ * @typedef {{
209
+ * readonly sequence: bigint;
210
+ * readonly effects: readonly DatabaseSubscriptionEffect[];
211
+ * }} DatabaseSubscriptionGroup
212
+ */
213
+
214
+ /**
215
+ * @typedef {(
216
+ * | {
217
+ * readonly version: 1;
218
+ * readonly phase: "snapshot";
219
+ * readonly rows: readonly DatabaseQueryRow[];
220
+ * readonly next_snapshot_cursor: string;
221
+ * }
222
+ * | {
223
+ * readonly version: 1;
224
+ * readonly phase: "snapshot";
225
+ * readonly rows: readonly DatabaseQueryRow[];
226
+ * readonly poll_cursor: string;
227
+ * }
228
+ * | {
229
+ * readonly version: 1;
230
+ * readonly phase: "poll";
231
+ * readonly groups: readonly DatabaseSubscriptionGroup[];
232
+ * readonly poll_cursor: string;
233
+ * }
234
+ * )} DatabaseSubscriptionResponse
235
+ */
236
+
237
+ export {};
@@ -0,0 +1,422 @@
1
+ import { encodeDatabaseSubscription, requireDatabaseUuid, requireDatabaseU64 } from "./database-codec.js";
2
+ /**
3
+ * @import { TerraBaseClient } from "./database.js"
4
+ * @import {
5
+ * DatabaseField, DatabasePoint, DatabaseQueryPredicate, DatabaseQueryRow,
6
+ * DatabaseResult, DatabaseScope, DatabaseSubscriptionRequest,
7
+ * DatabaseSubscriptionResponse, DatabaseValue,
8
+ * } from "./database-types.js"
9
+ * @import { ReactiveReset, ReactiveSubscriptionContext } from "./react/core.js"
10
+ */
11
+
12
+ /**
13
+ * - `limit`: snapshot page size, between 1 and 128.
14
+ * - `maximumRows`: client memory bound, including all snapshot pages and later inserts.
15
+ * - `retryLimit`: bounded retries of read transport failures with the identical cursor.
16
+ * @typedef {{
17
+ * readonly collection: string;
18
+ * readonly predicate: DatabaseQueryPredicate;
19
+ * readonly limit?: number;
20
+ * readonly maximumRows?: number;
21
+ * readonly pollIntervalMs?: number;
22
+ * readonly retryLimit?: number;
23
+ * readonly retryDelayMs?: number;
24
+ * }} DatabaseQueryOptions
25
+ */
26
+
27
+ /**
28
+ * @typedef {{
29
+ * readonly scope: DatabaseScope;
30
+ * readonly collection: string;
31
+ * readonly predicate: DatabaseQueryPredicate;
32
+ * readonly limit: number;
33
+ * }} DatabaseViewBinding
34
+ */
35
+
36
+ /**
37
+ * Local generation and server position of one complete authorized cut.
38
+ * @typedef {{
39
+ * readonly generation: number;
40
+ * readonly poll_cursor: string;
41
+ * readonly sequence?: bigint;
42
+ * }} DatabaseViewPosition
43
+ */
44
+
45
+ /**
46
+ * @typedef {{
47
+ * readonly rows: readonly DatabaseQueryRow[];
48
+ * readonly position: DatabaseViewPosition;
49
+ * }} DatabaseViewSnapshot
50
+ */
51
+
52
+ /**
53
+ * `next`: an async consumer can acknowledge publication before the next poll.
54
+ * @typedef {{
55
+ * readonly next: (rows: readonly DatabaseQueryRow[], position: DatabaseViewPosition) => void | Promise<void>;
56
+ * readonly error: (error: unknown) => void;
57
+ * readonly reset: (reset: ReactiveReset<readonly DatabaseQueryRow[]>) => void;
58
+ * }} DatabaseQueryCallbacks
59
+ */
60
+
61
+ /**
62
+ * @typedef {{
63
+ * readonly key?: string | readonly unknown[];
64
+ * readonly binding: DatabaseViewBinding;
65
+ * readonly subscribe: (
66
+ * observer: DatabaseQueryCallbacks,
67
+ * context: ReactiveSubscriptionContext,
68
+ * ) => () => void;
69
+ * }} DatabaseQuerySource
70
+ */
71
+
72
+ export class DatabaseSubscriptionError extends Error {
73
+ /**
74
+ * @override
75
+ * @readonly
76
+ * @type {"DatabaseSubscriptionError"}
77
+ */
78
+ name = "DatabaseSubscriptionError";
79
+ /**
80
+ * @readonly
81
+ * @type {string}
82
+ */
83
+ code;
84
+ /**
85
+ * @readonly
86
+ * @type {Extract<DatabaseResult<never>, { readonly ok: false }>["error"] | undefined}
87
+ */
88
+ detail;
89
+ /**
90
+ * @param {string} code
91
+ * @param {Extract<DatabaseResult<never>, { readonly ok: false }>["error"]} [detail]
92
+ */
93
+ constructor(code, detail) {
94
+ super(`TerraBase subscription: ${code}`, { cause: detail });
95
+ this.code = code;
96
+ this.detail = detail;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * @param {number | undefined} value
102
+ * @param {number} fallback
103
+ * @param {number} maximum
104
+ * @param {string} label
105
+ * @returns {number}
106
+ */
107
+ function boundedInteger(value, fallback, maximum, label) {
108
+ const resolved = value ?? fallback;
109
+ if (!Number.isInteger(resolved) || resolved < 1 || resolved > maximum) throw new TypeError(`${label} is outside its bounds.`);
110
+ return resolved;
111
+ }
112
+
113
+ /**
114
+ * @param {DatabaseValue} value
115
+ * @returns {DatabaseValue}
116
+ */
117
+ function valueCopy(value) {
118
+ switch (value.type) {
119
+ case "bytes": {
120
+ const bytes = value.value.slice();
121
+ // A typed array cannot be frozen. Keep its backing bytes private and
122
+ // return a copy on every read, including reads by another subscriber.
123
+ return Object.freeze({ type: /** @type {const} */ ("bytes"), get value() { return bytes.slice(); } });
124
+ }
125
+ case "map": return Object.freeze({ type: "map", fields: fieldsCopy(value.fields) });
126
+ case "array": return Object.freeze({ type: "array", items: Object.freeze(value.items.map(valueCopy)) });
127
+ default: return Object.freeze({ ...value });
128
+ }
129
+ }
130
+
131
+ /**
132
+ * @param {readonly DatabaseField[]} fields
133
+ * @returns {readonly DatabaseField[]}
134
+ */
135
+ function fieldsCopy(fields) {
136
+ return Object.freeze(fields.map(field => Object.freeze({ id: field.id, value: valueCopy(field.value) })));
137
+ }
138
+
139
+ /**
140
+ * @param {DatabaseQueryPredicate} predicate
141
+ * @returns {DatabaseQueryPredicate}
142
+ */
143
+ function predicateCopy(predicate) {
144
+ const path = Object.freeze([...predicate.path]);
145
+ if (!("value" in predicate)) return Object.freeze({ op: predicate.op, path });
146
+ return Object.freeze({ op: predicate.op, path, value: /** @type {typeof predicate.value} */ (valueCopy(predicate.value)) });
147
+ }
148
+
149
+ /**
150
+ * @param {DatabasePoint} point
151
+ * @param {DatabaseScope} scope
152
+ * @returns {DatabasePoint}
153
+ */
154
+ function pointCopy(point, scope) {
155
+ if (point.siblings.some(sibling => sibling.event.database !== scope.database || sibling.event.shard !== scope.shard)) {
156
+ throw new DatabaseSubscriptionError("scope_mismatch");
157
+ }
158
+ return Object.freeze({
159
+ ...point,
160
+ context: Object.freeze(point.context.map(observed => Object.freeze({ ...observed }))),
161
+ siblings: Object.freeze(point.siblings.map(sibling => Object.freeze({
162
+ ...sibling,
163
+ event: Object.freeze({ ...sibling.event }),
164
+ ...(sibling.kind === "live" ? { fields: fieldsCopy(sibling.fields) } : {}),
165
+ }))),
166
+ });
167
+ }
168
+
169
+ /**
170
+ * @param {string} value
171
+ * @returns {string}
172
+ */
173
+ function cursor(value) {
174
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096) throw new DatabaseSubscriptionError("invalid_cursor");
175
+ return value;
176
+ }
177
+
178
+ /**
179
+ * Pure native result reducer. A paged snapshot stays private until its
180
+ * terminal poll cursor. Poll groups are staged in order and published as one
181
+ * complete response; invalid input cannot partially replace the prior cut.
182
+ */
183
+ export class DatabaseSubscriptionView {
184
+ /**
185
+ * @readonly
186
+ * @type {DatabaseViewBinding}
187
+ */
188
+ binding;
189
+ /**
190
+ * @readonly
191
+ * @type {number}
192
+ */
193
+ #maximumRows;
194
+ /** @type {Map<string, DatabaseQueryRow>} */
195
+ #rows = new Map();
196
+ /** @type {DatabaseQueryRow[]} */
197
+ #snapshotRows = [];
198
+ /** @type {string | undefined} */
199
+ #snapshotCursor;
200
+ /** @type {Set<string>} */
201
+ #seenSnapshotCursors = new Set();
202
+ /** @type {string | undefined} */
203
+ #pollCursor;
204
+ /** @type {bigint | undefined} */
205
+ #sequence;
206
+ #generation = 0;
207
+ /** @type {DatabaseViewSnapshot | undefined} */
208
+ #snapshot;
209
+
210
+ /**
211
+ * @param {DatabaseScope} scope
212
+ * @param {Pick<DatabaseQueryOptions, "collection" | "predicate" | "limit" | "maximumRows">} options
213
+ */
214
+ constructor(scope, options) {
215
+ const capturedScope = Object.freeze({
216
+ tenant: requireDatabaseUuid(scope.tenant, "tenant"),
217
+ database: requireDatabaseUuid(scope.database, "database"),
218
+ shard: requireDatabaseU64(scope.shard, "shard"),
219
+ });
220
+ const predicate = predicateCopy(options.predicate);
221
+ const limit = boundedInteger(options.limit, 128, 128, "Snapshot page size");
222
+ encodeDatabaseSubscription({ phase: "snapshot", predicate, limit });
223
+ this.binding = Object.freeze({ scope: capturedScope, collection: requireDatabaseUuid(options.collection, "collection"), predicate, limit });
224
+ this.#maximumRows = boundedInteger(options.maximumRows, 10_000, 1_000_000, "Maximum rows");
225
+ }
226
+
227
+ /** @returns {DatabaseViewSnapshot | undefined} */
228
+ getSnapshot() { return this.#snapshot; }
229
+
230
+ /** @returns {DatabaseSubscriptionRequest} */
231
+ getRequest() {
232
+ const { predicate, limit } = this.binding;
233
+ return this.#pollCursor === undefined
234
+ ? Object.freeze({ phase: "snapshot", predicate, limit, ...(this.#snapshotCursor === undefined ? {} : { snapshot_cursor: this.#snapshotCursor }) })
235
+ : Object.freeze({ phase: "poll", predicate, poll_cursor: this.#pollCursor });
236
+ }
237
+
238
+ /**
239
+ * Discard every opaque position; the next request starts a fresh cut.
240
+ * @returns {void}
241
+ */
242
+ restart() {
243
+ this.#snapshotRows = [];
244
+ this.#snapshotCursor = undefined;
245
+ this.#seenSnapshotCursors.clear();
246
+ this.#pollCursor = undefined;
247
+ this.#sequence = undefined;
248
+ this.#snapshot = undefined;
249
+ }
250
+
251
+ /**
252
+ * @param {DatabaseSubscriptionResponse} response
253
+ * @returns {DatabaseViewSnapshot | undefined}
254
+ */
255
+ apply(response) {
256
+ if (this.#pollCursor === undefined) {
257
+ if (response.phase !== "snapshot") throw new DatabaseSubscriptionError("phase_mismatch");
258
+ if (response.rows.length > this.binding.limit) throw new DatabaseSubscriptionError("page_limit_exceeded");
259
+ let previous = this.#snapshotRows.at(-1)?.document ?? "";
260
+ const page = response.rows.map(row => {
261
+ const document = requireDatabaseUuid(row.document, "document");
262
+ if (document <= previous) throw new DatabaseSubscriptionError("snapshot_order_invalid");
263
+ previous = document;
264
+ return Object.freeze({ document, point: pointCopy(row.point, this.binding.scope) });
265
+ });
266
+ if (this.#snapshotRows.length + page.length > this.#maximumRows) throw new DatabaseSubscriptionError("view_limit_exceeded");
267
+ if ("next_snapshot_cursor" in response) {
268
+ const next = cursor(response.next_snapshot_cursor);
269
+ if (this.#seenSnapshotCursors.has(next)) throw new DatabaseSubscriptionError("snapshot_cursor_cycle");
270
+ this.#snapshotRows.push(...page);
271
+ this.#seenSnapshotCursors.add(next);
272
+ this.#snapshotCursor = next;
273
+ return undefined;
274
+ }
275
+ const pollCursor = cursor(response.poll_cursor);
276
+ this.#rows = new Map([...this.#snapshotRows, ...page].map(row => [row.document, row]));
277
+ this.#snapshotRows = [];
278
+ this.#snapshotCursor = undefined;
279
+ this.#seenSnapshotCursors.clear();
280
+ this.#pollCursor = pollCursor;
281
+ this.#generation += 1;
282
+ return this.#publish();
283
+ }
284
+ if (response.phase !== "poll") throw new DatabaseSubscriptionError("phase_mismatch");
285
+ const pollCursor = cursor(response.poll_cursor);
286
+ const unchanged = response.groups.every(group => group.effects.length === 0);
287
+ const rows = unchanged ? this.#rows : new Map(this.#rows);
288
+ let sequence = this.#sequence ?? 0n;
289
+ for (const group of response.groups) {
290
+ if (typeof group.sequence !== "bigint" || group.sequence <= sequence || group.sequence > 18_446_744_073_709_551_615n) {
291
+ throw new DatabaseSubscriptionError("group_order_invalid");
292
+ }
293
+ /** @type {Set<string>} */
294
+ const documents = new Set();
295
+ for (const effect of group.effects) {
296
+ const document = requireDatabaseUuid(effect.document, "document");
297
+ if (documents.has(document)) throw new DatabaseSubscriptionError("duplicate_group_document");
298
+ documents.add(document);
299
+ if (effect.point === null) rows.delete(document);
300
+ else rows.set(document, Object.freeze({ document, point: pointCopy(effect.point, this.binding.scope) }));
301
+ }
302
+ if (rows.size > this.#maximumRows) throw new DatabaseSubscriptionError("view_limit_exceeded");
303
+ // A filtered group may have no matching effects. Its sequence is still
304
+ // a valid advance; the signed response cursor must also be committed.
305
+ sequence = group.sequence;
306
+ }
307
+ this.#rows = rows;
308
+ this.#pollCursor = pollCursor;
309
+ if (response.groups.length > 0) this.#sequence = sequence;
310
+ return this.#publish(unchanged ? this.#snapshot?.rows : undefined);
311
+ }
312
+
313
+ /**
314
+ * @param {readonly DatabaseQueryRow[]} [rows]
315
+ * @returns {DatabaseViewSnapshot}
316
+ */
317
+ #publish(rows) {
318
+ this.#snapshot = Object.freeze({
319
+ rows: rows ?? Object.freeze([...this.#rows.values()].sort((left, right) => left.document < right.document ? -1 : left.document > right.document ? 1 : 0)),
320
+ position: Object.freeze({ generation: this.#generation, poll_cursor: /** @type {string} */ (this.#pollCursor), ...(this.#sequence === undefined ? {} : { sequence: this.#sequence }) }),
321
+ });
322
+ return this.#snapshot;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * @template T
328
+ * @param {Promise<T>} pending
329
+ * @param {AbortSignal} signal
330
+ * @returns {Promise<T>}
331
+ */
332
+ async function waitFor(pending, signal) {
333
+ signal.throwIfAborted();
334
+ return new Promise((resolve, reject) => {
335
+ /** @returns {void} */
336
+ const abort = () => reject(signal.reason);
337
+ signal.addEventListener("abort", abort, { once: true });
338
+ pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
339
+ });
340
+ }
341
+
342
+ /**
343
+ * @param {number} milliseconds
344
+ * @param {AbortSignal} signal
345
+ * @returns {Promise<void>}
346
+ */
347
+ async function delay(milliseconds, signal) {
348
+ signal.throwIfAborted();
349
+ return new Promise((resolve, reject) => {
350
+ /** @returns {void} */
351
+ const abort = () => { clearTimeout(timer); reject(signal.reason); };
352
+ const timer = setTimeout(() => { signal.removeEventListener("abort", abort); resolve(); }, milliseconds);
353
+ signal.addEventListener("abort", abort, { once: true });
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Native snapshot/poll source shared by React, Svelte and TanStack.
359
+ * @param {TerraBaseClient} client
360
+ * @param {DatabaseQueryOptions} options
361
+ * @returns {DatabaseQuerySource}
362
+ */
363
+ export function createDatabaseQuery(client, options) {
364
+ const template = new DatabaseSubscriptionView(client.scope, options);
365
+ const pollIntervalMs = boundedInteger(options.pollIntervalMs, 1_000, 60_000, "Poll interval");
366
+ const retryDelayMs = boundedInteger(options.retryDelayMs, 250, 60_000, "Retry delay");
367
+ const retryLimit = options.retryLimit ?? 2;
368
+ if (!Number.isInteger(retryLimit) || retryLimit < 0 || retryLimit > 10) throw new TypeError("Retry limit is outside its bounds.");
369
+ const capturedOptions = Object.freeze({ collection: template.binding.collection, predicate: template.binding.predicate, limit: template.binding.limit, ...(options.maximumRows === undefined ? {} : { maximumRows: options.maximumRows }) });
370
+ const binding = template.binding;
371
+ const requestKey = encodeDatabaseSubscription({ phase: "snapshot", predicate: binding.predicate, limit: binding.limit });
372
+ return Object.freeze({
373
+ key: Object.freeze(["terrabase-subscription", binding.scope.tenant, binding.scope.database, binding.scope.shard.toString(), binding.collection, requestKey]),
374
+ binding,
375
+ /**
376
+ * @param {DatabaseQueryCallbacks} observer
377
+ * @param {ReactiveSubscriptionContext} context
378
+ * @returns {() => void}
379
+ */
380
+ subscribe: (observer, context) => {
381
+ const abort = new AbortController();
382
+ const signal = AbortSignal.any([context.signal, abort.signal]);
383
+ const view = new DatabaseSubscriptionView(binding.scope, capturedOptions);
384
+ /** @returns {Promise<void>} */
385
+ const run = async () => {
386
+ while (!signal.aborted) {
387
+ const request = view.getRequest();
388
+ /** @type {DatabaseResult<DatabaseSubscriptionResponse>} */
389
+ let result;
390
+ let retries = 0;
391
+ while (true) {
392
+ result = await waitFor(client.subscription(binding.collection, request, { signal }), signal);
393
+ signal.throwIfAborted();
394
+ if (result.ok || !["transport_unavailable", "cancelled"].includes(result.error.code) || retries >= retryLimit) break;
395
+ retries += 1;
396
+ await delay(retryDelayMs * retries, signal);
397
+ }
398
+ if (!result.ok) {
399
+ if (result.error.status === 409 && result.error.code === "resync_required") {
400
+ view.restart();
401
+ observer.reset(Object.freeze({ reason: "resync_required", code: result.error.code }));
402
+ await delay(pollIntervalMs, signal);
403
+ continue;
404
+ }
405
+ throw new DatabaseSubscriptionError(result.error.code, result.error);
406
+ }
407
+ const snapshot = view.apply(result.value);
408
+ if (snapshot !== undefined) {
409
+ await waitFor(Promise.resolve(observer.next(snapshot.rows, snapshot.position)), signal);
410
+ await delay(pollIntervalMs, signal);
411
+ }
412
+ }
413
+ };
414
+ void run().catch(error => {
415
+ if (!signal.aborted) {
416
+ try { observer.error(error); } catch { /* consumer callback failed */ } finally { abort.abort(); }
417
+ }
418
+ });
419
+ return () => abort.abort();
420
+ },
421
+ });
422
+ }