@flightdev/data 0.0.6

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.
@@ -0,0 +1,398 @@
1
+ // src/cache.ts
2
+ var LRUCache = class {
3
+ map = /* @__PURE__ */ new Map();
4
+ head = null;
5
+ tail = null;
6
+ size = 0;
7
+ maxSize;
8
+ defaultMaxAge;
9
+ constructor(options = {}) {
10
+ this.maxSize = options.maxSize ?? 100;
11
+ this.defaultMaxAge = options.defaultMaxAge ?? 5 * 60 * 1e3;
12
+ }
13
+ /**
14
+ * Get value from cache
15
+ * Returns undefined if not found or expired
16
+ */
17
+ get(key) {
18
+ const entry = this.map.get(key);
19
+ if (!entry) {
20
+ return void 0;
21
+ }
22
+ if (this.isExpired(entry)) {
23
+ this.delete(key);
24
+ return void 0;
25
+ }
26
+ this.moveToFront(entry);
27
+ return entry.value;
28
+ }
29
+ /**
30
+ * Set value in cache
31
+ */
32
+ set(key, value, options = {}) {
33
+ const existingEntry = this.map.get(key);
34
+ if (existingEntry) {
35
+ existingEntry.value = value;
36
+ existingEntry.timestamp = Date.now();
37
+ existingEntry.maxAge = options.maxAge ?? this.defaultMaxAge;
38
+ this.moveToFront(existingEntry);
39
+ return;
40
+ }
41
+ const entry = {
42
+ key,
43
+ value,
44
+ timestamp: Date.now(),
45
+ maxAge: options.maxAge ?? this.defaultMaxAge,
46
+ prev: null,
47
+ next: null
48
+ };
49
+ this.map.set(key, entry);
50
+ this.size++;
51
+ this.addToFront(entry);
52
+ if (this.size > this.maxSize) {
53
+ this.evictLRU();
54
+ }
55
+ }
56
+ /**
57
+ * Delete entry from cache
58
+ */
59
+ delete(key) {
60
+ const entry = this.map.get(key);
61
+ if (!entry) {
62
+ return false;
63
+ }
64
+ this.removeFromList(entry);
65
+ this.map.delete(key);
66
+ this.size--;
67
+ return true;
68
+ }
69
+ /**
70
+ * Check if key exists and is not expired
71
+ */
72
+ has(key) {
73
+ const entry = this.map.get(key);
74
+ if (!entry) {
75
+ return false;
76
+ }
77
+ if (this.isExpired(entry)) {
78
+ this.delete(key);
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ /**
84
+ * Clear all entries
85
+ */
86
+ clear() {
87
+ this.map.clear();
88
+ this.head = null;
89
+ this.tail = null;
90
+ this.size = 0;
91
+ }
92
+ /**
93
+ * Get current cache size
94
+ */
95
+ getSize() {
96
+ return this.size;
97
+ }
98
+ /**
99
+ * Get all keys (for invalidation patterns)
100
+ */
101
+ keys() {
102
+ return Array.from(this.map.keys());
103
+ }
104
+ /**
105
+ * Invalidate entries matching a pattern
106
+ */
107
+ invalidate(pattern) {
108
+ let count = 0;
109
+ const keysToDelete = [];
110
+ for (const key of this.map.keys()) {
111
+ let shouldDelete = false;
112
+ if (typeof pattern === "string") {
113
+ shouldDelete = key === pattern || key.startsWith(pattern);
114
+ } else if (pattern instanceof RegExp) {
115
+ shouldDelete = pattern.test(key);
116
+ } else {
117
+ shouldDelete = pattern(key);
118
+ }
119
+ if (shouldDelete) {
120
+ keysToDelete.push(key);
121
+ }
122
+ }
123
+ for (const key of keysToDelete) {
124
+ this.delete(key);
125
+ count++;
126
+ }
127
+ return count;
128
+ }
129
+ // ========================================================================
130
+ // Private Methods
131
+ // ========================================================================
132
+ isExpired(entry) {
133
+ if (!entry.maxAge) return false;
134
+ return Date.now() - entry.timestamp > entry.maxAge;
135
+ }
136
+ addToFront(entry) {
137
+ entry.next = this.head;
138
+ entry.prev = null;
139
+ if (this.head) {
140
+ this.head.prev = entry;
141
+ }
142
+ this.head = entry;
143
+ if (!this.tail) {
144
+ this.tail = entry;
145
+ }
146
+ }
147
+ removeFromList(entry) {
148
+ if (entry.prev) {
149
+ entry.prev.next = entry.next;
150
+ } else {
151
+ this.head = entry.next;
152
+ }
153
+ if (entry.next) {
154
+ entry.next.prev = entry.prev;
155
+ } else {
156
+ this.tail = entry.prev;
157
+ }
158
+ entry.prev = null;
159
+ entry.next = null;
160
+ }
161
+ moveToFront(entry) {
162
+ if (this.head === entry) {
163
+ return;
164
+ }
165
+ this.removeFromList(entry);
166
+ this.addToFront(entry);
167
+ }
168
+ evictLRU() {
169
+ if (!this.tail) return;
170
+ const lruKey = this.tail.key;
171
+ this.removeFromList(this.tail);
172
+ this.map.delete(lruKey);
173
+ this.size--;
174
+ }
175
+ };
176
+ var globalCache = null;
177
+ function getCache() {
178
+ if (!globalCache) {
179
+ globalCache = new LRUCache();
180
+ }
181
+ return globalCache;
182
+ }
183
+ function configureCache(options) {
184
+ globalCache = new LRUCache(options);
185
+ }
186
+ function clearCache() {
187
+ if (globalCache) {
188
+ globalCache.clear();
189
+ }
190
+ }
191
+ function invalidateCache(pattern) {
192
+ if (!globalCache) return 0;
193
+ return globalCache.invalidate(pattern);
194
+ }
195
+
196
+ // src/fetcher.ts
197
+ var inFlightRequests = /* @__PURE__ */ new Map();
198
+ async function deduplicatedFetch(key, fetcher, options = {}) {
199
+ const { dedupe = true } = options;
200
+ if (!dedupe) {
201
+ return fetcher();
202
+ }
203
+ const existing = inFlightRequests.get(key);
204
+ if (existing) {
205
+ existing.subscribers++;
206
+ return existing.promise;
207
+ }
208
+ const controller = new AbortController();
209
+ const promise = fetcher();
210
+ const inFlight = {
211
+ promise,
212
+ controller,
213
+ subscribers: 1
214
+ };
215
+ inFlightRequests.set(key, inFlight);
216
+ try {
217
+ const result = await promise;
218
+ return result;
219
+ } finally {
220
+ inFlightRequests.delete(key);
221
+ }
222
+ }
223
+ function cancelRequest(key) {
224
+ const inFlight = inFlightRequests.get(key);
225
+ if (inFlight) {
226
+ inFlight.controller.abort();
227
+ inFlightRequests.delete(key);
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+ async function fetchData(url, options = {}) {
233
+ if (!url) {
234
+ return {
235
+ data: options.default,
236
+ error: void 0,
237
+ status: "idle",
238
+ isStale: false
239
+ };
240
+ }
241
+ const {
242
+ key = url,
243
+ cache: useCache = true,
244
+ dedupe = true,
245
+ maxAge,
246
+ transform,
247
+ fetchOptions,
248
+ signal
249
+ } = options;
250
+ const cacheInstance = getCache();
251
+ if (useCache) {
252
+ const cached = cacheInstance.get(key);
253
+ if (cached !== void 0) {
254
+ return {
255
+ data: cached,
256
+ error: void 0,
257
+ status: "success",
258
+ isStale: false
259
+ };
260
+ }
261
+ }
262
+ try {
263
+ const result = await deduplicatedFetch(key, async () => {
264
+ const response = await fetch(url, {
265
+ ...fetchOptions,
266
+ signal,
267
+ headers: {
268
+ "Accept": "application/json",
269
+ ...fetchOptions?.headers
270
+ }
271
+ });
272
+ if (!response.ok) {
273
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
274
+ }
275
+ const data = await response.json();
276
+ return transform ? transform(data) : data;
277
+ }, { dedupe });
278
+ if (useCache) {
279
+ cacheInstance.set(key, result, { maxAge });
280
+ }
281
+ return {
282
+ data: result,
283
+ error: void 0,
284
+ status: "success",
285
+ isStale: false
286
+ };
287
+ } catch (error) {
288
+ if (useCache) {
289
+ const stale = cacheInstance.get(key);
290
+ if (stale !== void 0) {
291
+ return {
292
+ data: stale,
293
+ error: error instanceof Error ? error : new Error(String(error)),
294
+ status: "error",
295
+ isStale: true
296
+ };
297
+ }
298
+ }
299
+ return {
300
+ data: options.default,
301
+ error: error instanceof Error ? error : new Error(String(error)),
302
+ status: "error",
303
+ isStale: false
304
+ };
305
+ }
306
+ }
307
+ async function asyncData(key, fetcher, options = {}) {
308
+ const { maxAge, transform } = options;
309
+ const cacheInstance = getCache();
310
+ const cached = cacheInstance.get(key);
311
+ if (cached !== void 0) {
312
+ return {
313
+ data: cached,
314
+ error: void 0,
315
+ status: "success",
316
+ isStale: false
317
+ };
318
+ }
319
+ try {
320
+ const result = await deduplicatedFetch(key, fetcher);
321
+ const transformed = transform ? transform(result) : result;
322
+ cacheInstance.set(key, transformed, { maxAge });
323
+ return {
324
+ data: transformed,
325
+ error: void 0,
326
+ status: "success",
327
+ isStale: false
328
+ };
329
+ } catch (error) {
330
+ return {
331
+ data: options.default,
332
+ error: error instanceof Error ? error : new Error(String(error)),
333
+ status: "error",
334
+ isStale: false
335
+ };
336
+ }
337
+ }
338
+ function invalidate(pattern) {
339
+ for (const key of inFlightRequests.keys()) {
340
+ let shouldCancel = false;
341
+ if (typeof pattern === "string") {
342
+ shouldCancel = key === pattern || key.startsWith(pattern);
343
+ } else if (pattern instanceof RegExp) {
344
+ shouldCancel = pattern.test(key);
345
+ } else {
346
+ shouldCancel = pattern(key);
347
+ }
348
+ if (shouldCancel) {
349
+ cancelRequest(key);
350
+ }
351
+ }
352
+ return invalidateCache(pattern);
353
+ }
354
+ async function prefetch(url, options) {
355
+ await fetchData(url, { ...options, immediate: true });
356
+ }
357
+ function getCachedData(key) {
358
+ return getCache().get(key);
359
+ }
360
+ function setCachedData(key, data, options) {
361
+ getCache().set(key, data, options);
362
+ }
363
+ var isBrowser = typeof window !== "undefined";
364
+ function createFetchHydrationScript(data) {
365
+ const serialized = JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
366
+ return `<script>window.__FLIGHT_FETCH_DATA__ = ${serialized};</script>`;
367
+ }
368
+ function hydrateFetchData() {
369
+ if (!isBrowser) return;
370
+ const hydrated = window.__FLIGHT_FETCH_DATA__;
371
+ if (!hydrated) return;
372
+ const cache = getCache();
373
+ for (const [key, value] of Object.entries(hydrated)) {
374
+ cache.set(key, value);
375
+ }
376
+ delete window.__FLIGHT_FETCH_DATA__;
377
+ }
378
+ var isServer = !isBrowser;
379
+ var isClient = isBrowser;
380
+
381
+ export {
382
+ LRUCache,
383
+ getCache,
384
+ configureCache,
385
+ clearCache,
386
+ invalidateCache,
387
+ cancelRequest,
388
+ fetchData,
389
+ asyncData,
390
+ invalidate,
391
+ prefetch,
392
+ getCachedData,
393
+ setCachedData,
394
+ createFetchHydrationScript,
395
+ hydrateFetchData,
396
+ isServer,
397
+ isClient
398
+ };
@@ -0,0 +1,277 @@
1
+ export { C as CacheOptions, j as FetchState, F as FetchStatus, L as LRUCache, S as SetOptions, l as UseAsyncDataOptions, m as UseAsyncDataReturn, U as UseFetchOptions, k as UseFetchReturn, a as asyncData, c as cancelRequest, q as clearCache, o as configureCache, b as createFetchHydrationScript, f as fetchData, n as getCache, g as getCachedData, h as hydrateFetchData, i as invalidate, r as invalidateCache, e as isClient, d as isServer, p as prefetch, s as setCachedData } from './cache-DU1v4CKe.js';
2
+
3
+ /**
4
+ * Type definitions for @flightdev/data
5
+ */
6
+ /**
7
+ * Context passed to loader functions
8
+ */
9
+ interface LoaderContext {
10
+ /** Route parameters from dynamic segments */
11
+ params: Record<string, string>;
12
+ /** Incoming request object (server-side only) */
13
+ request: Request;
14
+ /** URL search params */
15
+ searchParams: URLSearchParams;
16
+ }
17
+ /**
18
+ * Loader function signature
19
+ * Runs on the server before rendering the page
20
+ */
21
+ type LoaderFunction<T = unknown> = (context: LoaderContext) => Promise<T> | T;
22
+ /**
23
+ * Context passed to action functions
24
+ */
25
+ interface ActionContext {
26
+ /** Route parameters from dynamic segments */
27
+ params: Record<string, string>;
28
+ /** Incoming request with form data */
29
+ request: Request;
30
+ /** URL search params */
31
+ searchParams: URLSearchParams;
32
+ }
33
+ /**
34
+ * Action function signature
35
+ * Handles form submissions and mutations
36
+ */
37
+ type ActionFunction<T = unknown> = (context: ActionContext) => Promise<T | Response> | T | Response;
38
+ /**
39
+ * Utility type to extract the return type of a loader
40
+ * Removes Promise wrapper if present
41
+ */
42
+ type SerializeFrom<T> = T extends (...args: unknown[]) => infer R ? R extends Promise<infer U> ? U : R : never;
43
+ /**
44
+ * Fetcher state for client-side data fetching
45
+ */
46
+ type FetcherState<T = unknown> = {
47
+ state: 'idle';
48
+ data: undefined;
49
+ error: undefined;
50
+ } | {
51
+ state: 'loading';
52
+ data: undefined;
53
+ error: undefined;
54
+ } | {
55
+ state: 'success';
56
+ data: T;
57
+ error: undefined;
58
+ } | {
59
+ state: 'error';
60
+ data: undefined;
61
+ error: Error;
62
+ };
63
+
64
+ /**
65
+ * @flightdev/data - Data Hooks
66
+ *
67
+ * Framework-agnostic data hooks with React 19 support.
68
+ *
69
+ * Philosophy:
70
+ * - Zero external dependencies
71
+ * - Framework agnostic (React, Vue, Solid, etc.)
72
+ * - User decides how to provide/consume data
73
+ * - SSR/Hydration support built-in
74
+ *
75
+ * @module @flightdev/data/hooks
76
+ */
77
+
78
+ /**
79
+ * Set loader data in store (used during SSR hydration)
80
+ */
81
+ declare function setLoaderData(key: string, data: unknown): void;
82
+ /**
83
+ * Get loader data from store
84
+ */
85
+ declare function getLoaderData<T>(key: string): T | undefined;
86
+ /**
87
+ * Set action data in store
88
+ */
89
+ declare function setActionData(key: string, data: unknown): void;
90
+ /**
91
+ * Get action data from store
92
+ */
93
+ declare function getActionData<T>(key: string): T | undefined;
94
+ /**
95
+ * Clear all data stores (for testing/cleanup)
96
+ */
97
+ declare function clearDataStores(): void;
98
+ /**
99
+ * Universal `use()` hook - Suspense-aware promise unwrapping
100
+ *
101
+ * Works like React 19's use() hook but framework-agnostic.
102
+ * For React: integrates with Suspense
103
+ * For others: provides consistent API
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const data = use(fetchData());
108
+ * return <div>{data.name}</div>;
109
+ * ```
110
+ */
111
+ declare function use<T>(promise: Promise<T>): T;
112
+ /**
113
+ * useLoaderData - Access loader data
114
+ *
115
+ * Framework-aware: Uses React hooks when available,
116
+ * falls back to store-based implementation otherwise.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * export async function loader({ params }: LoaderContext) {
121
+ * const user = await db.users.find(params.id);
122
+ * return { user };
123
+ * }
124
+ *
125
+ * export default function UserPage() {
126
+ * const { user } = useLoaderData<typeof loader>();
127
+ * return <h1>{user.name}</h1>;
128
+ * }
129
+ * ```
130
+ */
131
+ declare function useLoaderData<T extends (...args: never[]) => unknown>(): SerializeFrom<T>;
132
+ /**
133
+ * useActionData - Access action result
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * export async function action({ request }: ActionContext) {
138
+ * const formData = await request.formData();
139
+ * const result = await processForm(formData);
140
+ * return result;
141
+ * }
142
+ *
143
+ * export default function FormPage() {
144
+ * const actionData = useActionData<typeof action>();
145
+ * return actionData?.error ? <p>{actionData.error}</p> : null;
146
+ * }
147
+ * ```
148
+ */
149
+ declare function useActionData<T>(): T | undefined;
150
+ /**
151
+ * useFetcher - Client-side data fetching
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * function SearchBox() {
156
+ * const fetcher = useFetcher<SearchResult[]>();
157
+ *
158
+ * return (
159
+ * <div>
160
+ * <input
161
+ * onChange={e => fetcher.load(`/api/search?q=${e.target.value}`)}
162
+ * />
163
+ * {fetcher.state === 'loading' && <Spinner />}
164
+ * {fetcher.data?.map(result => (
165
+ * <SearchResult key={result.id} {...result} />
166
+ * ))}
167
+ * {fetcher.error && <ErrorMessage error={fetcher.error} />}
168
+ * </div>
169
+ * );
170
+ * }
171
+ * ```
172
+ */
173
+ declare function useFetcher<T>(): {
174
+ state: FetcherState<T>['state'];
175
+ data: T | undefined;
176
+ error: Error | undefined;
177
+ submit: (data: FormData | Record<string, string>, options?: {
178
+ method?: string;
179
+ action?: string;
180
+ }) => void;
181
+ load: (href: string) => void;
182
+ reset: () => void;
183
+ };
184
+ /**
185
+ * Create hydration script for SSR
186
+ * Injects loader data into the page for client hydration
187
+ */
188
+ declare function createHydrationScript(pathname: string, loaderData: unknown, actionData?: unknown): string;
189
+ /**
190
+ * Hydrate data from SSR script (call on client initialization)
191
+ */
192
+ declare function hydrateFromWindow(): void;
193
+
194
+ /**
195
+ * Form Component
196
+ *
197
+ * Enhanced form that integrates with Flight actions.
198
+ * Handles submission, loading states, and redirects.
199
+ */
200
+ declare let Form: unknown;
201
+
202
+ /**
203
+ * Response Utilities
204
+ *
205
+ * Helper functions for creating standard responses in loaders and actions.
206
+ */
207
+ /**
208
+ * Create a redirect response
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * export async function action({ request }: ActionContext) {
213
+ * await saveData(request);
214
+ * return redirect('/success');
215
+ * }
216
+ * ```
217
+ */
218
+ declare function redirect(url: string, init?: ResponseInit): Response;
219
+ /**
220
+ * Create a JSON response
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * export async function loader() {
225
+ * const data = await fetchData();
226
+ * return json({ data });
227
+ * }
228
+ * ```
229
+ */
230
+ declare function json<T>(data: T, init?: ResponseInit): Response;
231
+ /**
232
+ * Create a deferred response for streaming SSR
233
+ * Allows returning promises that resolve during streaming
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * export async function loader() {
238
+ * return defer({
239
+ * // Critical data - blocks render
240
+ * user: await getUser(),
241
+ * // Deferred data - streams in later
242
+ * posts: getPosts(), // No await!
243
+ * });
244
+ * }
245
+ * ```
246
+ */
247
+ declare function defer<T extends Record<string, unknown>>(data: T): T;
248
+ /**
249
+ * Check if a response is a redirect
250
+ */
251
+ declare function isRedirectResponse(response: Response): boolean;
252
+ /**
253
+ * Check if a response is JSON
254
+ */
255
+ declare function isJsonResponse(response: Response): boolean;
256
+
257
+ /**
258
+ * Server Utilities
259
+ *
260
+ * Functions for running loaders and actions on the server.
261
+ * These are used by the Flight SSR runtime.
262
+ */
263
+
264
+ /**
265
+ * Run a loader function and return the result
266
+ */
267
+ declare function runLoader<T>(loader: LoaderFunction<T>, context: LoaderContext): Promise<T | Response>;
268
+ /**
269
+ * Run an action function and return the result
270
+ */
271
+ declare function runAction<T>(action: ActionFunction<T>, context: ActionContext): Promise<T | Response>;
272
+ /**
273
+ * Generate script tag for hydrating loader data
274
+ */
275
+ declare function hydrateLoaderData(pathname: string, data: unknown): string;
276
+
277
+ export { type ActionContext, type ActionFunction, Form, type LoaderContext, type LoaderFunction, type SerializeFrom, clearDataStores, createHydrationScript, defer, getActionData, getLoaderData, hydrateFromWindow, hydrateLoaderData, isJsonResponse, isRedirectResponse, json, redirect, runAction, runLoader, setActionData, setLoaderData, use, useActionData, useFetcher, useLoaderData };