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,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReactiveMutationObserver,
|
|
3
|
+
ReactiveQueryObserver,
|
|
4
|
+
} from "../react/core.js";
|
|
5
|
+
/**
|
|
6
|
+
* @import {
|
|
7
|
+
* ReactiveMutationOptions,
|
|
8
|
+
* ReactiveMutationState,
|
|
9
|
+
* ReactiveQueryOptions,
|
|
10
|
+
* ReactiveQuerySource,
|
|
11
|
+
* ReactiveQueryState,
|
|
12
|
+
* } from "../react/core.js"
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export * from "../react/core.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The Svelte store contract; `$store` auto-subscription binds to it directly.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {() => void} SvelteUnsubscriber
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @template TValue
|
|
25
|
+
* @typedef {(value: TValue) => void} SvelteSubscriber
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @template TValue
|
|
30
|
+
* @typedef {{
|
|
31
|
+
* readonly subscribe: (run: SvelteSubscriber<TValue>) => SvelteUnsubscriber;
|
|
32
|
+
* }} SvelteReadable
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A query store; `refetch` restarts delivery from a fresh generation, like
|
|
37
|
+
* the React `refetch`.
|
|
38
|
+
*
|
|
39
|
+
* @template TValue
|
|
40
|
+
* @typedef {SvelteReadable<ReactiveQueryState<TValue>> & {
|
|
41
|
+
* readonly refetch: () => void;
|
|
42
|
+
* }} SvelteQuery
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One reactive query as a Svelte store. The observer lifecycle follows the
|
|
47
|
+
* store subscription count, so `$query` in a component starts delivery on
|
|
48
|
+
* mount and disposes the transport on unmount. Keep the source object stable
|
|
49
|
+
* between renders.
|
|
50
|
+
*
|
|
51
|
+
* @template TValue
|
|
52
|
+
* @param {ReactiveQuerySource<TValue>} source
|
|
53
|
+
* @param {ReactiveQueryOptions<TValue>} [options]
|
|
54
|
+
* @returns {SvelteQuery<TValue>}
|
|
55
|
+
*/
|
|
56
|
+
export function createQuery(source, options = {}) {
|
|
57
|
+
/** @type {ReactiveQueryObserver<TValue>} */
|
|
58
|
+
const observer = new ReactiveQueryObserver(source, options);
|
|
59
|
+
/**
|
|
60
|
+
* @param {SvelteSubscriber<ReactiveQueryState<TValue>>} run
|
|
61
|
+
* @returns {SvelteUnsubscriber}
|
|
62
|
+
*/
|
|
63
|
+
const subscribe = run => {
|
|
64
|
+
const stop = observer.subscribe(() => run(observer.getSnapshot()));
|
|
65
|
+
run(observer.getSnapshot());
|
|
66
|
+
return stop;
|
|
67
|
+
};
|
|
68
|
+
/** @returns {void} */
|
|
69
|
+
const refetch = () => {
|
|
70
|
+
observer.refetch();
|
|
71
|
+
};
|
|
72
|
+
return Object.freeze({ subscribe, refetch });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @template TValue, TVariables
|
|
77
|
+
* @typedef {SvelteReadable<ReactiveMutationState<TValue>> & {
|
|
78
|
+
* readonly mutate: (variables: TVariables) => void;
|
|
79
|
+
* readonly mutateAsync: (variables: TVariables) => Promise<TValue>;
|
|
80
|
+
* readonly cancel: () => void;
|
|
81
|
+
* readonly reset: () => void;
|
|
82
|
+
* }} SvelteMutation
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A durable mutation as a Svelte store; `$mutation.isPending` drives the UI.
|
|
87
|
+
*
|
|
88
|
+
* @template TValue
|
|
89
|
+
* @template [TVariables=void]
|
|
90
|
+
* @param {ReactiveMutationOptions<TValue, TVariables>} options
|
|
91
|
+
* @returns {SvelteMutation<TValue, TVariables>}
|
|
92
|
+
*/
|
|
93
|
+
export function createMutation(options) {
|
|
94
|
+
/** @type {ReactiveMutationObserver<TValue, TVariables>} */
|
|
95
|
+
const observer = new ReactiveMutationObserver(options);
|
|
96
|
+
let subscribers = 0;
|
|
97
|
+
/**
|
|
98
|
+
* @param {SvelteSubscriber<ReactiveMutationState<TValue>>} run
|
|
99
|
+
* @returns {SvelteUnsubscriber}
|
|
100
|
+
*/
|
|
101
|
+
const subscribe = run => {
|
|
102
|
+
subscribers += 1;
|
|
103
|
+
const stop = observer.subscribe(() => run(observer.getSnapshot()));
|
|
104
|
+
run(observer.getSnapshot());
|
|
105
|
+
let active = true;
|
|
106
|
+
return () => {
|
|
107
|
+
if (!active) return;
|
|
108
|
+
active = false;
|
|
109
|
+
stop();
|
|
110
|
+
subscribers -= 1;
|
|
111
|
+
if (subscribers === 0) observer.cancel();
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* @param {TVariables} variables
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
const mutate = variables => observer.mutate(variables);
|
|
119
|
+
/**
|
|
120
|
+
* @param {TVariables} variables
|
|
121
|
+
* @returns {Promise<TValue>}
|
|
122
|
+
*/
|
|
123
|
+
const mutateAsync = variables => observer.mutateAsync(variables);
|
|
124
|
+
/** @returns {void} */
|
|
125
|
+
const cancel = () => observer.cancel();
|
|
126
|
+
/** @returns {void} */
|
|
127
|
+
const reset = () => observer.reset();
|
|
128
|
+
return Object.freeze({ subscribe, mutate, mutateAsync, cancel, reset });
|
|
129
|
+
}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { createDatabaseQuery, DatabaseSubscriptionError } from "../database-view.js";
|
|
2
|
+
/** @import { DatabaseQueryOptions, DatabaseViewPosition, DatabaseViewSnapshot } from "../database-view.js" */
|
|
3
|
+
/** @import { PreparedDatabaseCommit, TerraBaseClient } from "../database.js" */
|
|
4
|
+
/** @import { DatabaseCommitReceipt, DatabaseQueryRow, DatabaseResult, DatabaseScope } from "../database-types.js" */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Structural TanStack DB callbacks keep this optional integration dependency free.
|
|
8
|
+
*
|
|
9
|
+
* @template {object} T
|
|
10
|
+
* @template {string | number} TKey
|
|
11
|
+
* @typedef {{ readonly type: "insert" | "update"; readonly value: T }
|
|
12
|
+
* | { readonly type: "delete"; readonly key: TKey }} TerraScaleTanStackChange
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @template {object} T
|
|
17
|
+
* @template {string | number} TKey
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* readonly collection: unknown;
|
|
20
|
+
* readonly begin: (options?: { readonly immediate?: boolean }) => void;
|
|
21
|
+
* readonly write: (message: TerraScaleTanStackChange<T, TKey>) => void;
|
|
22
|
+
* readonly commit: (signal?: AbortSignal) => true | Promise<void>;
|
|
23
|
+
* readonly markReady: () => void;
|
|
24
|
+
* readonly markError: (error?: unknown) => void;
|
|
25
|
+
* readonly truncate: () => void;
|
|
26
|
+
* }} TerraScaleTanStackSyncParams
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @template {object} [T=Record<string, unknown>]
|
|
31
|
+
* @typedef {{
|
|
32
|
+
* readonly mutationId?: string;
|
|
33
|
+
* readonly key: string | number;
|
|
34
|
+
* readonly original?: Partial<T>;
|
|
35
|
+
* readonly modified: T;
|
|
36
|
+
* readonly changes?: Partial<T>;
|
|
37
|
+
* readonly type: "insert" | "update" | "delete";
|
|
38
|
+
* readonly metadata?: unknown;
|
|
39
|
+
* }} TerraScaleTanStackMutation
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @template {object} [T=Record<string, unknown>]
|
|
44
|
+
* @typedef {{
|
|
45
|
+
* readonly id?: string;
|
|
46
|
+
* readonly mutations: readonly TerraScaleTanStackMutation<T>[];
|
|
47
|
+
* readonly metadata?: Record<string, unknown>;
|
|
48
|
+
* }} TerraScaleTanStackTransaction
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @template {object} T
|
|
53
|
+
* @template {string | number} TKey
|
|
54
|
+
* @typedef {{
|
|
55
|
+
* readonly operation: "insert" | "update" | "delete";
|
|
56
|
+
* readonly idempotencyKey: string;
|
|
57
|
+
* readonly transactionId: string;
|
|
58
|
+
* readonly transaction: TerraScaleTanStackTransaction<T>;
|
|
59
|
+
* readonly scope: DatabaseScope;
|
|
60
|
+
* readonly collection: string;
|
|
61
|
+
* readonly signal: AbortSignal;
|
|
62
|
+
* readonly keyOf: (item: T) => TKey;
|
|
63
|
+
* }} TerraScaleTanStackMutationRequest
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @template {object} T
|
|
68
|
+
* @template {string | number} [TKey=string]
|
|
69
|
+
* @typedef {{
|
|
70
|
+
* readonly id?: string;
|
|
71
|
+
* readonly getKey: (item: T) => TKey;
|
|
72
|
+
* readonly sync: {
|
|
73
|
+
* readonly rowUpdateMode: "full";
|
|
74
|
+
* readonly sync: (params: TerraScaleTanStackSyncParams<T, TKey>) => () => void;
|
|
75
|
+
* };
|
|
76
|
+
* readonly onInsert: (params: { readonly transaction: TerraScaleTanStackTransaction<T> }) => Promise<DatabaseCommitReceipt>;
|
|
77
|
+
* readonly onUpdate: (params: { readonly transaction: TerraScaleTanStackTransaction<T> }) => Promise<DatabaseCommitReceipt>;
|
|
78
|
+
* readonly onDelete: (params: { readonly transaction: TerraScaleTanStackTransaction<T> }) => Promise<DatabaseCommitReceipt>;
|
|
79
|
+
* }} TerraScaleTanStackCollectionOptions
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `mapRow` maps the complete native point explicitly; conflicts are never
|
|
84
|
+
* flattened. `prepareMutation` prepares through this same client. The adapter
|
|
85
|
+
* sends the resulting commit to the server once; TanStack owns its reversible
|
|
86
|
+
* optimistic presentation.
|
|
87
|
+
*
|
|
88
|
+
* @template {object} T
|
|
89
|
+
* @template {string | number} [TKey=string]
|
|
90
|
+
* @typedef {DatabaseQueryOptions & {
|
|
91
|
+
* readonly client: TerraBaseClient;
|
|
92
|
+
* readonly mapRow: (row: DatabaseQueryRow) => T;
|
|
93
|
+
* readonly getKey: (item: T) => TKey;
|
|
94
|
+
* readonly id?: string;
|
|
95
|
+
* readonly prepareMutation?: (request: TerraScaleTanStackMutationRequest<T, TKey>) => Promise<PreparedDatabaseCommit>;
|
|
96
|
+
* readonly idempotencyKey?: (transaction: TerraScaleTanStackTransaction<T>) => string;
|
|
97
|
+
* readonly signal?: AbortSignal;
|
|
98
|
+
* }} TerraScaleTanStackAdapterOptions
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {unknown} value
|
|
103
|
+
* @param {Set<object>} [seen]
|
|
104
|
+
* @returns {unknown}
|
|
105
|
+
*/
|
|
106
|
+
function canonical(value, seen = new Set()) {
|
|
107
|
+
if (typeof value === "bigint") return ["bigint", value.toString()];
|
|
108
|
+
if (value === undefined) return ["undefined"];
|
|
109
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return [typeof value, value];
|
|
110
|
+
if (typeof value === "number" && Number.isFinite(value)) return ["number", value];
|
|
111
|
+
if (value instanceof Uint8Array) return ["bytes", [...value]];
|
|
112
|
+
if (typeof value !== "object" || value === null || seen.has(value)) throw new TypeError("TanStack values must be acyclic data.");
|
|
113
|
+
seen.add(value);
|
|
114
|
+
try {
|
|
115
|
+
if (Array.isArray(value)) return ["array", value.map(entry => canonical(entry, seen))];
|
|
116
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("TanStack values must contain plain objects.");
|
|
117
|
+
return ["object", Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, canonical(entry, seen)])];
|
|
118
|
+
} finally { seen.delete(value); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {unknown} value
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function fingerprint(value) { return JSON.stringify(canonical(value)); }
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {Promise<void>} pending
|
|
129
|
+
* @param {AbortSignal} signal
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
async function acknowledged(pending, signal) {
|
|
133
|
+
signal.throwIfAborted();
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
/** @returns {void} */
|
|
136
|
+
const abort = () => reject(signal.reason);
|
|
137
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
138
|
+
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every complete native cut owns its mapped values until collection acknowledgment.
|
|
144
|
+
*
|
|
145
|
+
* @template {object} T
|
|
146
|
+
* @template {string | number} [TKey=string]
|
|
147
|
+
*/
|
|
148
|
+
export class TerraScaleTanStackCollectionPublisher {
|
|
149
|
+
/** @readonly @type {TerraScaleTanStackSyncParams<T, TKey>} */
|
|
150
|
+
#params;
|
|
151
|
+
/** @readonly @type {(row: DatabaseQueryRow) => T} */
|
|
152
|
+
#mapRow;
|
|
153
|
+
/** @readonly @type {(item: T) => TKey} */
|
|
154
|
+
#getKey;
|
|
155
|
+
/** @readonly */
|
|
156
|
+
#abort = new AbortController();
|
|
157
|
+
/** @type {Map<string, { readonly value: T; readonly key: TKey; readonly fingerprint: string }>} */
|
|
158
|
+
#published = new Map();
|
|
159
|
+
/** @type {DatabaseViewPosition | undefined} */
|
|
160
|
+
#position;
|
|
161
|
+
#ready = false;
|
|
162
|
+
#needsReset = true;
|
|
163
|
+
#epoch = 0;
|
|
164
|
+
/** @type {Promise<void>} */
|
|
165
|
+
#tail = Promise.resolve();
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {TerraScaleTanStackSyncParams<T, TKey>} params
|
|
169
|
+
* @param {(row: DatabaseQueryRow) => T} mapRow
|
|
170
|
+
* @param {(item: T) => TKey} getKey
|
|
171
|
+
*/
|
|
172
|
+
constructor(params, mapRow, getKey) {
|
|
173
|
+
this.#params = params;
|
|
174
|
+
this.#mapRow = mapRow;
|
|
175
|
+
this.#getKey = getKey;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @returns {DatabaseViewPosition | undefined} */
|
|
179
|
+
getPosition() { return this.#position; }
|
|
180
|
+
|
|
181
|
+
/** @returns {void} */
|
|
182
|
+
reset() {
|
|
183
|
+
this.#epoch += 1;
|
|
184
|
+
this.#position = undefined;
|
|
185
|
+
this.#ready = false;
|
|
186
|
+
this.#needsReset = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @returns {void} */
|
|
190
|
+
dispose() { this.#abort.abort(); }
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {DatabaseViewSnapshot} snapshot
|
|
194
|
+
* @param {AbortSignal} [externalSignal]
|
|
195
|
+
* @returns {Promise<void>}
|
|
196
|
+
*/
|
|
197
|
+
publish(snapshot, externalSignal) {
|
|
198
|
+
const signal = AbortSignal.any([this.#abort.signal, ...(externalSignal === undefined ? [] : [externalSignal])]);
|
|
199
|
+
/** @type {Map<string, { readonly value: T; readonly key: TKey; readonly fingerprint: string }>} */
|
|
200
|
+
let values;
|
|
201
|
+
const epoch = this.#epoch;
|
|
202
|
+
const position = Object.freeze({ ...snapshot.position });
|
|
203
|
+
try {
|
|
204
|
+
signal.throwIfAborted();
|
|
205
|
+
values = new Map();
|
|
206
|
+
/** @type {Set<TKey>} */
|
|
207
|
+
const keys = new Set();
|
|
208
|
+
for (const row of snapshot.rows) {
|
|
209
|
+
const value = structuredClone(this.#mapRow(row));
|
|
210
|
+
const key = this.#getKey(structuredClone(value));
|
|
211
|
+
if ((typeof key !== "string" && typeof key !== "number") || (typeof key === "number" && !Number.isFinite(key)) || keys.has(key) || values.has(row.document)) {
|
|
212
|
+
throw new TypeError("TanStack rows require distinct string or finite number keys.");
|
|
213
|
+
}
|
|
214
|
+
keys.add(key);
|
|
215
|
+
values.set(row.document, { value, key, fingerprint: fingerprint(value) });
|
|
216
|
+
}
|
|
217
|
+
} catch (error) { return Promise.reject(error); }
|
|
218
|
+
const run = this.#tail.then(async () => {
|
|
219
|
+
signal.throwIfAborted();
|
|
220
|
+
if (epoch !== this.#epoch) throw new DOMException("The native publication was reset", "AbortError");
|
|
221
|
+
const params = this.#params;
|
|
222
|
+
try {
|
|
223
|
+
params.begin();
|
|
224
|
+
if (this.#needsReset || this.#position?.generation !== position.generation) {
|
|
225
|
+
params.truncate();
|
|
226
|
+
for (const row of values.values()) params.write({ type: "insert", value: structuredClone(row.value) });
|
|
227
|
+
} else {
|
|
228
|
+
for (const [document, previous] of this.#published) {
|
|
229
|
+
const next = values.get(document);
|
|
230
|
+
if (next === undefined || next.key !== previous.key) params.write({ type: "delete", key: previous.key });
|
|
231
|
+
}
|
|
232
|
+
for (const [document, row] of values) {
|
|
233
|
+
const previous = this.#published.get(document);
|
|
234
|
+
if (previous?.fingerprint === row.fingerprint && previous.key === row.key) continue;
|
|
235
|
+
params.write({ type: previous === undefined || previous.key !== row.key ? "insert" : "update", value: structuredClone(row.value) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const receipt = params.commit(signal);
|
|
239
|
+
if (receipt !== true) await acknowledged(receipt, signal);
|
|
240
|
+
signal.throwIfAborted();
|
|
241
|
+
if (epoch !== this.#epoch) throw new DOMException("The native publication was reset", "AbortError");
|
|
242
|
+
this.#published = values;
|
|
243
|
+
this.#position = position;
|
|
244
|
+
this.#needsReset = false;
|
|
245
|
+
if (!this.#ready) { this.#ready = true; params.markReady(); }
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.#needsReset = true;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
this.#tail = run.then(() => undefined, () => undefined);
|
|
252
|
+
return run;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export class TerraScaleTanStackMutationError extends Error {
|
|
257
|
+
/** @override @readonly */
|
|
258
|
+
name = "TerraScaleTanStackMutationError";
|
|
259
|
+
/** @readonly @type {Extract<DatabaseResult<DatabaseCommitReceipt>, { readonly ok: false }>["error"]} */
|
|
260
|
+
detail;
|
|
261
|
+
/** @returns {string} */
|
|
262
|
+
get code() { return this.detail.code; }
|
|
263
|
+
/**
|
|
264
|
+
* @param {Extract<DatabaseResult<DatabaseCommitReceipt>, { readonly ok: false }>["error"]} detail
|
|
265
|
+
*/
|
|
266
|
+
constructor(detail) {
|
|
267
|
+
super(`TerraBase mutation: ${detail.code}; outcome ${detail.mutation_outcome ?? "unknown"}`, { cause: detail });
|
|
268
|
+
this.detail = detail;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Native HTTP snapshot/poll collection adapter. All durable writes reach the server.
|
|
274
|
+
*
|
|
275
|
+
* @template {object} T
|
|
276
|
+
* @template {string | number} [TKey=string]
|
|
277
|
+
*/
|
|
278
|
+
export class TerraScaleTanStackCollectionSync {
|
|
279
|
+
/** @readonly @type {TerraScaleTanStackAdapterOptions<T, TKey>} */
|
|
280
|
+
#options;
|
|
281
|
+
/** @readonly @type {ReturnType<typeof createDatabaseQuery>} */
|
|
282
|
+
#query;
|
|
283
|
+
/** @readonly */
|
|
284
|
+
#abort = new AbortController();
|
|
285
|
+
/** @readonly @type {Map<string, { readonly fingerprint: string; readonly promise: Promise<DatabaseCommitReceipt> }>} */
|
|
286
|
+
#mutations = new Map();
|
|
287
|
+
/** @readonly @type {TerraScaleTanStackCollectionOptions<T, TKey>} */
|
|
288
|
+
#collectionOptions;
|
|
289
|
+
/** @type {TerraScaleTanStackCollectionPublisher<T, TKey> | undefined} */
|
|
290
|
+
#publisher;
|
|
291
|
+
/** @type {AbortController | undefined} */
|
|
292
|
+
#sessionAbort;
|
|
293
|
+
/** @type {AbortController | undefined} */
|
|
294
|
+
#deliveryAbort;
|
|
295
|
+
/** @type {(() => void) | undefined} */
|
|
296
|
+
#stopDelivery;
|
|
297
|
+
/** @type {(() => void) | undefined} */
|
|
298
|
+
#removeExternalAbort;
|
|
299
|
+
/** @type {Promise<void>} */
|
|
300
|
+
#mutationTail = Promise.resolve();
|
|
301
|
+
#online = false;
|
|
302
|
+
/** @type {(error: unknown) => void} */
|
|
303
|
+
#paramsError = () => undefined;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {TerraScaleTanStackAdapterOptions<T, TKey>} options
|
|
307
|
+
*/
|
|
308
|
+
constructor(options) {
|
|
309
|
+
this.#options = { ...options };
|
|
310
|
+
this.#query = createDatabaseQuery(options.client, options);
|
|
311
|
+
this.#collectionOptions = Object.freeze({
|
|
312
|
+
...(options.id === undefined ? {} : { id: options.id }),
|
|
313
|
+
getKey: options.getKey,
|
|
314
|
+
sync: Object.freeze({ rowUpdateMode: /** @type {const} */ ("full"), sync: /** @param {TerraScaleTanStackSyncParams<T, TKey>} params */ params => this.#start(params) }),
|
|
315
|
+
onInsert: /** @param {{ readonly transaction: TerraScaleTanStackTransaction<T> }} params */ params => this.#mutate("insert", params.transaction),
|
|
316
|
+
onUpdate: /** @param {{ readonly transaction: TerraScaleTanStackTransaction<T> }} params */ params => this.#mutate("update", params.transaction),
|
|
317
|
+
onDelete: /** @param {{ readonly transaction: TerraScaleTanStackTransaction<T> }} params */ params => this.#mutate("delete", params.transaction),
|
|
318
|
+
});
|
|
319
|
+
if (options.signal !== undefined) {
|
|
320
|
+
if (options.signal.aborted) this.dispose();
|
|
321
|
+
else {
|
|
322
|
+
/** @returns {void} */
|
|
323
|
+
const abort = () => this.dispose();
|
|
324
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
325
|
+
this.#removeExternalAbort = () => options.signal?.removeEventListener("abort", abort);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** @returns {TerraScaleTanStackCollectionOptions<T, TKey>} */
|
|
331
|
+
get options() { return this.#collectionOptions; }
|
|
332
|
+
/** @returns {string | undefined} */
|
|
333
|
+
getCheckpoint() { return this.#publisher?.getPosition()?.poll_cursor; }
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Reconnect correctness starts with a fresh, complete authorized snapshot.
|
|
337
|
+
*
|
|
338
|
+
* @returns {void}
|
|
339
|
+
*/
|
|
340
|
+
resubscribe() {
|
|
341
|
+
this.#assertActive();
|
|
342
|
+
const publisher = this.#publisher;
|
|
343
|
+
if (publisher === undefined) throw new Error("The collection sync callback has not started.");
|
|
344
|
+
this.#stop();
|
|
345
|
+
if (this.#publisher !== publisher || this.#deliveryAbort !== undefined || this.#abort.signal.aborted) return;
|
|
346
|
+
publisher.reset();
|
|
347
|
+
this.#open();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** @returns {void} */
|
|
351
|
+
dispose() {
|
|
352
|
+
if (this.#abort.signal.aborted) return;
|
|
353
|
+
this.#abort.abort();
|
|
354
|
+
this.#endSession();
|
|
355
|
+
this.#removeExternalAbort?.();
|
|
356
|
+
this.#removeExternalAbort = undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {TerraScaleTanStackSyncParams<T, TKey>} params
|
|
361
|
+
* @returns {() => void}
|
|
362
|
+
*/
|
|
363
|
+
#start(params) {
|
|
364
|
+
this.#assertActive();
|
|
365
|
+
if (this.#publisher !== undefined) throw new Error("The collection sync callback already started.");
|
|
366
|
+
const publisher = new TerraScaleTanStackCollectionPublisher(params, this.#options.mapRow, this.#options.getKey);
|
|
367
|
+
this.#publisher = publisher;
|
|
368
|
+
this.#sessionAbort = new AbortController();
|
|
369
|
+
this.#paramsError = error => params.markError(error);
|
|
370
|
+
this.#open();
|
|
371
|
+
// TanStack cleanup releases one sync session; the collection may restart.
|
|
372
|
+
return () => { if (this.#publisher === publisher) this.#endSession(); };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** @returns {void} */
|
|
376
|
+
#endSession() {
|
|
377
|
+
const publisher = this.#publisher;
|
|
378
|
+
const session = this.#sessionAbort;
|
|
379
|
+
this.#publisher = undefined;
|
|
380
|
+
this.#sessionAbort = undefined;
|
|
381
|
+
this.#paramsError = () => undefined;
|
|
382
|
+
this.#stop();
|
|
383
|
+
session?.abort();
|
|
384
|
+
publisher?.dispose();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** @returns {void} */
|
|
388
|
+
#open() {
|
|
389
|
+
const publisher = this.#publisher;
|
|
390
|
+
const session = this.#sessionAbort;
|
|
391
|
+
if (publisher === undefined || session === undefined) throw new Error("The collection sync callback has not started.");
|
|
392
|
+
const delivery = new AbortController();
|
|
393
|
+
this.#deliveryAbort = delivery;
|
|
394
|
+
const signal = AbortSignal.any([this.#abort.signal, session.signal, delivery.signal]);
|
|
395
|
+
this.#stopDelivery = this.#query.subscribe({
|
|
396
|
+
next: async (rows, position) => {
|
|
397
|
+
await publisher.publish({ rows, position }, signal);
|
|
398
|
+
if (!signal.aborted) this.#online = true;
|
|
399
|
+
},
|
|
400
|
+
reset: reset => {
|
|
401
|
+
if (signal.aborted) return;
|
|
402
|
+
this.#online = false;
|
|
403
|
+
publisher.reset();
|
|
404
|
+
this.#reportError(new DatabaseSubscriptionError(reset.code ?? reset.reason));
|
|
405
|
+
},
|
|
406
|
+
error: error => {
|
|
407
|
+
if (signal.aborted) return;
|
|
408
|
+
this.#online = false;
|
|
409
|
+
this.#reportError(error);
|
|
410
|
+
},
|
|
411
|
+
}, { signal });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** @returns {void} */
|
|
415
|
+
#stop() {
|
|
416
|
+
this.#online = false;
|
|
417
|
+
const delivery = this.#deliveryAbort;
|
|
418
|
+
const stop = this.#stopDelivery;
|
|
419
|
+
this.#deliveryAbort = undefined;
|
|
420
|
+
this.#stopDelivery = undefined;
|
|
421
|
+
delivery?.abort();
|
|
422
|
+
stop?.();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {unknown} error
|
|
427
|
+
* @returns {void}
|
|
428
|
+
*/
|
|
429
|
+
#reportError(error) {
|
|
430
|
+
try { this.#paramsError(error); } catch { /* consumer callback failed */ }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* @param {"insert" | "update" | "delete"} operation
|
|
435
|
+
* @param {TerraScaleTanStackTransaction<T>} transaction
|
|
436
|
+
* @returns {Promise<DatabaseCommitReceipt>}
|
|
437
|
+
*/
|
|
438
|
+
async #mutate(operation, transaction) {
|
|
439
|
+
this.#assertActive();
|
|
440
|
+
const transactionId = transaction.id ?? transaction.mutations[0]?.mutationId;
|
|
441
|
+
if (transactionId === undefined || transactionId.length === 0 || transactionId.length > 128) throw new TypeError("TanStack transaction requires a bounded stable identity.");
|
|
442
|
+
if (transaction.mutations.length === 0 || transaction.mutations.length > 128 || transaction.mutations.some(mutation => mutation.type !== operation)) throw new TypeError("TanStack transaction operations are empty, mixed, or outside their bound.");
|
|
443
|
+
// TanStack's real Transaction carries functions, collection instances and
|
|
444
|
+
// deferred receipts. Capture only the declared mutation data, never the
|
|
445
|
+
// framework transaction object or its runtime ownership graph.
|
|
446
|
+
/** @type {TerraScaleTanStackTransaction<T>} */
|
|
447
|
+
const captured = structuredClone({
|
|
448
|
+
...(transaction.id === undefined ? {} : { id: transaction.id }),
|
|
449
|
+
...(transaction.metadata === undefined ? {} : { metadata: transaction.metadata }),
|
|
450
|
+
mutations: transaction.mutations.map(mutation => ({
|
|
451
|
+
key: mutation.key, type: mutation.type, modified: mutation.modified,
|
|
452
|
+
...(mutation.mutationId === undefined ? {} : { mutationId: mutation.mutationId }),
|
|
453
|
+
...(mutation.original === undefined ? {} : { original: mutation.original }),
|
|
454
|
+
...(mutation.changes === undefined ? {} : { changes: mutation.changes }),
|
|
455
|
+
...(mutation.metadata === undefined ? {} : { metadata: mutation.metadata }),
|
|
456
|
+
})),
|
|
457
|
+
});
|
|
458
|
+
const digest = fingerprint({ operation, transaction: captured });
|
|
459
|
+
const existing = this.#mutations.get(transactionId);
|
|
460
|
+
if (existing !== undefined) {
|
|
461
|
+
if (existing.fingerprint !== digest) throw new TypeError("The transaction identity was reused with different effects.");
|
|
462
|
+
return existing.promise;
|
|
463
|
+
}
|
|
464
|
+
if (!this.#online) throw new Error("Durable mutations require a current native subscription.");
|
|
465
|
+
const prepareMutation = this.#options.prepareMutation;
|
|
466
|
+
if (prepareMutation === undefined) throw new Error("This collection is read only.");
|
|
467
|
+
if (this.#mutations.size >= 1024) throw new Error("The adapter's retained mutation identity limit was reached.");
|
|
468
|
+
const idempotencyKey = this.#options.idempotencyKey?.(structuredClone(captured)) ?? transactionId;
|
|
469
|
+
if (idempotencyKey.length === 0 || idempotencyKey.length > 128) throw new TypeError("Idempotency identity is outside its bound.");
|
|
470
|
+
const session = this.#sessionAbort;
|
|
471
|
+
if (session === undefined) throw new Error("Durable mutations require a current native subscription.");
|
|
472
|
+
const signal = AbortSignal.any([this.#abort.signal, session.signal]);
|
|
473
|
+
const run = this.#mutationTail.then(async () => {
|
|
474
|
+
this.#assertActive();
|
|
475
|
+
signal.throwIfAborted();
|
|
476
|
+
if (!this.#online) throw new Error("Durable mutations require a current native subscription.");
|
|
477
|
+
const prepared = await prepareMutation({
|
|
478
|
+
operation, idempotencyKey, transactionId, transaction: captured,
|
|
479
|
+
scope: this.#query.binding.scope, collection: this.#query.binding.collection,
|
|
480
|
+
signal, keyOf: this.#options.getKey,
|
|
481
|
+
});
|
|
482
|
+
signal.throwIfAborted();
|
|
483
|
+
const result = await this.#options.client.commit(prepared, { signal });
|
|
484
|
+
if (!result.ok) throw new TerraScaleTanStackMutationError(result.error);
|
|
485
|
+
this.#assertActive();
|
|
486
|
+
signal.throwIfAborted();
|
|
487
|
+
return result.value;
|
|
488
|
+
});
|
|
489
|
+
this.#mutations.set(transactionId, { fingerprint: digest, promise: run });
|
|
490
|
+
this.#mutationTail = run.then(() => undefined, () => undefined);
|
|
491
|
+
return run;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** @returns {void} */
|
|
495
|
+
#assertActive() {
|
|
496
|
+
if (this.#abort.signal.aborted) throw new DOMException("The TerraScale TanStack adapter was disposed", "AbortError");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Options for `createCollection(terrascaleCollectionOptions(...))`.
|
|
502
|
+
*
|
|
503
|
+
* @template {object} T
|
|
504
|
+
* @template {string | number} [TKey=string]
|
|
505
|
+
* @param {TerraScaleTanStackAdapterOptions<T, TKey>} options
|
|
506
|
+
* @returns {TerraScaleTanStackCollectionOptions<T, TKey> & { readonly adapter: TerraScaleTanStackCollectionSync<T, TKey> }}
|
|
507
|
+
*/
|
|
508
|
+
export function terrascaleCollectionOptions(options) {
|
|
509
|
+
const adapter = new TerraScaleTanStackCollectionSync(options);
|
|
510
|
+
return Object.freeze({ ...adapter.options, adapter });
|
|
511
|
+
}
|