sibujs 3.4.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +118 -15
  3. package/dist/browser.cjs +410 -101
  4. package/dist/browser.d.cts +50 -5
  5. package/dist/browser.d.ts +50 -5
  6. package/dist/browser.js +8 -6
  7. package/dist/build.cjs +777 -385
  8. package/dist/build.js +15 -15
  9. package/dist/cdn.global.js +11 -7
  10. package/dist/{chunk-JCO7AHNU.js → chunk-3EKJK5FZ.js} +73 -34
  11. package/dist/chunk-4MFZMLUU.js +147 -0
  12. package/dist/chunk-52XFPGSN.js +123 -0
  13. package/dist/chunk-5INI7D2L.js +194 -0
  14. package/dist/{chunk-OAVXAFAY.js → chunk-6LG2T7LO.js} +5 -5
  15. package/dist/{chunk-OOUQUCNN.js → chunk-ACBT6UZS.js} +57 -22
  16. package/dist/{chunk-F7HW2NI5.js → chunk-AW6HPJ3L.js} +1 -1
  17. package/dist/chunk-DHDZ7IVN.js +109 -0
  18. package/dist/{chunk-JNGGM2XE.js → chunk-DQVUBVNX.js} +16 -25
  19. package/dist/{chunk-AXFWGY32.js → chunk-DR7H6UGM.js} +232 -67
  20. package/dist/{chunk-WSGLJYFI.js → chunk-E62UADKL.js} +9 -3
  21. package/dist/{chunk-3Z2NGWQI.js → chunk-ES2GERY2.js} +1 -1
  22. package/dist/{chunk-S3V6ZXPI.js → chunk-EWEE7NBF.js} +115 -41
  23. package/dist/{chunk-3DVGCW7O.js → chunk-F64ZPCFE.js} +8 -8
  24. package/dist/{chunk-APFRCIRI.js → chunk-GCSGGA3Q.js} +295 -198
  25. package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
  26. package/dist/{chunk-ELXMRSVN.js → chunk-NF3LYCQH.js} +259 -124
  27. package/dist/chunk-O3QQ3XOL.js +61 -0
  28. package/dist/{chunk-BUKYCWHX.js → chunk-PBQEW5VW.js} +14 -10
  29. package/dist/{chunk-H5FI5WDP.js → chunk-RRIDJQ5C.js} +3 -3
  30. package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
  31. package/dist/{chunk-ODXVQ3CS.js → chunk-VFHIVEPF.js} +309 -188
  32. package/dist/chunk-VPP2FONR.js +142 -0
  33. package/dist/{chunk-KZBEVFXZ.js → chunk-W55YHPEP.js} +5 -3
  34. package/dist/{chunk-FKQG7FTY.js → chunk-WN6WDFCB.js} +4 -4
  35. package/dist/{chunk-CEBWDD63.js → chunk-WOOMUJFT.js} +228 -86
  36. package/dist/{chunk-ITWCTZYH.js → chunk-Z2CE42DB.js} +19 -25
  37. package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.cts} +6 -6
  38. package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.ts} +6 -6
  39. package/dist/{customElement-CPfIrbvg.d.ts → customElement-CKLoO63w.d.cts} +73 -11
  40. package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.ts} +73 -11
  41. package/dist/data.cjs +483 -205
  42. package/dist/data.d.cts +139 -7
  43. package/dist/data.d.ts +139 -7
  44. package/dist/data.js +9 -9
  45. package/dist/devtools.cjs +222 -87
  46. package/dist/devtools.js +8 -8
  47. package/dist/dispose-4FZ6OFXR.js +19 -0
  48. package/dist/ecosystem.cjs +419 -175
  49. package/dist/ecosystem.js +11 -11
  50. package/dist/extras.cjs +1981 -765
  51. package/dist/extras.d.cts +7 -7
  52. package/dist/extras.d.ts +7 -7
  53. package/dist/extras.js +29 -26
  54. package/dist/index.cjs +784 -385
  55. package/dist/index.d.cts +299 -28
  56. package/dist/index.d.ts +299 -28
  57. package/dist/index.js +33 -18
  58. package/dist/motion.cjs +180 -50
  59. package/dist/motion.d.cts +26 -0
  60. package/dist/motion.d.ts +26 -0
  61. package/dist/motion.js +5 -5
  62. package/dist/patterns.cjs +437 -170
  63. package/dist/patterns.d.cts +60 -13
  64. package/dist/patterns.d.ts +60 -13
  65. package/dist/patterns.js +7 -7
  66. package/dist/performance.cjs +333 -148
  67. package/dist/performance.d.cts +55 -7
  68. package/dist/performance.d.ts +55 -7
  69. package/dist/performance.js +9 -9
  70. package/dist/plugins.cjs +1374 -576
  71. package/dist/plugins.d.cts +227 -9
  72. package/dist/plugins.d.ts +227 -9
  73. package/dist/plugins.js +613 -321
  74. package/dist/{ssr-D62yFwuw.d.cts → ssr-BiPRdZ6n.d.cts} +44 -15
  75. package/dist/{ssr-D62yFwuw.d.ts → ssr-BiPRdZ6n.d.ts} +44 -15
  76. package/dist/{ssr-2PPULEK2.js → ssr-JFP5535P.js} +5 -4
  77. package/dist/ssr.cjs +1104 -443
  78. package/dist/ssr.d.cts +230 -20
  79. package/dist/ssr.d.ts +230 -20
  80. package/dist/ssr.js +15 -12
  81. package/dist/{startup-0Qv6aosO.d.cts → startup-BMpaiMhP.d.cts} +1 -1
  82. package/dist/{startup-0Qv6aosO.d.ts → startup-BMpaiMhP.d.ts} +1 -1
  83. package/dist/testing.cjs +205 -63
  84. package/dist/testing.d.cts +1 -1
  85. package/dist/testing.d.ts +1 -1
  86. package/dist/testing.js +8 -10
  87. package/dist/ui.cjs +640 -199
  88. package/dist/ui.d.cts +2 -2
  89. package/dist/ui.d.ts +2 -2
  90. package/dist/ui.js +10 -10
  91. package/dist/widgets.cjs +207 -82
  92. package/dist/widgets.js +9 -9
  93. package/package.json +12 -7
  94. package/dist/chunk-5VGSK6D2.js +0 -80
  95. package/dist/chunk-COY6PUD2.js +0 -34
  96. package/dist/chunk-K35BAR4P.js +0 -96
  97. package/dist/chunk-L3GAGWCC.js +0 -111
  98. package/dist/chunk-RT3IYFN2.js +0 -38
package/dist/data.d.cts CHANGED
@@ -13,7 +13,15 @@ interface RetryOptions {
13
13
  maxDelay?: number;
14
14
  /** Jitter factor (0-1) to randomize delay. Default: 0.1 */
15
15
  jitter?: number;
16
- /** Predicate to decide if an error is retryable. Default: () => true */
16
+ /**
17
+ * Predicate deciding whether a **non-cancellation** failure is retryable.
18
+ * Default: `() => true`.
19
+ *
20
+ * Cancellation never reaches this callback. An aborted signal, or a rejection
21
+ * that is itself an `AbortError`, bypasses retry policy entirely — so a
22
+ * `shouldRetry` returning `true` for everything still cannot resurrect a
23
+ * cancelled operation.
24
+ */
17
25
  shouldRetry?: (error: unknown, attempt: number) => boolean;
18
26
  }
19
27
  /**
@@ -24,9 +32,16 @@ declare function calculateDelay(attempt: number, strategy: "exponential" | "line
24
32
  * Execute an async function with retry logic.
25
33
  * Returns the result or throws after all retries are exhausted.
26
34
  *
35
+ * CANCELLATION OUTRANKS RETRY POLICY. A cancelled operation — signalled either
36
+ * by `signal.aborted` or by a rejection that is itself an `AbortError` — skips
37
+ * `shouldRetry`, `onRetry` and the backoff, and rejects immediately.
38
+ *
27
39
  * @param fn The async function to execute
28
40
  * @param options Retry configuration
29
- * @param onRetry Callback fired before each retry with error, attempt, and delay
41
+ * @param onRetry Fired only when another attempt WILL actually be scheduled
42
+ * never on the final failure, never for a rejection `shouldRetry` declined,
43
+ * and never for a cancellation. Receives the error, the attempt index, and
44
+ * the delay before the next attempt.
30
45
  * @param signal AbortSignal to cancel retries
31
46
  *
32
47
  * @example
@@ -40,6 +55,44 @@ declare function calculateDelay(attempt: number, strategy: "exponential" | "line
40
55
  */
41
56
  declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions, onRetry?: (error: unknown, attempt: number, delay: number) => void, signal?: AbortSignal): Promise<T>;
42
57
 
58
+ /**
59
+ * ## Callback semantics
60
+ *
61
+ * These rules are shared by `query()`, `resource()`, `infiniteQuery()`, and
62
+ * `mutation()`.
63
+ *
64
+ * **A callback exception is not an operation failure.** Exceptions thrown by
65
+ * lifecycle callbacks (`onSuccess`, `onError`, `onSettled`, `onStart`) and by
66
+ * `select` do not retroactively change the success/failure state of the
67
+ * underlying request. A fetch that succeeded stays successful, its data stays
68
+ * available, and the shared cache keeps the value it committed:
69
+ *
70
+ * ```text
71
+ * network success → cache commit → onSuccess throws → still a success
72
+ * ```
73
+ *
74
+ * **Callback errors are surfaced separately.** They are reported via
75
+ * `console.error` (prefixed `[SibuJS data]`), never silently swallowed, and
76
+ * never routed into the operation's own error channel — a throwing `onSuccess`
77
+ * does not trigger `onError`, and does not populate `error()`.
78
+ *
79
+ * **Observers are isolated from each other.** Multiple observers sharing one
80
+ * cache key are notified independently: one observer's `select` throwing cannot
81
+ * prevent the others from receiving the shared result, and cannot poison the
82
+ * shared cache entry.
83
+ *
84
+ * **A throwing `select` keeps the previous data.** `select` is the observer's
85
+ * own transform, not part of the request. If it throws, this observer retains
86
+ * whatever data it already had rather than committing a value that was never
87
+ * produced; the request is still recorded as successful.
88
+ *
89
+ * **Ordering is guaranteed.** State is committed first, then `onSuccess` /
90
+ * `onError`, then `onSettled`. `onSettled` runs even when the callback before
91
+ * it threw.
92
+ *
93
+ * The one exception is `mutation()`'s `onMutate`, which is a step *of* the
94
+ * mutation rather than a notification — see `MutationOptions.onMutate`.
95
+ */
43
96
  interface QueryOptions<T> {
44
97
  /** Time in ms before cached data is considered stale. Default: 0 (always stale) */
45
98
  staleTime?: number;
@@ -103,10 +156,33 @@ declare function clearQueryCache(): void;
103
156
  */
104
157
  declare function __resetQueryCache(): void;
105
158
 
159
+ /**
160
+ * Lifecycle callbacks follow the shared data-layer contract: an exception
161
+ * thrown by `onSuccess`, `onError`, or `onSettled` never changes the status of
162
+ * the mutation itself — a mutation that succeeded stays `success` and
163
+ * `mutateAsync` still resolves — and is reported separately via
164
+ * `console.error`. See `QueryOptions` for the full statement.
165
+ *
166
+ * `onMutate` is the deliberate exception; see its own note below.
167
+ */
106
168
  interface MutationOptions<TData, TVariables, TContext = unknown> {
107
169
  /** Retry options for failed mutations */
108
170
  retry?: RetryOptions;
109
- /** Called before mutation — return context for rollback in onError */
171
+ /**
172
+ * Called before the mutation — return context for rollback in `onError`.
173
+ *
174
+ * Unlike the notification callbacks, `onMutate` is a **step of** the
175
+ * mutation: it produces the rollback context everything downstream depends
176
+ * on. An ordinary exception therefore fails the mutation — there is no
177
+ * context, and the optimistic update it was meant to apply never happened.
178
+ *
179
+ * An `AbortError` is the one exception, and it is cancellation rather than
180
+ * failure — the same rule `mutationFn` follows. `isAbortError()` is
181
+ * authoritative for the whole operation, so where a cancellation is raised
182
+ * inside it does not change what it means: no error state, no `onError`, and
183
+ * the state the mutation replaced is restored. Throw an ordinary `Error` from
184
+ * `onMutate` to fail the mutation.
185
+ */
110
186
  onMutate?: (variables: TVariables) => TContext | Promise<TContext>;
111
187
  /** Called on successful mutation */
112
188
  onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;
@@ -135,6 +211,13 @@ interface MutationResult<TData, TVariables> {
135
211
  }
136
212
  declare function mutation<TData, TVariables = void, TContext = unknown>(mutationFn: (variables: TVariables, signal?: AbortSignal) => Promise<TData>, options?: MutationOptions<TData, TVariables, TContext>): MutationResult<TData, TVariables>;
137
213
 
214
+ /**
215
+ * Lifecycle callbacks follow the shared data-layer contract: an exception
216
+ * thrown by `onSuccess` or `onError` never changes the success/failure state of
217
+ * the page fetch itself — a page that was appended stays appended — and is
218
+ * reported separately via `console.error`. See `QueryOptions` for the full
219
+ * statement.
220
+ */
138
221
  interface InfiniteQueryOptions<TData, TPageParam = number> {
139
222
  /** Get the param for the next page. Return undefined to signal end. */
140
223
  getNextPageParam: (lastPage: TData, allPages: TData[]) => TPageParam | undefined;
@@ -245,6 +328,12 @@ declare function debounce<T>(getter: () => T, delay: number): () => T;
245
328
  */
246
329
  declare function throttle<T>(getter: () => T, interval: number): () => T;
247
330
 
331
+ /**
332
+ * Lifecycle callbacks follow the shared data-layer contract: an exception
333
+ * thrown by `onStart`, `onSuccess`, `onError`, or `onSettled` never changes the
334
+ * success/failure state of the fetch itself, and is reported separately via
335
+ * `console.error`. See `QueryOptions` for the full statement.
336
+ */
248
337
  interface ResourceOptions<T> {
249
338
  /** Initial data value before first fetch. Default: undefined */
250
339
  initialValue?: T;
@@ -318,13 +407,53 @@ interface OfflineStoreOptions<T> {
318
407
  /** Auto-sync when online status changes (default: true) */
319
408
  autoSync?: boolean;
320
409
  }
410
+ /**
411
+ * How a pulled remote record is reconciled with a local record that still has
412
+ * an unpushed change queued for the same key. Records with no queued local
413
+ * change are never conflicts — the remote value always applies.
414
+ */
415
+ type ConflictStrategy = "client-wins" | "server-wins" | "manual";
416
+ /** The state handed to a `manual` resolver for one conflicting key. */
417
+ interface SyncConflict<T> {
418
+ /** Value of the store's `keyPath` for the conflicting record. */
419
+ key: unknown;
420
+ /** Current local item — `undefined` when the queued local change is a delete. */
421
+ local: T | undefined;
422
+ /** The record as returned by the remote `pull()`. */
423
+ remote: T;
424
+ /** The queued local change that makes this key a conflict. */
425
+ pending: SyncChange<T>;
426
+ }
321
427
  interface SyncAdapter<T> {
322
428
  /** Push local changes to remote */
323
429
  push: (changes: SyncChange<T>[]) => Promise<SyncResult>;
324
430
  /** Pull remote changes since last sync */
325
431
  pull: (since: number | null) => Promise<T[]>;
326
- /** Conflict resolution strategy */
327
- conflictStrategy: "client-wins" | "server-wins" | "manual";
432
+ /**
433
+ * Conflict resolution strategy. Defaults to `"client-wins"`.
434
+ *
435
+ * - `client-wins` — a pulled record is DISCARDED for any key that still has
436
+ * an unpushed local change. The local edit survives and is pushed on the
437
+ * next round. This is the safe default: it never destroys work made
438
+ * offline.
439
+ * - `server-wins` — the pulled record OVERWRITES local state even for keys
440
+ * with queued changes. The queued change itself is **retained**, not
441
+ * dropped: it still has to reach the server, and discarding it here would
442
+ * turn a display-precedence choice into silent data loss. A subsequent
443
+ * successful push therefore re-asserts the local edit remotely.
444
+ * - `manual` — `resolveConflict` decides per key. Without a `resolveConflict`
445
+ * implementation this degrades to `client-wins` and warns, because the only
446
+ * other option is to guess with the user's unsynced data.
447
+ */
448
+ conflictStrategy?: ConflictStrategy;
449
+ /**
450
+ * Resolve one conflicting key. Required for `conflictStrategy: "manual"`.
451
+ *
452
+ * Return the value to store locally, or `undefined` to keep the local record
453
+ * untouched. May be async. A resolver that throws falls back to keeping the
454
+ * local record — the conservative direction.
455
+ */
456
+ resolveConflict?: (conflict: SyncConflict<T>) => T | undefined | Promise<T | undefined>;
328
457
  }
329
458
  interface SyncChange<T> {
330
459
  type: "put" | "delete";
@@ -369,6 +498,9 @@ interface OfflineStore<T> {
369
498
  * adapter: syncAdapter({
370
499
  * push: (changes) => fetch("/api/sync", { method: "POST", body: JSON.stringify(changes) }),
371
500
  * pull: (since) => fetch(`/api/todos?since=${since}`).then(r => r.json()),
501
+ * // client-wins (the default) never lets a pulled record overwrite a key
502
+ * // that still has an unpushed local edit. Use "server-wins" to let the
503
+ * // remote value win, or "manual" plus a `resolveConflict` resolver.
372
504
  * conflictStrategy: "client-wins",
373
505
  * }),
374
506
  * });
@@ -377,7 +509,7 @@ interface OfflineStore<T> {
377
509
  * store.data(); // [{ id: "1", text: "Buy milk", done: false }]
378
510
  * ```
379
511
  */
380
- declare function offlineStore<T extends Record<string, unknown>>(options: OfflineStoreOptions<T>): Promise<OfflineStore<T>>;
512
+ declare function offlineStore<T extends object>(options: OfflineStoreOptions<T>): Promise<OfflineStore<T>>;
381
513
  /**
382
514
  * Helper to create a sync adapter configuration.
383
515
  */
@@ -464,4 +596,4 @@ declare function stream(url: string, options?: {
464
596
  dispose: () => void;
465
597
  };
466
598
 
467
- export { type InfiniteQueryOptions, type InfiniteQueryResult, type LoaderRoute, type MutationOptions, type MutationResult, type OfflineStore, type OfflineStoreOptions, type QueryOptions, type QueryResult, type Resource, type ResourceOptions, type RetryOptions, type RouteLoaderFn, type SyncAdapter, type SyncChange, type SyncResult, __resetQueryCache, calculateDelay, clearQueryCache, debounce, executeLoader, getQueryData, infiniteQuery, invalidateQueries, loaderData, mutation, offlineStore, preloadRoute, previous, query, resource, setQueryData, socket, stream, syncAdapter, throttle, withRetry };
599
+ export { type ConflictStrategy, type InfiniteQueryOptions, type InfiniteQueryResult, type LoaderRoute, type MutationOptions, type MutationResult, type OfflineStore, type OfflineStoreOptions, type QueryOptions, type QueryResult, type Resource, type ResourceOptions, type RetryOptions, type RouteLoaderFn, type SyncAdapter, type SyncChange, type SyncConflict, type SyncResult, __resetQueryCache, calculateDelay, clearQueryCache, debounce, executeLoader, getQueryData, infiniteQuery, invalidateQueries, loaderData, mutation, offlineStore, preloadRoute, previous, query, resource, setQueryData, socket, stream, syncAdapter, throttle, withRetry };
package/dist/data.d.ts CHANGED
@@ -13,7 +13,15 @@ interface RetryOptions {
13
13
  maxDelay?: number;
14
14
  /** Jitter factor (0-1) to randomize delay. Default: 0.1 */
15
15
  jitter?: number;
16
- /** Predicate to decide if an error is retryable. Default: () => true */
16
+ /**
17
+ * Predicate deciding whether a **non-cancellation** failure is retryable.
18
+ * Default: `() => true`.
19
+ *
20
+ * Cancellation never reaches this callback. An aborted signal, or a rejection
21
+ * that is itself an `AbortError`, bypasses retry policy entirely — so a
22
+ * `shouldRetry` returning `true` for everything still cannot resurrect a
23
+ * cancelled operation.
24
+ */
17
25
  shouldRetry?: (error: unknown, attempt: number) => boolean;
18
26
  }
19
27
  /**
@@ -24,9 +32,16 @@ declare function calculateDelay(attempt: number, strategy: "exponential" | "line
24
32
  * Execute an async function with retry logic.
25
33
  * Returns the result or throws after all retries are exhausted.
26
34
  *
35
+ * CANCELLATION OUTRANKS RETRY POLICY. A cancelled operation — signalled either
36
+ * by `signal.aborted` or by a rejection that is itself an `AbortError` — skips
37
+ * `shouldRetry`, `onRetry` and the backoff, and rejects immediately.
38
+ *
27
39
  * @param fn The async function to execute
28
40
  * @param options Retry configuration
29
- * @param onRetry Callback fired before each retry with error, attempt, and delay
41
+ * @param onRetry Fired only when another attempt WILL actually be scheduled
42
+ * never on the final failure, never for a rejection `shouldRetry` declined,
43
+ * and never for a cancellation. Receives the error, the attempt index, and
44
+ * the delay before the next attempt.
30
45
  * @param signal AbortSignal to cancel retries
31
46
  *
32
47
  * @example
@@ -40,6 +55,44 @@ declare function calculateDelay(attempt: number, strategy: "exponential" | "line
40
55
  */
41
56
  declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions, onRetry?: (error: unknown, attempt: number, delay: number) => void, signal?: AbortSignal): Promise<T>;
42
57
 
58
+ /**
59
+ * ## Callback semantics
60
+ *
61
+ * These rules are shared by `query()`, `resource()`, `infiniteQuery()`, and
62
+ * `mutation()`.
63
+ *
64
+ * **A callback exception is not an operation failure.** Exceptions thrown by
65
+ * lifecycle callbacks (`onSuccess`, `onError`, `onSettled`, `onStart`) and by
66
+ * `select` do not retroactively change the success/failure state of the
67
+ * underlying request. A fetch that succeeded stays successful, its data stays
68
+ * available, and the shared cache keeps the value it committed:
69
+ *
70
+ * ```text
71
+ * network success → cache commit → onSuccess throws → still a success
72
+ * ```
73
+ *
74
+ * **Callback errors are surfaced separately.** They are reported via
75
+ * `console.error` (prefixed `[SibuJS data]`), never silently swallowed, and
76
+ * never routed into the operation's own error channel — a throwing `onSuccess`
77
+ * does not trigger `onError`, and does not populate `error()`.
78
+ *
79
+ * **Observers are isolated from each other.** Multiple observers sharing one
80
+ * cache key are notified independently: one observer's `select` throwing cannot
81
+ * prevent the others from receiving the shared result, and cannot poison the
82
+ * shared cache entry.
83
+ *
84
+ * **A throwing `select` keeps the previous data.** `select` is the observer's
85
+ * own transform, not part of the request. If it throws, this observer retains
86
+ * whatever data it already had rather than committing a value that was never
87
+ * produced; the request is still recorded as successful.
88
+ *
89
+ * **Ordering is guaranteed.** State is committed first, then `onSuccess` /
90
+ * `onError`, then `onSettled`. `onSettled` runs even when the callback before
91
+ * it threw.
92
+ *
93
+ * The one exception is `mutation()`'s `onMutate`, which is a step *of* the
94
+ * mutation rather than a notification — see `MutationOptions.onMutate`.
95
+ */
43
96
  interface QueryOptions<T> {
44
97
  /** Time in ms before cached data is considered stale. Default: 0 (always stale) */
45
98
  staleTime?: number;
@@ -103,10 +156,33 @@ declare function clearQueryCache(): void;
103
156
  */
104
157
  declare function __resetQueryCache(): void;
105
158
 
159
+ /**
160
+ * Lifecycle callbacks follow the shared data-layer contract: an exception
161
+ * thrown by `onSuccess`, `onError`, or `onSettled` never changes the status of
162
+ * the mutation itself — a mutation that succeeded stays `success` and
163
+ * `mutateAsync` still resolves — and is reported separately via
164
+ * `console.error`. See `QueryOptions` for the full statement.
165
+ *
166
+ * `onMutate` is the deliberate exception; see its own note below.
167
+ */
106
168
  interface MutationOptions<TData, TVariables, TContext = unknown> {
107
169
  /** Retry options for failed mutations */
108
170
  retry?: RetryOptions;
109
- /** Called before mutation — return context for rollback in onError */
171
+ /**
172
+ * Called before the mutation — return context for rollback in `onError`.
173
+ *
174
+ * Unlike the notification callbacks, `onMutate` is a **step of** the
175
+ * mutation: it produces the rollback context everything downstream depends
176
+ * on. An ordinary exception therefore fails the mutation — there is no
177
+ * context, and the optimistic update it was meant to apply never happened.
178
+ *
179
+ * An `AbortError` is the one exception, and it is cancellation rather than
180
+ * failure — the same rule `mutationFn` follows. `isAbortError()` is
181
+ * authoritative for the whole operation, so where a cancellation is raised
182
+ * inside it does not change what it means: no error state, no `onError`, and
183
+ * the state the mutation replaced is restored. Throw an ordinary `Error` from
184
+ * `onMutate` to fail the mutation.
185
+ */
110
186
  onMutate?: (variables: TVariables) => TContext | Promise<TContext>;
111
187
  /** Called on successful mutation */
112
188
  onSuccess?: (data: TData, variables: TVariables, context: TContext) => void;
@@ -135,6 +211,13 @@ interface MutationResult<TData, TVariables> {
135
211
  }
136
212
  declare function mutation<TData, TVariables = void, TContext = unknown>(mutationFn: (variables: TVariables, signal?: AbortSignal) => Promise<TData>, options?: MutationOptions<TData, TVariables, TContext>): MutationResult<TData, TVariables>;
137
213
 
214
+ /**
215
+ * Lifecycle callbacks follow the shared data-layer contract: an exception
216
+ * thrown by `onSuccess` or `onError` never changes the success/failure state of
217
+ * the page fetch itself — a page that was appended stays appended — and is
218
+ * reported separately via `console.error`. See `QueryOptions` for the full
219
+ * statement.
220
+ */
138
221
  interface InfiniteQueryOptions<TData, TPageParam = number> {
139
222
  /** Get the param for the next page. Return undefined to signal end. */
140
223
  getNextPageParam: (lastPage: TData, allPages: TData[]) => TPageParam | undefined;
@@ -245,6 +328,12 @@ declare function debounce<T>(getter: () => T, delay: number): () => T;
245
328
  */
246
329
  declare function throttle<T>(getter: () => T, interval: number): () => T;
247
330
 
331
+ /**
332
+ * Lifecycle callbacks follow the shared data-layer contract: an exception
333
+ * thrown by `onStart`, `onSuccess`, `onError`, or `onSettled` never changes the
334
+ * success/failure state of the fetch itself, and is reported separately via
335
+ * `console.error`. See `QueryOptions` for the full statement.
336
+ */
248
337
  interface ResourceOptions<T> {
249
338
  /** Initial data value before first fetch. Default: undefined */
250
339
  initialValue?: T;
@@ -318,13 +407,53 @@ interface OfflineStoreOptions<T> {
318
407
  /** Auto-sync when online status changes (default: true) */
319
408
  autoSync?: boolean;
320
409
  }
410
+ /**
411
+ * How a pulled remote record is reconciled with a local record that still has
412
+ * an unpushed change queued for the same key. Records with no queued local
413
+ * change are never conflicts — the remote value always applies.
414
+ */
415
+ type ConflictStrategy = "client-wins" | "server-wins" | "manual";
416
+ /** The state handed to a `manual` resolver for one conflicting key. */
417
+ interface SyncConflict<T> {
418
+ /** Value of the store's `keyPath` for the conflicting record. */
419
+ key: unknown;
420
+ /** Current local item — `undefined` when the queued local change is a delete. */
421
+ local: T | undefined;
422
+ /** The record as returned by the remote `pull()`. */
423
+ remote: T;
424
+ /** The queued local change that makes this key a conflict. */
425
+ pending: SyncChange<T>;
426
+ }
321
427
  interface SyncAdapter<T> {
322
428
  /** Push local changes to remote */
323
429
  push: (changes: SyncChange<T>[]) => Promise<SyncResult>;
324
430
  /** Pull remote changes since last sync */
325
431
  pull: (since: number | null) => Promise<T[]>;
326
- /** Conflict resolution strategy */
327
- conflictStrategy: "client-wins" | "server-wins" | "manual";
432
+ /**
433
+ * Conflict resolution strategy. Defaults to `"client-wins"`.
434
+ *
435
+ * - `client-wins` — a pulled record is DISCARDED for any key that still has
436
+ * an unpushed local change. The local edit survives and is pushed on the
437
+ * next round. This is the safe default: it never destroys work made
438
+ * offline.
439
+ * - `server-wins` — the pulled record OVERWRITES local state even for keys
440
+ * with queued changes. The queued change itself is **retained**, not
441
+ * dropped: it still has to reach the server, and discarding it here would
442
+ * turn a display-precedence choice into silent data loss. A subsequent
443
+ * successful push therefore re-asserts the local edit remotely.
444
+ * - `manual` — `resolveConflict` decides per key. Without a `resolveConflict`
445
+ * implementation this degrades to `client-wins` and warns, because the only
446
+ * other option is to guess with the user's unsynced data.
447
+ */
448
+ conflictStrategy?: ConflictStrategy;
449
+ /**
450
+ * Resolve one conflicting key. Required for `conflictStrategy: "manual"`.
451
+ *
452
+ * Return the value to store locally, or `undefined` to keep the local record
453
+ * untouched. May be async. A resolver that throws falls back to keeping the
454
+ * local record — the conservative direction.
455
+ */
456
+ resolveConflict?: (conflict: SyncConflict<T>) => T | undefined | Promise<T | undefined>;
328
457
  }
329
458
  interface SyncChange<T> {
330
459
  type: "put" | "delete";
@@ -369,6 +498,9 @@ interface OfflineStore<T> {
369
498
  * adapter: syncAdapter({
370
499
  * push: (changes) => fetch("/api/sync", { method: "POST", body: JSON.stringify(changes) }),
371
500
  * pull: (since) => fetch(`/api/todos?since=${since}`).then(r => r.json()),
501
+ * // client-wins (the default) never lets a pulled record overwrite a key
502
+ * // that still has an unpushed local edit. Use "server-wins" to let the
503
+ * // remote value win, or "manual" plus a `resolveConflict` resolver.
372
504
  * conflictStrategy: "client-wins",
373
505
  * }),
374
506
  * });
@@ -377,7 +509,7 @@ interface OfflineStore<T> {
377
509
  * store.data(); // [{ id: "1", text: "Buy milk", done: false }]
378
510
  * ```
379
511
  */
380
- declare function offlineStore<T extends Record<string, unknown>>(options: OfflineStoreOptions<T>): Promise<OfflineStore<T>>;
512
+ declare function offlineStore<T extends object>(options: OfflineStoreOptions<T>): Promise<OfflineStore<T>>;
381
513
  /**
382
514
  * Helper to create a sync adapter configuration.
383
515
  */
@@ -464,4 +596,4 @@ declare function stream(url: string, options?: {
464
596
  dispose: () => void;
465
597
  };
466
598
 
467
- export { type InfiniteQueryOptions, type InfiniteQueryResult, type LoaderRoute, type MutationOptions, type MutationResult, type OfflineStore, type OfflineStoreOptions, type QueryOptions, type QueryResult, type Resource, type ResourceOptions, type RetryOptions, type RouteLoaderFn, type SyncAdapter, type SyncChange, type SyncResult, __resetQueryCache, calculateDelay, clearQueryCache, debounce, executeLoader, getQueryData, infiniteQuery, invalidateQueries, loaderData, mutation, offlineStore, preloadRoute, previous, query, resource, setQueryData, socket, stream, syncAdapter, throttle, withRetry };
599
+ export { type ConflictStrategy, type InfiniteQueryOptions, type InfiniteQueryResult, type LoaderRoute, type MutationOptions, type MutationResult, type OfflineStore, type OfflineStoreOptions, type QueryOptions, type QueryResult, type Resource, type ResourceOptions, type RetryOptions, type RouteLoaderFn, type SyncAdapter, type SyncChange, type SyncConflict, type SyncResult, __resetQueryCache, calculateDelay, clearQueryCache, debounce, executeLoader, getQueryData, infiniteQuery, invalidateQueries, loaderData, mutation, offlineStore, preloadRoute, previous, query, resource, setQueryData, socket, stream, syncAdapter, throttle, withRetry };
package/dist/data.js CHANGED
@@ -20,16 +20,16 @@ import {
20
20
  syncAdapter,
21
21
  throttle,
22
22
  withRetry
23
- } from "./chunk-ELXMRSVN.js";
24
- import "./chunk-RT3IYFN2.js";
25
- import "./chunk-ITWCTZYH.js";
23
+ } from "./chunk-NF3LYCQH.js";
24
+ import "./chunk-O3QQ3XOL.js";
25
+ import "./chunk-Z2CE42DB.js";
26
+ import "./chunk-5INI7D2L.js";
26
27
  import "./chunk-7ZHH77QA.js";
27
- import "./chunk-L3GAGWCC.js";
28
- import "./chunk-BUKYCWHX.js";
29
- import "./chunk-S3NFJO6L.js";
30
- import "./chunk-KZBEVFXZ.js";
31
- import "./chunk-OOUQUCNN.js";
32
- import "./chunk-COY6PUD2.js";
28
+ import "./chunk-PBQEW5VW.js";
29
+ import "./chunk-TIRZCERI.js";
30
+ import "./chunk-W55YHPEP.js";
31
+ import "./chunk-ACBT6UZS.js";
32
+ import "./chunk-VPP2FONR.js";
33
33
  export {
34
34
  __resetQueryCache,
35
35
  calculateDelay,