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,743 @@
1
+ /** @import { TerraScaleError, TerraScaleResult } from "../result.js" */
2
+ /** @import { DatabaseResult } from "../database-types.js" */
3
+
4
+ export {
5
+ createDatabaseQuery,
6
+ DatabaseSubscriptionError,
7
+ DatabaseSubscriptionView,
8
+ } from "../database-view.js";
9
+
10
+ /** @typedef {import("../database-view.js").DatabaseQueryCallbacks} DatabaseQueryCallbacks */
11
+ /** @typedef {import("../database-view.js").DatabaseQueryOptions} DatabaseQueryOptions */
12
+ /** @typedef {import("../database-view.js").DatabaseQuerySource} DatabaseQuerySource */
13
+ /** @typedef {import("../database-view.js").DatabaseViewBinding} DatabaseViewBinding */
14
+ /** @typedef {import("../database-view.js").DatabaseViewPosition} DatabaseViewPosition */
15
+ /** @typedef {import("../database-view.js").DatabaseViewSnapshot} DatabaseViewSnapshot */
16
+
17
+ /**
18
+ * A subscription cleanup returned by a typed reactive transport.
19
+ *
20
+ * @typedef {(() => void) | { readonly dispose: () => void } | { readonly unsubscribe: () => void }} ReactiveDisposer
21
+ */
22
+
23
+ /**
24
+ * @typedef {{
25
+ * readonly signal: AbortSignal;
26
+ * }} ReactiveSubscriptionContext
27
+ */
28
+
29
+ /**
30
+ * @template TValue
31
+ * @typedef {{
32
+ * readonly next: (value: TValue) => void;
33
+ * readonly error: (error: unknown) => void;
34
+ * readonly reset: (reset: ReactiveReset<TValue>) => void;
35
+ * }} ReactiveQueryCallbacks
36
+ */
37
+
38
+ /**
39
+ * The operation owned by the public reactive contract. It may use HTTP
40
+ * long-polling or WebSocket delivery; the hook does not know or care which.
41
+ *
42
+ * @template TValue
43
+ * @typedef {{
44
+ * readonly key?: string | readonly unknown[];
45
+ * readonly subscribe: (
46
+ * observer: ReactiveQueryCallbacks<TValue>,
47
+ * context: ReactiveSubscriptionContext,
48
+ * ) => ReactiveDisposer | void | Promise<ReactiveDisposer | void>;
49
+ * }} ReactiveQuerySource
50
+ */
51
+
52
+ /**
53
+ * @typedef {"checkpoint-expired"
54
+ * | "retention-advanced"
55
+ * | "generation-changed"
56
+ * | "checkpoint-not-current"
57
+ * | "view-diverged"
58
+ * | "oversized"
59
+ * | "invalid-operations"
60
+ * | "invalid-checkpoint"
61
+ * | "backpressure"
62
+ * | (string & {})} ReactiveResetReason
63
+ */
64
+
65
+ /**
66
+ * A server-authorized reset. The checkpoint remains opaque to the hook.
67
+ *
68
+ * @template TValue
69
+ * @typedef {{
70
+ * readonly reason: ReactiveResetReason;
71
+ * readonly data?: TValue;
72
+ * readonly checkpoint?: string | Uint8Array;
73
+ * readonly code?: string;
74
+ * readonly message?: string;
75
+ * }} ReactiveReset
76
+ */
77
+
78
+ /** @typedef {"subscription" | "transport" | "remote" | "cancelled" | "mutation"} ReactiveErrorKind */
79
+
80
+ /**
81
+ * @typedef {{
82
+ * readonly kind: ReactiveErrorKind;
83
+ * readonly code: string;
84
+ * readonly message: string;
85
+ * readonly cause?: unknown;
86
+ * readonly terraScale?: TerraScaleError;
87
+ * readonly database?: Extract<DatabaseResult<never>, { readonly ok: false }>["error"];
88
+ * }} ReactiveError
89
+ */
90
+
91
+ /** @typedef {"pending" | "success" | "error" | "reset"} ReactiveQueryStatus */
92
+
93
+ /**
94
+ * @template TValue
95
+ * @typedef {{
96
+ * readonly status: ReactiveQueryStatus;
97
+ * readonly data: TValue | undefined;
98
+ * readonly error: ReactiveError | undefined;
99
+ * readonly reset: ReactiveReset<TValue> | undefined;
100
+ * readonly isLoading: boolean;
101
+ * readonly isFetching: boolean;
102
+ * readonly isStale: boolean;
103
+ * }} ReactiveQueryState
104
+ */
105
+
106
+ /**
107
+ * @template TValue
108
+ * @typedef {{
109
+ * readonly enabled?: boolean;
110
+ * readonly initialData?: TValue;
111
+ * }} ReactiveQueryOptions
112
+ */
113
+
114
+ /**
115
+ * Framework-neutral query observer used by useQuery and SSR-safe tests.
116
+ *
117
+ * @template TValue
118
+ */
119
+ export class ReactiveQueryObserver {
120
+ /** @readonly @type {ReactiveQuerySource<TValue>} */
121
+ #source;
122
+ /** @type {boolean} */
123
+ #enabled;
124
+ /** @readonly @type {Set<() => void>} */
125
+ #listeners = new Set();
126
+ /** @readonly @type {ReactiveQueryState<TValue>} */
127
+ #serverSnapshot;
128
+ /** @type {ReactiveQueryState<TValue>} */
129
+ #state;
130
+ /** @type {AbortController | undefined} */
131
+ #abort;
132
+ /** @type {(() => void) | undefined} */
133
+ #cleanup;
134
+ #generation = 0;
135
+ #started = false;
136
+
137
+ /**
138
+ * @param {ReactiveQuerySource<TValue>} source
139
+ * @param {ReactiveQueryOptions<TValue>} [options]
140
+ */
141
+ constructor(source, options = {}) {
142
+ this.#source = source;
143
+ this.#enabled = options.enabled ?? true;
144
+ const hasInitial = options.initialData !== undefined;
145
+ this.#state = freezeState({
146
+ status: hasInitial ? "success" : "pending",
147
+ data: options.initialData,
148
+ error: undefined,
149
+ reset: undefined,
150
+ isLoading: !hasInitial,
151
+ isFetching: false,
152
+ isStale: false,
153
+ });
154
+ this.#serverSnapshot = this.#state;
155
+ }
156
+
157
+ /** @returns {ReactiveQueryState<TValue>} */
158
+ getSnapshot = () => this.#state;
159
+ /** @returns {ReactiveQueryState<TValue>} */
160
+ getServerSnapshot = () => this.#serverSnapshot;
161
+
162
+ /**
163
+ * Updates the small, hook-facing option set without replacing the external
164
+ * store. React may render a hook with a new options object while preserving
165
+ * the observer; an `enabled` transition must therefore start/stop delivery
166
+ * at the subscription boundary instead of leaving a stale subscription.
167
+ *
168
+ * @param {ReactiveQueryOptions<TValue>} [options]
169
+ * @returns {void}
170
+ */
171
+ updateOptions(options = {}) {
172
+ const enabled = options.enabled ?? true;
173
+ const wasEnabled = this.#enabled;
174
+ this.#enabled = enabled;
175
+ if (!enabled && wasEnabled) {
176
+ this.dispose();
177
+ } else if (enabled && !wasEnabled && this.#listeners.size > 0) {
178
+ this.start();
179
+ }
180
+ }
181
+
182
+ /**
183
+ * @param {() => void} listener
184
+ * @returns {() => void}
185
+ */
186
+ subscribe = listener => {
187
+ this.#listeners.add(listener);
188
+ if (this.#listeners.size === 1 && this.#enabled) this.start();
189
+ return () => {
190
+ this.#listeners.delete(listener);
191
+ if (this.#listeners.size === 0) this.dispose();
192
+ };
193
+ };
194
+
195
+ /** @returns {void} */
196
+ start() {
197
+ if (this.#started || !this.#enabled) return;
198
+ this.#started = true;
199
+ const abort = new AbortController();
200
+ this.#abort = abort;
201
+ const generation = ++this.#generation;
202
+ this.#setState({
203
+ ...this.#state,
204
+ isFetching: true,
205
+ isLoading: this.#state.data === undefined,
206
+ ...(this.#state.data === undefined ? { status: /** @type {const} */ ("pending") } : {}),
207
+ });
208
+ // State listeners may synchronously refetch or disable this generation.
209
+ if (generation !== this.#generation || abort.signal.aborted) return;
210
+ /** @type {ReactiveDisposer | void | Promise<ReactiveDisposer | void>} */
211
+ let result;
212
+ try {
213
+ result = this.#source.subscribe(this.#observer(generation), { signal: abort.signal });
214
+ } catch (error) {
215
+ this.#onError(generation, error);
216
+ return;
217
+ }
218
+ /**
219
+ * @param {ReactiveDisposer | void} cleanup
220
+ * @returns {void}
221
+ */
222
+ const retainCleanup = cleanup => {
223
+ if (generation !== this.#generation || abort.signal.aborted) disposeSubscription(cleanup);
224
+ else this.#cleanup = toCleanup(cleanup);
225
+ };
226
+ if (result instanceof Promise) {
227
+ void result.then(retainCleanup, error => this.#onError(generation, error));
228
+ } else {
229
+ retainCleanup(result);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Aborts HTTP polling and disposes the current transport subscription.
235
+ *
236
+ * @returns {void}
237
+ */
238
+ dispose() {
239
+ if (!this.#started) return;
240
+ ++this.#generation;
241
+ const abort = this.#abort;
242
+ this.#abort = undefined;
243
+ const cleanup = this.#cleanup;
244
+ this.#cleanup = undefined;
245
+ this.#started = false;
246
+ abort?.abort();
247
+ cleanup?.();
248
+ }
249
+
250
+ /**
251
+ * Restart only while an enabled query has live observers.
252
+ *
253
+ * @returns {void}
254
+ */
255
+ refetch() {
256
+ if (!this.#enabled || this.#listeners.size === 0) return;
257
+ this.dispose();
258
+ this.start();
259
+ }
260
+
261
+ /**
262
+ * @param {number} generation
263
+ * @returns {ReactiveQueryCallbacks<TValue>}
264
+ */
265
+ #observer(generation) {
266
+ return {
267
+ next: value => {
268
+ if (generation !== this.#generation) return;
269
+ if (this.#state.status === "success" && this.#state.data === value && this.#state.isFetching && !this.#state.isStale) return;
270
+ this.#setState({
271
+ status: "success",
272
+ data: value,
273
+ error: undefined,
274
+ reset: undefined,
275
+ isLoading: false,
276
+ isFetching: true,
277
+ isStale: false,
278
+ });
279
+ },
280
+ error: error => this.#onError(generation, error),
281
+ reset: reset => {
282
+ if (generation !== this.#generation) return;
283
+ this.#setState({
284
+ status: "reset",
285
+ data: reset.data,
286
+ error: undefined,
287
+ reset,
288
+ isLoading: reset.data === undefined,
289
+ isFetching: true,
290
+ isStale: true,
291
+ });
292
+ },
293
+ };
294
+ }
295
+
296
+ /**
297
+ * @param {number} generation
298
+ * @param {unknown} cause
299
+ * @returns {void}
300
+ */
301
+ #onError(generation, cause) {
302
+ if (generation !== this.#generation || this.#abort?.signal.aborted) return;
303
+ const error = normalizeReactiveError(cause, "subscription");
304
+ if (generation !== this.#generation || this.#abort?.signal.aborted) return;
305
+ // A subscription error is terminal for this delivery generation. Fence
306
+ // late stream events before aborting and disposing the transport so an
307
+ // already reported error cannot be replaced by a subsequent value.
308
+ const terminalGeneration = ++this.#generation;
309
+ const abort = this.#abort;
310
+ this.#abort = undefined;
311
+ const cleanup = this.#cleanup;
312
+ this.#cleanup = undefined;
313
+ this.#started = false;
314
+ // Abort listeners and transport cleanup may synchronously refetch. Detach
315
+ // the failed generation first, then leave any replacement ownership intact.
316
+ abort?.abort();
317
+ cleanup?.();
318
+ if (terminalGeneration !== this.#generation) return;
319
+ this.#setState({
320
+ ...this.#state,
321
+ status: "error",
322
+ error,
323
+ isLoading: this.#state.data === undefined,
324
+ isFetching: false,
325
+ isStale: true,
326
+ });
327
+ }
328
+
329
+ /**
330
+ * @param {ReactiveQueryState<TValue>} state
331
+ * @returns {void}
332
+ */
333
+ #setState(state) {
334
+ this.#state = freezeState(state);
335
+ for (const listener of this.#listeners) listener();
336
+ }
337
+ }
338
+
339
+ /**
340
+ * @template TValue
341
+ * @typedef {{ readonly kind: "snapshot" | "data"; readonly value: TValue }
342
+ * | { readonly kind: "reset"; readonly reset: ReactiveReset<TValue> }
343
+ * | { readonly kind: "error"; readonly error: unknown }} ReactiveHttpEvent
344
+ */
345
+
346
+ /**
347
+ * Typed boundary for the owning HTTP contract. Implementations perform the
348
+ * documented request and polling protocol; this adapter only maps events to
349
+ * the hook's observer and propagates AbortSignal.
350
+ *
351
+ * @template TRequest, TValue
352
+ * @typedef {{
353
+ * readonly subscribe: (
354
+ * request: TRequest,
355
+ * observer: { readonly event: (event: ReactiveHttpEvent<TValue>) => void },
356
+ * context: ReactiveSubscriptionContext,
357
+ * ) => ReactiveDisposer | void | Promise<ReactiveDisposer | void>;
358
+ * }} ReactiveHttpTransport
359
+ */
360
+
361
+ /**
362
+ * @template TRequest, TValue
363
+ * @param {ReactiveHttpTransport<TRequest, TValue>} transport
364
+ * @param {TRequest} request
365
+ * @param {string | readonly unknown[]} [key]
366
+ * @returns {ReactiveQuerySource<TValue>}
367
+ */
368
+ export function createReactiveHttpQuery(transport, request, key) {
369
+ return Object.freeze({
370
+ ...(key === undefined ? {} : { key }),
371
+ /**
372
+ * @param {ReactiveQueryCallbacks<TValue>} observer
373
+ * @param {ReactiveSubscriptionContext} context
374
+ */
375
+ subscribe: (observer, context) =>
376
+ transport.subscribe(request, {
377
+ event: event => {
378
+ switch (event.kind) {
379
+ case "snapshot":
380
+ case "data":
381
+ observer.next(event.value);
382
+ break;
383
+ case "reset":
384
+ observer.reset(event.reset);
385
+ break;
386
+ case "error":
387
+ observer.error(event.error);
388
+ break;
389
+ }
390
+ },
391
+ }, context),
392
+ });
393
+ }
394
+
395
+ /**
396
+ * @typedef {{
397
+ * readonly signal: AbortSignal;
398
+ * readonly idempotencyKey: string;
399
+ * }} ReactiveMutationContext
400
+ */
401
+
402
+ /** @typedef {void | (() => void) | { readonly rollback: () => void }} OptimisticRollback */
403
+
404
+ /**
405
+ * @template TValue, TVariables
406
+ * @typedef {{
407
+ * readonly mutationFn: (
408
+ * variables: TVariables,
409
+ * context: ReactiveMutationContext,
410
+ * ) => Promise<TValue | TerraScaleResult<TValue> | DatabaseResult<TValue>>;
411
+ * readonly idempotencyKey?: string | ((variables: TVariables) => string);
412
+ * readonly onMutate?: (variables: TVariables, context: ReactiveMutationContext) => OptimisticRollback | Promise<OptimisticRollback>;
413
+ * }} ReactiveMutationOptions
414
+ */
415
+
416
+ /** @typedef {"idle" | "pending" | "success" | "error" | "cancelled"} ReactiveMutationStatus */
417
+
418
+ /**
419
+ * @template TValue
420
+ * @typedef {{
421
+ * readonly status: ReactiveMutationStatus;
422
+ * readonly data: TValue | undefined;
423
+ * readonly error: ReactiveError | undefined;
424
+ * readonly idempotencyKey: string | undefined;
425
+ * readonly isPending: boolean;
426
+ * }} ReactiveMutationState
427
+ */
428
+
429
+ /**
430
+ * @template TValue, TVariables
431
+ */
432
+ export class ReactiveMutationObserver {
433
+ /** @type {ReactiveMutationOptions<TValue, TVariables>} */
434
+ #options;
435
+ /** @readonly @type {Set<() => void>} */
436
+ #listeners = new Set();
437
+ /** @type {ReactiveMutationState<TValue>} */
438
+ #state = freezeMutationState({ status: "idle", data: /** @type {TValue | undefined} */ (undefined), error: undefined, idempotencyKey: undefined, isPending: false });
439
+ /** @type {AbortController | undefined} */
440
+ #abort;
441
+ /** @type {(() => void) | undefined} */
442
+ #rollback;
443
+ /** @type {string | undefined} */
444
+ #activeKey;
445
+ #run = 0;
446
+ #activeRun = 0;
447
+ /** @type {number | undefined} */
448
+ #cancelledRun;
449
+ #disposed = false;
450
+
451
+ /**
452
+ * @param {ReactiveMutationOptions<TValue, TVariables>} options
453
+ */
454
+ constructor(options) {
455
+ this.#options = options;
456
+ }
457
+
458
+ /**
459
+ * Keep callbacks current when a component re-renders without replacing the
460
+ * observer (the normal `useMutation` lifecycle). A mutation already running
461
+ * captures the callbacks it started with, so a render cannot redirect an
462
+ * in-flight durable request.
463
+ *
464
+ * @param {ReactiveMutationOptions<TValue, TVariables>} options
465
+ * @returns {void}
466
+ */
467
+ updateOptions(options) {
468
+ this.#options = options;
469
+ }
470
+
471
+ /** @returns {ReactiveMutationState<TValue>} */
472
+ getSnapshot = () => this.#state;
473
+ /**
474
+ * @param {() => void} listener
475
+ * @returns {() => void}
476
+ */
477
+ subscribe = listener => {
478
+ this.#listeners.add(listener);
479
+ return () => this.#listeners.delete(listener);
480
+ };
481
+
482
+ /**
483
+ * @param {TVariables} variables
484
+ * @returns {Promise<TValue>}
485
+ */
486
+ async mutateAsync(variables) {
487
+ if (this.#disposed) throw new ReactiveMutationError("cancelled", "the mutation observer has been disposed");
488
+ if (this.#state.isPending) throw new ReactiveMutationError("mutation_in_flight", "only one mutation may be in flight per hook");
489
+ const options = this.#options;
490
+ const idempotencyKey = normalizeIdempotencyKey(
491
+ typeof options.idempotencyKey === "function"
492
+ ? options.idempotencyKey(variables)
493
+ : options.idempotencyKey ?? createIdempotencyKey(),
494
+ );
495
+ const abort = new AbortController();
496
+ const run = ++this.#run;
497
+ const context = Object.freeze({ signal: abort.signal, idempotencyKey });
498
+ this.#abort = abort;
499
+ this.#activeRun = run;
500
+ this.#cancelledRun = undefined;
501
+ this.#activeKey = idempotencyKey;
502
+ /** @type {(() => void) | undefined} */
503
+ let rollback;
504
+ /** @returns {void} */
505
+ const rollbackNow = () => {
506
+ const pending = rollback;
507
+ rollback = undefined;
508
+ pending?.();
509
+ };
510
+ this.#rollback = rollbackNow;
511
+ try {
512
+ this.#setState({ status: "pending", data: this.#state.data, error: undefined, idempotencyKey, isPending: true });
513
+ if (abort.signal.aborted || run !== this.#activeRun) throw cancelledError();
514
+ rollback = toRollback(await options.onMutate?.(variables, context));
515
+ if (abort.signal.aborted || run !== this.#activeRun) throw cancelledError();
516
+ const result = await options.mutationFn(variables, context);
517
+ const value = unwrapMutationResult(result);
518
+ if (abort.signal.aborted) throw cancelledError();
519
+ rollback = undefined;
520
+ if (run === this.#activeRun) this.#setState({ status: "success", data: value, error: undefined, idempotencyKey, isPending: false });
521
+ return value;
522
+ } catch (cause) {
523
+ rollbackNow();
524
+ const error = normalizeReactiveError(cause, abort.signal.aborted ? "cancelled" : "mutation");
525
+ if (run === this.#activeRun) this.#setState({ status: error.kind === "cancelled" ? "cancelled" : "error", data: this.#state.data, error, idempotencyKey, isPending: false });
526
+ else if (run === this.#cancelledRun && this.#state.status === "cancelled" && error.database !== undefined) {
527
+ // Cancellation stops presentation immediately. A later native result
528
+ // may still establish uncertainty for this same server operation.
529
+ this.#setState({ ...this.#state, error });
530
+ }
531
+ throw cause instanceof Error ? cause : new ReactiveMutationError(error.code, error.message, cause);
532
+ } finally {
533
+ if (run === this.#activeRun) {
534
+ this.#rollback = undefined;
535
+ this.#abort = undefined;
536
+ this.#activeKey = undefined;
537
+ }
538
+ }
539
+ }
540
+
541
+ /**
542
+ * @param {TVariables} variables
543
+ * @returns {void}
544
+ */
545
+ mutate(variables) {
546
+ void this.mutateAsync(variables).catch(() => undefined);
547
+ }
548
+
549
+ /**
550
+ * Cancels the durable request and immediately rolls back its presentation.
551
+ *
552
+ * @returns {void}
553
+ */
554
+ cancel() {
555
+ if (!this.#state.isPending) return;
556
+ const abort = this.#abort;
557
+ const rollback = this.#rollback;
558
+ const key = this.#activeKey;
559
+ this.#abort = undefined;
560
+ this.#rollback = undefined;
561
+ this.#activeKey = undefined;
562
+ this.#cancelledRun = this.#activeRun;
563
+ const cancelledGeneration = this.#activeRun = ++this.#run;
564
+ const error = normalizeReactiveError(cancelledError(), "cancelled");
565
+ // Publish the detached ownership before invoking abort/rollback callbacks,
566
+ // then notify only if those callbacks did not start a replacement run.
567
+ this.#state = freezeMutationState({ status: "cancelled", data: this.#state.data, error, idempotencyKey: key, isPending: false });
568
+ abort?.abort();
569
+ rollback?.();
570
+ if (cancelledGeneration === this.#activeRun) this.#notify();
571
+ }
572
+
573
+ /** @returns {void} */
574
+ reset() {
575
+ const resetGeneration = this.#run + (this.#state.isPending ? 1 : 0);
576
+ this.cancel();
577
+ if (resetGeneration !== this.#run) return;
578
+ this.#setState({ status: "idle", data: undefined, error: undefined, idempotencyKey: undefined, isPending: false });
579
+ }
580
+
581
+ /** @returns {void} */
582
+ dispose() {
583
+ this.#disposed = true;
584
+ this.cancel();
585
+ this.#listeners.clear();
586
+ }
587
+
588
+ /**
589
+ * @param {ReactiveMutationState<TValue>} state
590
+ * @returns {void}
591
+ */
592
+ #setState(state) {
593
+ this.#state = freezeMutationState(state);
594
+ this.#notify();
595
+ }
596
+
597
+ /** @returns {void} */
598
+ #notify() {
599
+ for (const listener of this.#listeners) listener();
600
+ }
601
+ }
602
+
603
+ export class ReactiveMutationError extends Error {
604
+ /** @override @readonly */
605
+ name = "ReactiveMutationError";
606
+ /** @readonly @type {string} */
607
+ code;
608
+ /** @override @readonly @type {unknown} */
609
+ cause;
610
+
611
+ /**
612
+ * @param {string} code
613
+ * @param {string} message
614
+ * @param {unknown} [cause]
615
+ */
616
+ constructor(code, message, cause) {
617
+ super(`terrascale reactive mutation: ${code}: ${message}`, { cause });
618
+ this.code = code;
619
+ this.cause = cause;
620
+ }
621
+ }
622
+
623
+ /**
624
+ * @template TValue
625
+ * @param {TValue | TerraScaleResult<TValue> | DatabaseResult<TValue>} result
626
+ * @returns {TValue}
627
+ */
628
+ function unwrapMutationResult(result) {
629
+ if (isTerraScaleResult(result)) {
630
+ if (!result.ok) throw new ReactiveMutationError(result.error.code, "message" in result.error ? result.error.message : `TerraBase request failed: ${result.error.code}`, result.error);
631
+ return result.value;
632
+ }
633
+ return result;
634
+ }
635
+
636
+ /**
637
+ * @template TValue
638
+ * @param {TValue | TerraScaleResult<TValue> | DatabaseResult<TValue>} value
639
+ * @returns {value is TerraScaleResult<TValue> | DatabaseResult<TValue>}
640
+ */
641
+ function isTerraScaleResult(value) {
642
+ return typeof value === "object" && value !== null && "ok" in value && typeof (/** @type {{ readonly ok?: unknown }} */ (value)).ok === "boolean";
643
+ }
644
+
645
+ /**
646
+ * @param {unknown} cause
647
+ * @param {ReactiveErrorKind} fallback
648
+ * @returns {ReactiveError}
649
+ */
650
+ function normalizeReactiveError(cause, fallback) {
651
+ if (cause instanceof ReactiveMutationError) return Object.freeze({ kind: fallback, code: cause.code, message: cause.message, cause: cause.cause, ...(isDatabaseError(cause.cause) ? { database: cause.cause } : {}) });
652
+ if (isTerraScaleError(cause)) {
653
+ return Object.freeze({ kind: fallback, code: cause.code, message: cause.message, cause, terraScale: cause, ...(cause instanceof Error && isDatabaseError(cause.cause) ? { database: cause.cause } : {}) });
654
+ }
655
+ if (isDatabaseError(cause)) return Object.freeze({ kind: fallback, code: cause.code, message: `TerraBase request failed: ${cause.code}`, cause, database: cause });
656
+ if (cause instanceof Error) return Object.freeze({ kind: fallback, code: fallback, message: cause.message, cause });
657
+ return Object.freeze({ kind: fallback, code: fallback, message: String(cause), cause });
658
+ }
659
+
660
+ /**
661
+ * @param {unknown} value
662
+ * @returns {value is Extract<DatabaseResult<never>, { readonly ok: false }>["error"]}
663
+ */
664
+ function isDatabaseError(value) {
665
+ return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string" && !("message" in value);
666
+ }
667
+
668
+ /**
669
+ * @param {unknown} value
670
+ * @returns {value is TerraScaleError}
671
+ */
672
+ function isTerraScaleError(value) {
673
+ return typeof value === "object" && value !== null && "code" in value && "message" in value && typeof (/** @type {{ readonly code?: unknown }} */ (value)).code === "string";
674
+ }
675
+
676
+ /**
677
+ * @template TValue
678
+ * @param {ReactiveQueryState<TValue>} state
679
+ * @returns {ReactiveQueryState<TValue>}
680
+ */
681
+ function freezeState(state) {
682
+ return Object.freeze(state);
683
+ }
684
+
685
+ /**
686
+ * @template TValue
687
+ * @param {ReactiveMutationState<TValue>} state
688
+ * @returns {ReactiveMutationState<TValue>}
689
+ */
690
+ function freezeMutationState(state) {
691
+ return Object.freeze(state);
692
+ }
693
+
694
+ /**
695
+ * @param {ReactiveDisposer | void} disposer
696
+ * @returns {(() => void) | undefined}
697
+ */
698
+ function toCleanup(disposer) {
699
+ if (disposer === undefined) return undefined;
700
+ if (typeof disposer === "function") return disposer;
701
+ if ("dispose" in disposer) return () => disposer.dispose();
702
+ return () => disposer.unsubscribe();
703
+ }
704
+
705
+ /**
706
+ * @param {ReactiveDisposer | void} disposer
707
+ * @returns {void}
708
+ */
709
+ function disposeSubscription(disposer) {
710
+ toCleanup(disposer)?.();
711
+ }
712
+
713
+ /**
714
+ * @param {OptimisticRollback} rollback
715
+ * @returns {(() => void) | undefined}
716
+ */
717
+ function toRollback(rollback) {
718
+ if (rollback === undefined) return undefined;
719
+ if (typeof rollback === "function") return rollback;
720
+ return rollback.rollback;
721
+ }
722
+
723
+ /**
724
+ * @param {string} value
725
+ * @returns {string}
726
+ */
727
+ function normalizeIdempotencyKey(value) {
728
+ if (typeof value !== "string" || value.trim().length === 0 || value.trim().length > 128) {
729
+ throw new TypeError("Reactive mutation idempotency keys must be nonblank and contain at most 128 characters.");
730
+ }
731
+ return value.trim();
732
+ }
733
+
734
+ /** @returns {string} */
735
+ function createIdempotencyKey() {
736
+ const randomUuid = globalThis.crypto?.randomUUID;
737
+ return randomUuid === undefined ? `tsm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}` : randomUuid.call(globalThis.crypto);
738
+ }
739
+
740
+ /** @returns {ReactiveMutationError} */
741
+ function cancelledError() {
742
+ return new ReactiveMutationError("cancelled", "the mutation was cancelled");
743
+ }