dalila 1.9.7 → 1.9.9

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/README.md CHANGED
@@ -60,6 +60,8 @@ bind(document.getElementById('app')!, ctx);
60
60
 
61
61
  - [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, transitions, portal, text interpolation, events
62
62
  - [Components](./docs/runtime/component.md) — `defineComponent`, typed props/emits/refs, slots
63
+ - [Lazy Loading](./docs/runtime/lazy.md) — `createLazyComponent`, `d-lazy`, `createSuspense` wrapper, code splitting
64
+ - [Error Boundary](./docs/runtime/boundary.md) — `createErrorBoundary`, `createErrorBoundaryState`, `withErrorBoundary`, `d-boundary`
63
65
  - [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
64
66
 
65
67
  ### Routing
@@ -1,55 +1,130 @@
1
1
  import { type QueryKey } from "./key.js";
2
- export interface MutationConfig<TInput, TResult> {
3
- mutate: (signal: AbortSignal, input: TInput) => Promise<TResult>;
2
+ /**
3
+ * Configuration for creating a mutation (write operation).
4
+ *
5
+ * @template TInput - Type of input data passed to the mutation
6
+ * @template TResult - Type of result returned by the mutation
7
+ * @template TContext - Type of optional context kept between callbacks
8
+ */
9
+ export interface MutationConfig<TInput, TResult, TContext = unknown> {
10
+ /**
11
+ * Main function that executes the mutation.
12
+ * Receives an AbortSignal for cancellation and the input data.
13
+ * Alias: can also use the 'mutate' property.
14
+ */
15
+ mutationFn?: (signal: AbortSignal, input: TInput) => Promise<TResult>;
16
+ /**
17
+ * Alias for mutationFn. Useful for API compatibility.
18
+ */
19
+ mutate?: (signal: AbortSignal, input: TInput) => Promise<TResult>;
4
20
  /**
5
- * Optional invalidation (runs on success).
6
- * - Tags revalidate all cached resources that registered those tags.
7
- * - Keys revalidate a specific cached resource by key.
21
+ * Cache tags to automatically invalidate after success.
22
+ * Useful for updating related queries after mutation.
8
23
  */
9
24
  invalidateTags?: readonly string[];
25
+ /**
26
+ * Query keys to automatically invalidate after success.
27
+ */
10
28
  invalidateKeys?: readonly QueryKey[];
11
- onSuccess?: (result: TResult, input: TInput) => void;
12
- onError?: (error: Error, input: TInput) => void;
13
- onSettled?: (input: TInput) => void;
29
+ /**
30
+ * Callback executed before the mutation.
31
+ * Useful for optimistically updating UI state.
32
+ * Returns a context that will be passed to subsequent callbacks.
33
+ */
34
+ onMutate?: (input: TInput) => Promise<TContext> | TContext;
35
+ /**
36
+ * Callback executed when mutation succeeds.
37
+ * @param result - The result returned by the mutation
38
+ * @param input - The original input data
39
+ * @param context - The context returned by onMutate (or undefined)
40
+ */
41
+ onSuccess?: (result: TResult, input: TInput, context: TContext | undefined) => void;
42
+ /**
43
+ * Callback executed when mutation fails.
44
+ * @param error - The error that occurred
45
+ * @param input - The original input data
46
+ * @param context - The context returned by onMutate (or undefined)
47
+ */
48
+ onError?: (error: Error, input: TInput, context: TContext | undefined) => void;
49
+ /**
50
+ * Callback executed after mutation completes (success or error).
51
+ * Always executed, regardless of outcome.
52
+ * @param result - The result (or null if error)
53
+ * @param error - The error (or null if success)
54
+ * @param input - The original input data
55
+ * @param context - The context returned by onMutate (or undefined)
56
+ */
57
+ onSettled?: (result: TResult | null, error: Error | null, input: TInput, context: TContext | undefined) => void;
14
58
  }
59
+ /**
60
+ * Mutation state, exposing functions to control and access data.
61
+ *
62
+ * @template TInput - Type of input data
63
+ * @template TResult - Type of result
64
+ */
15
65
  export interface MutationState<TInput, TResult> {
66
+ /**
67
+ * Signal accessor for data returned by the mutation.
68
+ * Returns null if mutation hasn't run yet or failed.
69
+ */
16
70
  data: () => TResult | null;
71
+ /**
72
+ * Signal accessor indicating if mutation is in progress.
73
+ */
17
74
  loading: () => boolean;
75
+ /**
76
+ * Signal accessor for the last mutation error (if any).
77
+ */
18
78
  error: () => Error | null;
19
79
  /**
20
- * Runs the mutation.
21
- * - Dedupe: if already loading and not forced, it awaits the current run.
22
- * - Force: aborts the current run and starts a new request.
80
+ * Executes the mutation with the provided input data.
81
+ * @param input - Input data for the mutation
82
+ * @param opts.force - If true, aborts any in-flight mutation and starts a new one
83
+ * @returns Promise that resolves with the result or null on error
23
84
  */
24
85
  run: (input: TInput, opts?: {
25
86
  force?: boolean;
26
87
  }) => Promise<TResult | null>;
27
88
  /**
28
- * Resets local mutation state.
29
- * Does not affect the query cache.
89
+ * Resets the mutation state to initial state.
90
+ * Aborts any in-flight mutation and clears data, loading, and error.
30
91
  */
31
92
  reset: () => void;
32
93
  }
33
94
  /**
34
- * Mutation primitive (scope-safe).
95
+ * Creates a mutation - a write operation that executes side effects
96
+ * and can invalidate related queries to maintain cache consistency.
97
+ *
98
+ * Follows the Mutation pattern from React Query/TanStack Query:
99
+ * - Provides lifecycle callbacks (onMutate, onSuccess, onError, onSettled)
100
+ * - Supports automatic cache invalidation by tags or keys
101
+ * - Automatically manages cancellation via AbortSignal
35
102
  *
36
- * Design goals:
37
- * - DOM-first friendly: mutations are just async actions with reactive state.
38
- * - Scope-safe: abort on scope disposal (best-effort cleanup).
39
- * - Dedupe-by-default: concurrent `run()` calls share the same in-flight promise.
40
- * - Force re-run: abort the current request and start a new one.
41
- * - React Query-like behavior: keep the last successful `data()` until overwritten or reset.
103
+ * @template TInput - Type of input data
104
+ * @template TResult - Type of expected result
105
+ * @template TContext - Type of context for communication between callbacks
106
+ * @param cfg - Mutation configuration
107
+ * @returns Mutation state with methods to execute and control
42
108
  *
43
- * Semantics:
44
- * - Each run uses its own AbortController.
45
- * - If a run is aborted:
46
- * - it returns null,
47
- * - it MUST NOT call onSuccess/onError/onSettled,
48
- * - and it MUST NOT overwrite state from a newer run.
109
+ * @example
110
+ * ```ts
111
+ * const mutation = createMutation({
112
+ * mutationFn: async (signal, data) => {
113
+ * const response = await fetch('/api/items', {
114
+ * method: 'POST',
115
+ * body: JSON.stringify(data),
116
+ * signal
117
+ * });
118
+ * return response.json();
119
+ * },
120
+ * invalidateTags: ['items'],
121
+ * onSuccess: (result) => {
122
+ * console.log('Item created:', result);
123
+ * }
124
+ * });
49
125
  *
50
- * Invalidation:
51
- * - Runs only after a successful, non-aborted mutation.
52
- * - invalidateTags: revalidates all cached resources registered for those tags.
53
- * - invalidateKeys: revalidates specific cached resources by encoded key.
126
+ * // Execute the mutation
127
+ * mutation.run({ name: 'New Item' });
128
+ * ```
54
129
  */
55
- export declare function createMutation<TInput, TResult>(cfg: MutationConfig<TInput, TResult>): MutationState<TInput, TResult>;
130
+ export declare function createMutation<TInput, TResult, TContext = unknown>(cfg: MutationConfig<TInput, TResult, TContext>): MutationState<TInput, TResult>;
@@ -3,39 +3,47 @@ import { getCurrentScope } from "./scope.js";
3
3
  import { encodeKey } from "./key.js";
4
4
  import { invalidateResourceCache, invalidateResourceTags } from "./resource.js";
5
5
  /**
6
- * Mutation primitive (scope-safe).
6
+ * Creates a mutation - a write operation that executes side effects
7
+ * and can invalidate related queries to maintain cache consistency.
7
8
  *
8
- * Design goals:
9
- * - DOM-first friendly: mutations are just async actions with reactive state.
10
- * - Scope-safe: abort on scope disposal (best-effort cleanup).
11
- * - Dedupe-by-default: concurrent `run()` calls share the same in-flight promise.
12
- * - Force re-run: abort the current request and start a new one.
13
- * - React Query-like behavior: keep the last successful `data()` until overwritten or reset.
9
+ * Follows the Mutation pattern from React Query/TanStack Query:
10
+ * - Provides lifecycle callbacks (onMutate, onSuccess, onError, onSettled)
11
+ * - Supports automatic cache invalidation by tags or keys
12
+ * - Automatically manages cancellation via AbortSignal
14
13
  *
15
- * Semantics:
16
- * - Each run uses its own AbortController.
17
- * - If a run is aborted:
18
- * - it returns null,
19
- * - it MUST NOT call onSuccess/onError/onSettled,
20
- * - and it MUST NOT overwrite state from a newer run.
14
+ * @template TInput - Type of input data
15
+ * @template TResult - Type of expected result
16
+ * @template TContext - Type of context for communication between callbacks
17
+ * @param cfg - Mutation configuration
18
+ * @returns Mutation state with methods to execute and control
21
19
  *
22
- * Invalidation:
23
- * - Runs only after a successful, non-aborted mutation.
24
- * - invalidateTags: revalidates all cached resources registered for those tags.
25
- * - invalidateKeys: revalidates specific cached resources by encoded key.
20
+ * @example
21
+ * ```ts
22
+ * const mutation = createMutation({
23
+ * mutationFn: async (signal, data) => {
24
+ * const response = await fetch('/api/items', {
25
+ * method: 'POST',
26
+ * body: JSON.stringify(data),
27
+ * signal
28
+ * });
29
+ * return response.json();
30
+ * },
31
+ * invalidateTags: ['items'],
32
+ * onSuccess: (result) => {
33
+ * console.log('Item created:', result);
34
+ * }
35
+ * });
36
+ *
37
+ * // Execute the mutation
38
+ * mutation.run({ name: 'New Item' });
39
+ * ```
26
40
  */
27
41
  export function createMutation(cfg) {
28
42
  const data = signal(null);
29
43
  const loading = signal(false);
30
44
  const error = signal(null);
31
- /** In-flight promise for dedupe (represents the latest started run). */
32
45
  let inFlight = null;
33
- /** AbortController for the latest started run (used for force + scope cleanup). */
34
46
  let controller = null;
35
- /**
36
- * If created inside a scope, abort the active run when the scope is disposed.
37
- * This prevents orphan network work and avoids updating dead UI.
38
- */
39
47
  const scope = getCurrentScope();
40
48
  if (scope) {
41
49
  scope.onCleanup(() => {
@@ -44,22 +52,15 @@ export function createMutation(cfg) {
44
52
  inFlight = null;
45
53
  });
46
54
  }
55
+ const mutationFn = cfg.mutationFn ?? cfg.mutate;
56
+ if (!mutationFn) {
57
+ throw new Error("createMutation requires mutationFn or mutate");
58
+ }
47
59
  async function run(input, opts = {}) {
48
- /**
49
- * Dedupe:
50
- * - If a run is already loading and we're not forcing, await the current promise.
51
- * - Snapshot `inFlight` to avoid races if a forced run starts mid-await.
52
- */
53
60
  if (loading() && !opts.force) {
54
- const p0 = inFlight;
55
- return (await (p0 ?? Promise.resolve(null)));
61
+ const pending = inFlight;
62
+ return (await (pending ?? Promise.resolve(null)));
56
63
  }
57
- /**
58
- * Start a new run:
59
- * - Abort previous run (if any).
60
- * - Create a fresh controller/signal for this run.
61
- * - Capture controller identity so older runs cannot clobber newer state.
62
- */
63
64
  controller?.abort();
64
65
  controller = new AbortController();
65
66
  const sig = controller.signal;
@@ -67,14 +68,35 @@ export function createMutation(cfg) {
67
68
  loading.set(true);
68
69
  error.set(null);
69
70
  inFlight = (async () => {
71
+ let context = undefined;
72
+ try {
73
+ if (cfg.onMutate) {
74
+ context = await cfg.onMutate(input);
75
+ }
76
+ }
77
+ catch (e) {
78
+ if (sig.aborted)
79
+ return null;
80
+ const err = e instanceof Error ? e : new Error(String(e));
81
+ error.set(err);
82
+ if (controller === localController)
83
+ loading.set(false);
84
+ try {
85
+ cfg.onError?.(err, input, context);
86
+ cfg.onSettled?.(null, err, input, context);
87
+ }
88
+ catch {
89
+ }
90
+ return null;
91
+ }
92
+ if (sig.aborted)
93
+ return null;
70
94
  try {
71
- const result = await cfg.mutate(sig, input);
72
- // Aborted runs never commit state or call callbacks.
95
+ const result = await mutationFn(sig, input);
73
96
  if (sig.aborted)
74
97
  return null;
75
98
  data.set(result);
76
- cfg.onSuccess?.(result, input);
77
- // Invalidate only after a successful, non-aborted mutation.
99
+ cfg.onSuccess?.(result, input, context);
78
100
  if (cfg.invalidateTags && cfg.invalidateTags.length > 0) {
79
101
  invalidateResourceTags(cfg.invalidateTags, { revalidate: true, force: true });
80
102
  }
@@ -83,39 +105,25 @@ export function createMutation(cfg) {
83
105
  invalidateResourceCache(encodeKey(k), { revalidate: true, force: true });
84
106
  }
85
107
  }
108
+ cfg.onSettled?.(result, null, input, context);
86
109
  return result;
87
110
  }
88
111
  catch (e) {
89
- // Aborted runs are treated as null (no error state, no callbacks).
90
112
  if (sig.aborted)
91
113
  return null;
92
114
  const err = e instanceof Error ? e : new Error(String(e));
93
115
  error.set(err);
94
- cfg.onError?.(err, input);
116
+ cfg.onError?.(err, input, context);
117
+ cfg.onSettled?.(null, err, input, context);
95
118
  return null;
96
119
  }
97
120
  finally {
98
- /**
99
- * Only the latest run is allowed to update `loading`.
100
- *
101
- * Why?
102
- * - If run A is aborted because run B starts, A's finally will still execute.
103
- * - Without this guard, A could flip loading(false) while B is still running.
104
- */
105
- const stillCurrent = controller === localController;
106
- if (stillCurrent)
121
+ if (controller === localController)
107
122
  loading.set(false);
108
- // Keep onSettled consistent with onSuccess/onError: never run it for aborted runs.
109
- if (!sig.aborted)
110
- cfg.onSettled?.(input);
111
123
  }
112
124
  })();
113
125
  return await inFlight;
114
126
  }
115
- /**
116
- * Resets local state and aborts any active run.
117
- * Does not touch the resource/query cache.
118
- */
119
127
  function reset() {
120
128
  controller?.abort();
121
129
  controller = null;