fict 0.1.0 → 0.2.1

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/dist/plus.d.cts CHANGED
@@ -1,48 +1,248 @@
1
- export { $ as $store } from './store-BmwKJIj6.cjs';
1
+ export { $ as $store } from './store-UpSYx4_N.cjs';
2
2
  import { Component } from '@fictjs/runtime';
3
3
 
4
+ /**
5
+ * @fileoverview Async data fetching with caching and Suspense support.
6
+ *
7
+ * The `resource` function creates a reactive data fetcher that:
8
+ * - Automatically cancels in-flight requests when args change
9
+ * - Supports Suspense for loading states
10
+ * - Provides caching with TTL and stale-while-revalidate
11
+ * - Handles errors gracefully
12
+ */
13
+ /**
14
+ * The result of reading a resource.
15
+ *
16
+ * @typeParam T - The type of data returned by the fetcher
17
+ */
4
18
  interface ResourceResult<T> {
5
- data: T | undefined;
6
- loading: boolean;
7
- error: unknown;
19
+ /** The fetched data, or undefined if not yet loaded or on error */
20
+ readonly data: T | undefined;
21
+ /** Whether the resource is currently loading (initial fetch or refetch) */
22
+ readonly loading: boolean;
23
+ /**
24
+ * Any error that occurred during fetching.
25
+ * Type is unknown since errors can be any value in JavaScript.
26
+ */
27
+ readonly error: unknown;
28
+ /** Manually trigger a refetch of the resource */
8
29
  refresh: () => void;
9
30
  }
31
+ /**
32
+ * Cache configuration options for a resource.
33
+ */
10
34
  interface ResourceCacheOptions {
35
+ /**
36
+ * Caching mode:
37
+ * - `'memory'`: Cache responses in memory (default)
38
+ * - `'none'`: No caching, always refetch
39
+ * @default 'memory'
40
+ */
11
41
  mode?: 'memory' | 'none';
42
+ /**
43
+ * Time-to-live in milliseconds before cached data is considered stale.
44
+ * @default Infinity
45
+ */
12
46
  ttlMs?: number;
47
+ /**
48
+ * If true, return stale cached data immediately while refetching in background.
49
+ * @default false
50
+ */
13
51
  staleWhileRevalidate?: boolean;
52
+ /**
53
+ * If true, cache error responses as well.
54
+ * @default false
55
+ */
14
56
  cacheErrors?: boolean;
15
57
  }
58
+ /**
59
+ * Configuration options for creating a resource.
60
+ *
61
+ * @typeParam T - The type of data returned by the fetcher
62
+ * @typeParam Args - The type of arguments passed to the fetcher
63
+ */
16
64
  interface ResourceOptions<T, Args> {
17
- key?: unknown;
65
+ /**
66
+ * Custom cache key. Can be a static value or a function that computes
67
+ * the key from the args. If not provided, args are used as the key.
68
+ */
69
+ key?: unknown | ((args: Args) => unknown);
70
+ /**
71
+ * The fetcher function that performs the async data retrieval.
72
+ * Receives an AbortController signal for cancellation support.
73
+ */
18
74
  fetch: (ctx: {
19
75
  signal: AbortSignal;
20
76
  }, args: Args) => Promise<T>;
77
+ /**
78
+ * If true, the resource will throw a Suspense token while loading,
79
+ * enabling React-like Suspense boundaries.
80
+ * @default false
81
+ */
21
82
  suspense?: boolean;
83
+ /**
84
+ * Cache configuration options.
85
+ */
22
86
  cache?: ResourceCacheOptions;
87
+ /**
88
+ * A value or reactive getter that, when changed, resets the resource.
89
+ * Useful for clearing cache when certain conditions change.
90
+ */
23
91
  reset?: unknown | (() => unknown);
24
92
  }
25
93
  /**
26
- * Creates a resource factory that can be read with arguments.
94
+ * Return type of the resource factory.
27
95
  *
28
- * @param optionsOrFetcher - Configuration object or fetcher function
96
+ * @typeParam T - The type of data returned by the fetcher
97
+ * @typeParam Args - The type of arguments passed to the fetcher
98
+ */
99
+ interface Resource<T, Args> {
100
+ /**
101
+ * Read the resource data, triggering a fetch if needed.
102
+ * Can accept static args or a reactive getter.
103
+ *
104
+ * @param argsAccessor - Arguments or a getter returning arguments
105
+ */
106
+ read(argsAccessor: (() => Args) | Args): ResourceResult<T>;
107
+ /**
108
+ * Invalidate cached data, causing the next read to refetch.
109
+ *
110
+ * @param key - Optional specific key to invalidate. If omitted, invalidates all.
111
+ */
112
+ invalidate(key?: unknown): void;
113
+ /**
114
+ * Prefetch data without reading it. Useful for eager loading.
115
+ *
116
+ * @param args - Arguments to pass to the fetcher
117
+ * @param keyOverride - Optional cache key override
118
+ */
119
+ prefetch(args: Args, keyOverride?: unknown): void;
120
+ }
121
+ /**
122
+ * Create a reactive async data resource.
123
+ *
124
+ * Resources handle async data fetching with automatic caching, cancellation,
125
+ * and optional Suspense integration.
126
+ *
127
+ * @param optionsOrFetcher - A fetcher function or full configuration object
128
+ * @returns A resource factory with read, invalidate, and prefetch methods
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * import { resource } from 'fict'
133
+ *
134
+ * // Simple fetcher
135
+ * const userResource = resource(
136
+ * ({ signal }, userId: string) =>
137
+ * fetch(`/api/users/${userId}`, { signal }).then(r => r.json())
138
+ * )
139
+ *
140
+ * // With full options
141
+ * const postsResource = resource({
142
+ * fetch: ({ signal }, userId: string) =>
143
+ * fetch(`/api/users/${userId}/posts`, { signal }).then(r => r.json()),
144
+ * suspense: true,
145
+ * cache: {
146
+ * ttlMs: 60_000,
147
+ * staleWhileRevalidate: true,
148
+ * },
149
+ * })
150
+ *
151
+ * // Usage in component
152
+ * function UserProfile({ userId }: { userId: string }) {
153
+ * const { data, loading, error, refresh } = userResource.read(() => userId)
154
+ *
155
+ * if (loading) return <Spinner />
156
+ * if (error) return <ErrorMessage error={error} />
157
+ * return <div>{data.name}</div>
158
+ * }
159
+ * ```
160
+ *
161
+ * @public
29
162
  */
30
163
  declare function resource<T, Args = void>(optionsOrFetcher: ((ctx: {
31
164
  signal: AbortSignal;
32
- }, args: Args) => Promise<T>) | ResourceOptions<T, Args>): {
33
- read(argsAccessor: (() => Args) | Args): ResourceResult<T>;
34
- invalidate: (key?: unknown) => void;
35
- prefetch: (args: Args, keyOverride?: unknown) => void;
36
- };
165
+ }, args: Args) => Promise<T>) | ResourceOptions<T, Args>): Resource<T, Args>;
166
+
167
+ /**
168
+ * @fileoverview Lazy component loading with Suspense support.
169
+ *
170
+ * Creates a component that loads its implementation asynchronously,
171
+ * suspending rendering until the module is loaded.
172
+ */
37
173
 
174
+ /** Module shape expected from dynamic imports */
38
175
  interface LazyModule<TProps extends Record<string, unknown>> {
39
176
  default: Component<TProps>;
40
177
  }
178
+ /** Options for lazy loading behavior */
179
+ interface LazyOptions {
180
+ /**
181
+ * Maximum number of retry attempts on load failure.
182
+ * Set to 0 to disable retries (default behavior).
183
+ * @default 0
184
+ */
185
+ maxRetries?: number;
186
+ /**
187
+ * Delay in milliseconds between retry attempts.
188
+ * Uses exponential backoff: delay * 2^(attempt - 1)
189
+ * @default 1000
190
+ */
191
+ retryDelay?: number;
192
+ }
193
+ /** Extended component with retry capability */
194
+ interface LazyComponent<TProps extends Record<string, unknown>> extends Component<TProps> {
195
+ /**
196
+ * Reset the lazy component state, allowing it to retry loading.
197
+ * Useful when used with ErrorBoundary reset functionality.
198
+ */
199
+ reset: () => void;
200
+ /**
201
+ * Preload the component without rendering it.
202
+ * Returns a promise that resolves when the component is loaded.
203
+ */
204
+ preload: () => Promise<void>;
205
+ }
41
206
  /**
42
207
  * Create a lazy component that suspends while loading.
208
+ *
209
+ * @param loader - Function that returns a promise resolving to the component module
210
+ * @param options - Optional configuration for retry behavior
211
+ * @returns A component that suspends during loading and supports retry on failure
212
+ *
213
+ * @example
214
+ * ```tsx
215
+ * import { lazy, Suspense } from 'fict'
216
+ *
217
+ * // Basic usage
218
+ * const LazyChart = lazy(() => import('./Chart'))
219
+ *
220
+ * // With retry options
221
+ * const LazyDashboard = lazy(() => import('./Dashboard'), {
222
+ * maxRetries: 3,
223
+ * retryDelay: 1000
224
+ * })
225
+ *
226
+ * function App() {
227
+ * return (
228
+ * <Suspense fallback={<Loading />}>
229
+ * <LazyChart />
230
+ * </Suspense>
231
+ * )
232
+ * }
233
+ *
234
+ * // Reset on error (with ErrorBoundary)
235
+ * <ErrorBoundary fallback={(err, reset) => (
236
+ * <button onClick={() => { LazyChart.reset(); reset(); }}>Retry</button>
237
+ * )}>
238
+ * <LazyChart />
239
+ * </ErrorBoundary>
240
+ * ```
241
+ *
242
+ * @public
43
243
  */
44
244
  declare function lazy<TProps extends Record<string, unknown> = Record<string, unknown>>(loader: () => Promise<LazyModule<TProps> | {
45
245
  default: Component<TProps>;
46
- }>): Component<TProps>;
246
+ }>, options?: LazyOptions): LazyComponent<TProps>;
47
247
 
48
248
  export { type LazyModule, type ResourceCacheOptions, type ResourceOptions, type ResourceResult, lazy, resource };
package/dist/plus.d.ts CHANGED
@@ -1,48 +1,248 @@
1
- export { $ as $store } from './store-BmwKJIj6.js';
1
+ export { $ as $store } from './store-UpSYx4_N.js';
2
2
  import { Component } from '@fictjs/runtime';
3
3
 
4
+ /**
5
+ * @fileoverview Async data fetching with caching and Suspense support.
6
+ *
7
+ * The `resource` function creates a reactive data fetcher that:
8
+ * - Automatically cancels in-flight requests when args change
9
+ * - Supports Suspense for loading states
10
+ * - Provides caching with TTL and stale-while-revalidate
11
+ * - Handles errors gracefully
12
+ */
13
+ /**
14
+ * The result of reading a resource.
15
+ *
16
+ * @typeParam T - The type of data returned by the fetcher
17
+ */
4
18
  interface ResourceResult<T> {
5
- data: T | undefined;
6
- loading: boolean;
7
- error: unknown;
19
+ /** The fetched data, or undefined if not yet loaded or on error */
20
+ readonly data: T | undefined;
21
+ /** Whether the resource is currently loading (initial fetch or refetch) */
22
+ readonly loading: boolean;
23
+ /**
24
+ * Any error that occurred during fetching.
25
+ * Type is unknown since errors can be any value in JavaScript.
26
+ */
27
+ readonly error: unknown;
28
+ /** Manually trigger a refetch of the resource */
8
29
  refresh: () => void;
9
30
  }
31
+ /**
32
+ * Cache configuration options for a resource.
33
+ */
10
34
  interface ResourceCacheOptions {
35
+ /**
36
+ * Caching mode:
37
+ * - `'memory'`: Cache responses in memory (default)
38
+ * - `'none'`: No caching, always refetch
39
+ * @default 'memory'
40
+ */
11
41
  mode?: 'memory' | 'none';
42
+ /**
43
+ * Time-to-live in milliseconds before cached data is considered stale.
44
+ * @default Infinity
45
+ */
12
46
  ttlMs?: number;
47
+ /**
48
+ * If true, return stale cached data immediately while refetching in background.
49
+ * @default false
50
+ */
13
51
  staleWhileRevalidate?: boolean;
52
+ /**
53
+ * If true, cache error responses as well.
54
+ * @default false
55
+ */
14
56
  cacheErrors?: boolean;
15
57
  }
58
+ /**
59
+ * Configuration options for creating a resource.
60
+ *
61
+ * @typeParam T - The type of data returned by the fetcher
62
+ * @typeParam Args - The type of arguments passed to the fetcher
63
+ */
16
64
  interface ResourceOptions<T, Args> {
17
- key?: unknown;
65
+ /**
66
+ * Custom cache key. Can be a static value or a function that computes
67
+ * the key from the args. If not provided, args are used as the key.
68
+ */
69
+ key?: unknown | ((args: Args) => unknown);
70
+ /**
71
+ * The fetcher function that performs the async data retrieval.
72
+ * Receives an AbortController signal for cancellation support.
73
+ */
18
74
  fetch: (ctx: {
19
75
  signal: AbortSignal;
20
76
  }, args: Args) => Promise<T>;
77
+ /**
78
+ * If true, the resource will throw a Suspense token while loading,
79
+ * enabling React-like Suspense boundaries.
80
+ * @default false
81
+ */
21
82
  suspense?: boolean;
83
+ /**
84
+ * Cache configuration options.
85
+ */
22
86
  cache?: ResourceCacheOptions;
87
+ /**
88
+ * A value or reactive getter that, when changed, resets the resource.
89
+ * Useful for clearing cache when certain conditions change.
90
+ */
23
91
  reset?: unknown | (() => unknown);
24
92
  }
25
93
  /**
26
- * Creates a resource factory that can be read with arguments.
94
+ * Return type of the resource factory.
27
95
  *
28
- * @param optionsOrFetcher - Configuration object or fetcher function
96
+ * @typeParam T - The type of data returned by the fetcher
97
+ * @typeParam Args - The type of arguments passed to the fetcher
98
+ */
99
+ interface Resource<T, Args> {
100
+ /**
101
+ * Read the resource data, triggering a fetch if needed.
102
+ * Can accept static args or a reactive getter.
103
+ *
104
+ * @param argsAccessor - Arguments or a getter returning arguments
105
+ */
106
+ read(argsAccessor: (() => Args) | Args): ResourceResult<T>;
107
+ /**
108
+ * Invalidate cached data, causing the next read to refetch.
109
+ *
110
+ * @param key - Optional specific key to invalidate. If omitted, invalidates all.
111
+ */
112
+ invalidate(key?: unknown): void;
113
+ /**
114
+ * Prefetch data without reading it. Useful for eager loading.
115
+ *
116
+ * @param args - Arguments to pass to the fetcher
117
+ * @param keyOverride - Optional cache key override
118
+ */
119
+ prefetch(args: Args, keyOverride?: unknown): void;
120
+ }
121
+ /**
122
+ * Create a reactive async data resource.
123
+ *
124
+ * Resources handle async data fetching with automatic caching, cancellation,
125
+ * and optional Suspense integration.
126
+ *
127
+ * @param optionsOrFetcher - A fetcher function or full configuration object
128
+ * @returns A resource factory with read, invalidate, and prefetch methods
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * import { resource } from 'fict'
133
+ *
134
+ * // Simple fetcher
135
+ * const userResource = resource(
136
+ * ({ signal }, userId: string) =>
137
+ * fetch(`/api/users/${userId}`, { signal }).then(r => r.json())
138
+ * )
139
+ *
140
+ * // With full options
141
+ * const postsResource = resource({
142
+ * fetch: ({ signal }, userId: string) =>
143
+ * fetch(`/api/users/${userId}/posts`, { signal }).then(r => r.json()),
144
+ * suspense: true,
145
+ * cache: {
146
+ * ttlMs: 60_000,
147
+ * staleWhileRevalidate: true,
148
+ * },
149
+ * })
150
+ *
151
+ * // Usage in component
152
+ * function UserProfile({ userId }: { userId: string }) {
153
+ * const { data, loading, error, refresh } = userResource.read(() => userId)
154
+ *
155
+ * if (loading) return <Spinner />
156
+ * if (error) return <ErrorMessage error={error} />
157
+ * return <div>{data.name}</div>
158
+ * }
159
+ * ```
160
+ *
161
+ * @public
29
162
  */
30
163
  declare function resource<T, Args = void>(optionsOrFetcher: ((ctx: {
31
164
  signal: AbortSignal;
32
- }, args: Args) => Promise<T>) | ResourceOptions<T, Args>): {
33
- read(argsAccessor: (() => Args) | Args): ResourceResult<T>;
34
- invalidate: (key?: unknown) => void;
35
- prefetch: (args: Args, keyOverride?: unknown) => void;
36
- };
165
+ }, args: Args) => Promise<T>) | ResourceOptions<T, Args>): Resource<T, Args>;
166
+
167
+ /**
168
+ * @fileoverview Lazy component loading with Suspense support.
169
+ *
170
+ * Creates a component that loads its implementation asynchronously,
171
+ * suspending rendering until the module is loaded.
172
+ */
37
173
 
174
+ /** Module shape expected from dynamic imports */
38
175
  interface LazyModule<TProps extends Record<string, unknown>> {
39
176
  default: Component<TProps>;
40
177
  }
178
+ /** Options for lazy loading behavior */
179
+ interface LazyOptions {
180
+ /**
181
+ * Maximum number of retry attempts on load failure.
182
+ * Set to 0 to disable retries (default behavior).
183
+ * @default 0
184
+ */
185
+ maxRetries?: number;
186
+ /**
187
+ * Delay in milliseconds between retry attempts.
188
+ * Uses exponential backoff: delay * 2^(attempt - 1)
189
+ * @default 1000
190
+ */
191
+ retryDelay?: number;
192
+ }
193
+ /** Extended component with retry capability */
194
+ interface LazyComponent<TProps extends Record<string, unknown>> extends Component<TProps> {
195
+ /**
196
+ * Reset the lazy component state, allowing it to retry loading.
197
+ * Useful when used with ErrorBoundary reset functionality.
198
+ */
199
+ reset: () => void;
200
+ /**
201
+ * Preload the component without rendering it.
202
+ * Returns a promise that resolves when the component is loaded.
203
+ */
204
+ preload: () => Promise<void>;
205
+ }
41
206
  /**
42
207
  * Create a lazy component that suspends while loading.
208
+ *
209
+ * @param loader - Function that returns a promise resolving to the component module
210
+ * @param options - Optional configuration for retry behavior
211
+ * @returns A component that suspends during loading and supports retry on failure
212
+ *
213
+ * @example
214
+ * ```tsx
215
+ * import { lazy, Suspense } from 'fict'
216
+ *
217
+ * // Basic usage
218
+ * const LazyChart = lazy(() => import('./Chart'))
219
+ *
220
+ * // With retry options
221
+ * const LazyDashboard = lazy(() => import('./Dashboard'), {
222
+ * maxRetries: 3,
223
+ * retryDelay: 1000
224
+ * })
225
+ *
226
+ * function App() {
227
+ * return (
228
+ * <Suspense fallback={<Loading />}>
229
+ * <LazyChart />
230
+ * </Suspense>
231
+ * )
232
+ * }
233
+ *
234
+ * // Reset on error (with ErrorBoundary)
235
+ * <ErrorBoundary fallback={(err, reset) => (
236
+ * <button onClick={() => { LazyChart.reset(); reset(); }}>Retry</button>
237
+ * )}>
238
+ * <LazyChart />
239
+ * </ErrorBoundary>
240
+ * ```
241
+ *
242
+ * @public
43
243
  */
44
244
  declare function lazy<TProps extends Record<string, unknown> = Record<string, unknown>>(loader: () => Promise<LazyModule<TProps> | {
45
245
  default: Component<TProps>;
46
- }>): Component<TProps>;
246
+ }>, options?: LazyOptions): LazyComponent<TProps>;
47
247
 
48
248
  export { type LazyModule, type ResourceCacheOptions, type ResourceOptions, type ResourceResult, lazy, resource };
package/dist/plus.js CHANGED
@@ -1,4 +1,4 @@
1
- export { $store } from './chunk-5AWNWCDT.js';
1
+ export { $store } from './chunk-T5BGEI6S.js';
2
2
  import { createEffect, createSuspenseToken, onCleanup } from '@fictjs/runtime';
3
3
  import { createSignal } from '@fictjs/runtime/advanced';
4
4
 
@@ -207,12 +207,38 @@ function resource(optionsOrFetcher) {
207
207
  prefetch
208
208
  };
209
209
  }
210
- function lazy(loader) {
210
+ function lazy(loader, options = {}) {
211
+ const { maxRetries = 0, retryDelay = 1e3 } = options;
211
212
  let loaded = null;
212
213
  let loadError = null;
213
214
  let loadingPromise = null;
214
215
  let pendingToken = null;
215
- return (props) => {
216
+ let retryCount = 0;
217
+ const attemptLoad = () => {
218
+ return loader().then((mod) => {
219
+ loaded = mod.default;
220
+ loadError = null;
221
+ retryCount = 0;
222
+ pendingToken?.resolve();
223
+ }).catch((err) => {
224
+ if (retryCount < maxRetries) {
225
+ retryCount++;
226
+ const delay = retryDelay * Math.pow(2, retryCount - 1);
227
+ return new Promise((resolve) => {
228
+ setTimeout(() => {
229
+ resolve(attemptLoad());
230
+ }, delay);
231
+ });
232
+ }
233
+ loadError = err;
234
+ pendingToken?.reject(err);
235
+ return void 0;
236
+ }).finally(() => {
237
+ loadingPromise = null;
238
+ pendingToken = null;
239
+ });
240
+ };
241
+ const component = ((props) => {
216
242
  if (loaded) {
217
243
  return loaded(props);
218
244
  }
@@ -221,22 +247,31 @@ function lazy(loader) {
221
247
  }
222
248
  if (!loadingPromise) {
223
249
  pendingToken = createSuspenseToken();
224
- loadingPromise = loader().then((mod) => {
225
- loaded = mod.default;
226
- pendingToken?.resolve();
227
- }).catch((err) => {
228
- loadError = err;
229
- pendingToken?.reject(err);
230
- }).finally(() => {
231
- loadingPromise = null;
232
- pendingToken = null;
233
- });
250
+ loadingPromise = attemptLoad();
234
251
  }
235
252
  if (pendingToken) {
236
253
  throw pendingToken.token;
237
254
  }
238
255
  throw new Error("Lazy component failed to start loading");
256
+ });
257
+ component.reset = () => {
258
+ loadError = null;
259
+ loadingPromise = null;
260
+ pendingToken = null;
261
+ retryCount = 0;
262
+ };
263
+ component.preload = () => {
264
+ if (loaded) {
265
+ return Promise.resolve();
266
+ }
267
+ if (loadingPromise) {
268
+ return loadingPromise;
269
+ }
270
+ pendingToken = createSuspenseToken();
271
+ loadingPromise = attemptLoad();
272
+ return loadingPromise;
239
273
  };
274
+ return component;
240
275
  }
241
276
 
242
277
  export { lazy, resource };