@lolyjs/core 0.2.0-alpha.3 → 0.2.0-alpha.30

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 (46) hide show
  1. package/README.md +1463 -761
  2. package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
  3. package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
  4. package/dist/cli.cjs +15701 -2448
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.js +15704 -2441
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.cjs +17861 -4115
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.mts +323 -55
  11. package/dist/index.d.ts +323 -55
  12. package/dist/index.js +17982 -4227
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.types-B9j4OQft.d.mts +222 -0
  15. package/dist/index.types-B9j4OQft.d.ts +222 -0
  16. package/dist/react/cache.cjs +107 -32
  17. package/dist/react/cache.cjs.map +1 -1
  18. package/dist/react/cache.d.mts +29 -21
  19. package/dist/react/cache.d.ts +29 -21
  20. package/dist/react/cache.js +107 -32
  21. package/dist/react/cache.js.map +1 -1
  22. package/dist/react/components.cjs +11 -12
  23. package/dist/react/components.cjs.map +1 -1
  24. package/dist/react/components.js +11 -12
  25. package/dist/react/components.js.map +1 -1
  26. package/dist/react/hooks.cjs +124 -74
  27. package/dist/react/hooks.cjs.map +1 -1
  28. package/dist/react/hooks.d.mts +6 -24
  29. package/dist/react/hooks.d.ts +6 -24
  30. package/dist/react/hooks.js +122 -71
  31. package/dist/react/hooks.js.map +1 -1
  32. package/dist/react/sockets.cjs +5 -6
  33. package/dist/react/sockets.cjs.map +1 -1
  34. package/dist/react/sockets.js +5 -6
  35. package/dist/react/sockets.js.map +1 -1
  36. package/dist/react/themes.cjs +61 -18
  37. package/dist/react/themes.cjs.map +1 -1
  38. package/dist/react/themes.js +63 -20
  39. package/dist/react/themes.js.map +1 -1
  40. package/dist/runtime.cjs +531 -104
  41. package/dist/runtime.cjs.map +1 -1
  42. package/dist/runtime.d.mts +2 -2
  43. package/dist/runtime.d.ts +2 -2
  44. package/dist/runtime.js +531 -104
  45. package/dist/runtime.js.map +1 -1
  46. package/package.json +56 -14
@@ -0,0 +1,222 @@
1
+ import { Request, Response } from 'express';
2
+
3
+ type GenerateStaticParams = () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;
4
+ interface ServerContext {
5
+ req: Request;
6
+ res: Response;
7
+ params: Record<string, string>;
8
+ pathname: string;
9
+ locals: Record<string, any>;
10
+ }
11
+ interface WssActions {
12
+ /**
13
+ * Emit to current socket only (reply)
14
+ */
15
+ reply(event: string, payload?: any): void;
16
+ /**
17
+ * Emit an event to all clients in the namespace
18
+ */
19
+ emit: (event: string, ...args: any[]) => void;
20
+ /**
21
+ * Emit to everyone except current socket
22
+ */
23
+ broadcast(event: string, payload?: any, opts?: {
24
+ excludeSelf?: boolean;
25
+ }): void;
26
+ /**
27
+ * Join a room
28
+ */
29
+ join(room: string): Promise<void>;
30
+ /**
31
+ * Leave a room
32
+ */
33
+ leave(room: string): Promise<void>;
34
+ /**
35
+ * Emit to a specific room
36
+ */
37
+ toRoom(room: string): {
38
+ emit(event: string, payload?: any): void;
39
+ };
40
+ /**
41
+ * Emit to a specific user (by userId)
42
+ * Uses presence mapping to find user's sockets
43
+ */
44
+ toUser(userId: string): {
45
+ emit(event: string, payload?: any): void;
46
+ };
47
+ /**
48
+ * Emit error event (reserved event: __loly:error)
49
+ */
50
+ error(code: string, message: string, details?: any): void;
51
+ /**
52
+ * Emit an event to a specific socket by Socket.IO socket ID
53
+ * @deprecated Use toUser() for user targeting
54
+ */
55
+ emitTo?: (socketId: string, event: string, ...args: any[]) => void;
56
+ /**
57
+ * Emit an event to a specific client by custom clientId
58
+ * @deprecated Use toUser() for user targeting
59
+ */
60
+ emitToClient?: (clientId: string, event: string, ...args: any[]) => void;
61
+ }
62
+ /**
63
+ * Route middleware function type.
64
+ * Middlewares run before getServerSideProps and can modify ctx.locals, set headers, redirect, etc.
65
+ *
66
+ * @param ctx - Server context with optional theme
67
+ * @param next - Function to call the next middleware in the chain (must be awaited if used)
68
+ * @returns Promise<void> | void
69
+ *
70
+ * @example
71
+ * // Simple middleware that adds data to ctx.locals
72
+ * export const beforeServerData: RouteMiddleware[] = [
73
+ * async (ctx, next) => {
74
+ * ctx.locals.user = await getUser(ctx.req);
75
+ * await next();
76
+ * }
77
+ * ];
78
+ *
79
+ * @example
80
+ * // Middleware that redirects
81
+ * export const beforeServerData: RouteMiddleware[] = [
82
+ * async (ctx, next) => {
83
+ * if (!ctx.locals.user) {
84
+ * ctx.res.redirect('/login');
85
+ * return; // Don't call next() if redirecting
86
+ * }
87
+ * await next();
88
+ * }
89
+ * ];
90
+ */
91
+ type RouteMiddleware = (ctx: ServerContext & {
92
+ theme?: string;
93
+ }, next: () => Promise<void>) => Promise<void> | void;
94
+ /**
95
+ * Result returned by a server loader (getServerSideProps).
96
+ * @template TProps - Type of props that will be passed to the component (defaults to Record<string, any>)
97
+ */
98
+ interface LoaderResult<TProps extends Record<string, any> = Record<string, any>> {
99
+ pathname?: string;
100
+ props?: TProps;
101
+ redirect?: {
102
+ destination: string;
103
+ permanent?: boolean;
104
+ };
105
+ notFound?: boolean;
106
+ metadata?: PageMetadata | null;
107
+ className?: string;
108
+ theme?: string;
109
+ }
110
+ /**
111
+ * Server loader function type (getServerSideProps).
112
+ * This function is exported from server.hook.ts files.
113
+ *
114
+ * @template TProps - Type of props that will be returned (defaults to Record<string, any>)
115
+ *
116
+ * @example
117
+ * // Typed loader
118
+ * export const getServerSideProps: ServerLoader<{ user: User; posts: Post[] }> = async (ctx) => ({
119
+ * props: { user: await getUser(), posts: await getPosts() }
120
+ * });
121
+ *
122
+ * @example
123
+ * // Untyped loader (backward compatible)
124
+ * export const getServerSideProps: ServerLoader = async (ctx) => ({
125
+ * props: { any: 'data' }
126
+ * });
127
+ */
128
+ type ServerLoader<TProps extends Record<string, any> = Record<string, any>> = (ctx: ServerContext) => Promise<LoaderResult<TProps>>;
129
+ /**
130
+ * Comprehensive page metadata for SEO and social sharing.
131
+ * Supports standard HTML meta tags, Open Graph, Twitter Cards, and more.
132
+ */
133
+ interface PageMetadata {
134
+ /** Page title (sets <title> tag) */
135
+ title?: string;
136
+ /** Page description (sets <meta name="description">) */
137
+ description?: string;
138
+ /** Language code (sets <html lang="...">) */
139
+ lang?: string;
140
+ /** Canonical URL (sets <link rel="canonical">) */
141
+ canonical?: string;
142
+ /** Robots directive (sets <meta name="robots">) */
143
+ robots?: string;
144
+ /** Theme color (sets <meta name="theme-color">) */
145
+ themeColor?: string;
146
+ /** Viewport configuration (sets <meta name="viewport">) */
147
+ viewport?: string;
148
+ /** Open Graph metadata for social sharing */
149
+ openGraph?: {
150
+ title?: string;
151
+ description?: string;
152
+ type?: string;
153
+ url?: string;
154
+ image?: string | {
155
+ url: string;
156
+ width?: number;
157
+ height?: number;
158
+ alt?: string;
159
+ };
160
+ siteName?: string;
161
+ locale?: string;
162
+ };
163
+ /** Twitter Card metadata */
164
+ twitter?: {
165
+ card?: "summary" | "summary_large_image" | "app" | "player";
166
+ title?: string;
167
+ description?: string;
168
+ image?: string;
169
+ imageAlt?: string;
170
+ site?: string;
171
+ creator?: string;
172
+ };
173
+ /** Additional custom meta tags */
174
+ metaTags?: {
175
+ name?: string;
176
+ property?: string;
177
+ httpEquiv?: string;
178
+ content: string;
179
+ }[];
180
+ /** Additional link tags (e.g., preconnect, dns-prefetch) */
181
+ links?: {
182
+ rel: string;
183
+ href: string;
184
+ as?: string;
185
+ crossorigin?: string;
186
+ type?: string;
187
+ }[];
188
+ }
189
+ type MetadataLoader = (ctx: ServerContext) => PageMetadata | Promise<PageMetadata>;
190
+ interface ApiContext {
191
+ req: Request;
192
+ res: Response;
193
+ Response: (body?: any, status?: number) => Response<any, Record<string, any>>;
194
+ NotFound: (body?: any) => Response<any, Record<string, any>>;
195
+ params: Record<string, string>;
196
+ pathname: string;
197
+ locals: Record<string, any>;
198
+ }
199
+ /**
200
+ * API middleware function type.
201
+ * Middlewares run before the API handler and can modify ctx.locals, set headers, etc.
202
+ *
203
+ * @param ctx - API context
204
+ * @param next - Function to call the next middleware in the chain (must be awaited if used)
205
+ * @returns Promise<void> | void
206
+ *
207
+ * @example
208
+ * // Authentication middleware
209
+ * export const middlewares: ApiMiddleware[] = [
210
+ * async (ctx, next) => {
211
+ * const token = ctx.req.headers.authorization;
212
+ * if (!token) {
213
+ * return ctx.Response({ error: 'Unauthorized' }, 401);
214
+ * }
215
+ * ctx.locals.user = await verifyToken(token);
216
+ * await next();
217
+ * }
218
+ * ];
219
+ */
220
+ type ApiMiddleware = (ctx: ApiContext, next: () => Promise<void>) => void | Promise<void>;
221
+
222
+ export type { ApiMiddleware as A, GenerateStaticParams as G, LoaderResult as L, MetadataLoader as M, PageMetadata as P, RouteMiddleware as R, ServerContext as S, WssActions as W, ApiContext as a, ServerLoader as b };
@@ -0,0 +1,222 @@
1
+ import { Request, Response } from 'express';
2
+
3
+ type GenerateStaticParams = () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;
4
+ interface ServerContext {
5
+ req: Request;
6
+ res: Response;
7
+ params: Record<string, string>;
8
+ pathname: string;
9
+ locals: Record<string, any>;
10
+ }
11
+ interface WssActions {
12
+ /**
13
+ * Emit to current socket only (reply)
14
+ */
15
+ reply(event: string, payload?: any): void;
16
+ /**
17
+ * Emit an event to all clients in the namespace
18
+ */
19
+ emit: (event: string, ...args: any[]) => void;
20
+ /**
21
+ * Emit to everyone except current socket
22
+ */
23
+ broadcast(event: string, payload?: any, opts?: {
24
+ excludeSelf?: boolean;
25
+ }): void;
26
+ /**
27
+ * Join a room
28
+ */
29
+ join(room: string): Promise<void>;
30
+ /**
31
+ * Leave a room
32
+ */
33
+ leave(room: string): Promise<void>;
34
+ /**
35
+ * Emit to a specific room
36
+ */
37
+ toRoom(room: string): {
38
+ emit(event: string, payload?: any): void;
39
+ };
40
+ /**
41
+ * Emit to a specific user (by userId)
42
+ * Uses presence mapping to find user's sockets
43
+ */
44
+ toUser(userId: string): {
45
+ emit(event: string, payload?: any): void;
46
+ };
47
+ /**
48
+ * Emit error event (reserved event: __loly:error)
49
+ */
50
+ error(code: string, message: string, details?: any): void;
51
+ /**
52
+ * Emit an event to a specific socket by Socket.IO socket ID
53
+ * @deprecated Use toUser() for user targeting
54
+ */
55
+ emitTo?: (socketId: string, event: string, ...args: any[]) => void;
56
+ /**
57
+ * Emit an event to a specific client by custom clientId
58
+ * @deprecated Use toUser() for user targeting
59
+ */
60
+ emitToClient?: (clientId: string, event: string, ...args: any[]) => void;
61
+ }
62
+ /**
63
+ * Route middleware function type.
64
+ * Middlewares run before getServerSideProps and can modify ctx.locals, set headers, redirect, etc.
65
+ *
66
+ * @param ctx - Server context with optional theme
67
+ * @param next - Function to call the next middleware in the chain (must be awaited if used)
68
+ * @returns Promise<void> | void
69
+ *
70
+ * @example
71
+ * // Simple middleware that adds data to ctx.locals
72
+ * export const beforeServerData: RouteMiddleware[] = [
73
+ * async (ctx, next) => {
74
+ * ctx.locals.user = await getUser(ctx.req);
75
+ * await next();
76
+ * }
77
+ * ];
78
+ *
79
+ * @example
80
+ * // Middleware that redirects
81
+ * export const beforeServerData: RouteMiddleware[] = [
82
+ * async (ctx, next) => {
83
+ * if (!ctx.locals.user) {
84
+ * ctx.res.redirect('/login');
85
+ * return; // Don't call next() if redirecting
86
+ * }
87
+ * await next();
88
+ * }
89
+ * ];
90
+ */
91
+ type RouteMiddleware = (ctx: ServerContext & {
92
+ theme?: string;
93
+ }, next: () => Promise<void>) => Promise<void> | void;
94
+ /**
95
+ * Result returned by a server loader (getServerSideProps).
96
+ * @template TProps - Type of props that will be passed to the component (defaults to Record<string, any>)
97
+ */
98
+ interface LoaderResult<TProps extends Record<string, any> = Record<string, any>> {
99
+ pathname?: string;
100
+ props?: TProps;
101
+ redirect?: {
102
+ destination: string;
103
+ permanent?: boolean;
104
+ };
105
+ notFound?: boolean;
106
+ metadata?: PageMetadata | null;
107
+ className?: string;
108
+ theme?: string;
109
+ }
110
+ /**
111
+ * Server loader function type (getServerSideProps).
112
+ * This function is exported from server.hook.ts files.
113
+ *
114
+ * @template TProps - Type of props that will be returned (defaults to Record<string, any>)
115
+ *
116
+ * @example
117
+ * // Typed loader
118
+ * export const getServerSideProps: ServerLoader<{ user: User; posts: Post[] }> = async (ctx) => ({
119
+ * props: { user: await getUser(), posts: await getPosts() }
120
+ * });
121
+ *
122
+ * @example
123
+ * // Untyped loader (backward compatible)
124
+ * export const getServerSideProps: ServerLoader = async (ctx) => ({
125
+ * props: { any: 'data' }
126
+ * });
127
+ */
128
+ type ServerLoader<TProps extends Record<string, any> = Record<string, any>> = (ctx: ServerContext) => Promise<LoaderResult<TProps>>;
129
+ /**
130
+ * Comprehensive page metadata for SEO and social sharing.
131
+ * Supports standard HTML meta tags, Open Graph, Twitter Cards, and more.
132
+ */
133
+ interface PageMetadata {
134
+ /** Page title (sets <title> tag) */
135
+ title?: string;
136
+ /** Page description (sets <meta name="description">) */
137
+ description?: string;
138
+ /** Language code (sets <html lang="...">) */
139
+ lang?: string;
140
+ /** Canonical URL (sets <link rel="canonical">) */
141
+ canonical?: string;
142
+ /** Robots directive (sets <meta name="robots">) */
143
+ robots?: string;
144
+ /** Theme color (sets <meta name="theme-color">) */
145
+ themeColor?: string;
146
+ /** Viewport configuration (sets <meta name="viewport">) */
147
+ viewport?: string;
148
+ /** Open Graph metadata for social sharing */
149
+ openGraph?: {
150
+ title?: string;
151
+ description?: string;
152
+ type?: string;
153
+ url?: string;
154
+ image?: string | {
155
+ url: string;
156
+ width?: number;
157
+ height?: number;
158
+ alt?: string;
159
+ };
160
+ siteName?: string;
161
+ locale?: string;
162
+ };
163
+ /** Twitter Card metadata */
164
+ twitter?: {
165
+ card?: "summary" | "summary_large_image" | "app" | "player";
166
+ title?: string;
167
+ description?: string;
168
+ image?: string;
169
+ imageAlt?: string;
170
+ site?: string;
171
+ creator?: string;
172
+ };
173
+ /** Additional custom meta tags */
174
+ metaTags?: {
175
+ name?: string;
176
+ property?: string;
177
+ httpEquiv?: string;
178
+ content: string;
179
+ }[];
180
+ /** Additional link tags (e.g., preconnect, dns-prefetch) */
181
+ links?: {
182
+ rel: string;
183
+ href: string;
184
+ as?: string;
185
+ crossorigin?: string;
186
+ type?: string;
187
+ }[];
188
+ }
189
+ type MetadataLoader = (ctx: ServerContext) => PageMetadata | Promise<PageMetadata>;
190
+ interface ApiContext {
191
+ req: Request;
192
+ res: Response;
193
+ Response: (body?: any, status?: number) => Response<any, Record<string, any>>;
194
+ NotFound: (body?: any) => Response<any, Record<string, any>>;
195
+ params: Record<string, string>;
196
+ pathname: string;
197
+ locals: Record<string, any>;
198
+ }
199
+ /**
200
+ * API middleware function type.
201
+ * Middlewares run before the API handler and can modify ctx.locals, set headers, etc.
202
+ *
203
+ * @param ctx - API context
204
+ * @param next - Function to call the next middleware in the chain (must be awaited if used)
205
+ * @returns Promise<void> | void
206
+ *
207
+ * @example
208
+ * // Authentication middleware
209
+ * export const middlewares: ApiMiddleware[] = [
210
+ * async (ctx, next) => {
211
+ * const token = ctx.req.headers.authorization;
212
+ * if (!token) {
213
+ * return ctx.Response({ error: 'Unauthorized' }, 401);
214
+ * }
215
+ * ctx.locals.user = await verifyToken(token);
216
+ * await next();
217
+ * }
218
+ * ];
219
+ */
220
+ type ApiMiddleware = (ctx: ApiContext, next: () => Promise<void>) => void | Promise<void>;
221
+
222
+ export type { ApiMiddleware as A, GenerateStaticParams as G, LoaderResult as L, MetadataLoader as M, PageMetadata as P, RouteMiddleware as R, ServerContext as S, WssActions as W, ApiContext as a, ServerLoader as b };
@@ -113,14 +113,16 @@ function deleteCacheEntry(key) {
113
113
  function buildDataUrl(url) {
114
114
  return url + (url.includes("?") ? "&" : "?") + "__fw_data=1";
115
115
  }
116
- async function fetchRouteDataOnce(url) {
116
+ async function fetchRouteDataOnce(url, skipLayoutHooks = true) {
117
117
  const dataUrl = buildDataUrl(url);
118
- const res = await fetch(dataUrl, {
119
- headers: {
120
- "x-fw-data": "1",
121
- Accept: "application/json"
122
- }
123
- });
118
+ const headers = {
119
+ "x-fw-data": "1",
120
+ Accept: "application/json"
121
+ };
122
+ if (skipLayoutHooks) {
123
+ headers["x-skip-layout-hooks"] = "true";
124
+ }
125
+ const res = await fetch(dataUrl, { headers });
124
126
  let json = {};
125
127
  try {
126
128
  const text = await res.text();
@@ -140,7 +142,7 @@ async function fetchRouteDataOnce(url) {
140
142
  };
141
143
  return result;
142
144
  }
143
- function revalidatePath(path) {
145
+ function revalidatePath(path, skipAutoRevalidate = false) {
144
146
  const normalizedPath = path.split("?")[0];
145
147
  const hasQueryParams = path.includes("?");
146
148
  const keysForPath = pathIndex.get(normalizedPath);
@@ -167,30 +169,86 @@ function revalidatePath(path) {
167
169
  keysToDelete.forEach((key) => {
168
170
  deleteCacheEntry(key);
169
171
  });
172
+ if (!skipAutoRevalidate && typeof window !== "undefined") {
173
+ const currentPathname = window.location.pathname;
174
+ const currentSearch = window.location.search;
175
+ const matchesCurrentPath = normalizedPath === currentPathname;
176
+ if (matchesCurrentPath) {
177
+ if (hasQueryParams && specificQueryParams) {
178
+ const currentQueryParams = currentSearch.replace("?", "").split("&").filter((p) => !p.startsWith("__fw_data=")).sort().join("&");
179
+ if (currentQueryParams === specificQueryParams) {
180
+ revalidate().catch((err) => {
181
+ console.error(
182
+ "[client][cache] Error revalidating current route:",
183
+ err
184
+ );
185
+ });
186
+ }
187
+ } else {
188
+ revalidate().catch((err) => {
189
+ console.error(
190
+ "[client][cache] Error revalidating current route:",
191
+ err
192
+ );
193
+ });
194
+ }
195
+ }
196
+ }
170
197
  }
198
+ var isRevalidating = false;
171
199
  async function revalidate() {
172
200
  if (typeof window === "undefined") {
173
201
  throw new Error("revalidate() can only be called on the client");
174
202
  }
175
- const pathname = window.location.pathname + window.location.search;
176
- revalidatePath(pathname);
177
- const freshData = await getRouteData(pathname, { revalidate: true });
178
- if (window.__FW_DATA__ && freshData.ok && freshData.json) {
179
- const currentData = window.__FW_DATA__;
180
- window.__FW_DATA__ = {
181
- ...currentData,
182
- pathname: pathname.split("?")[0],
183
- params: freshData.json.params || currentData.params || {},
184
- props: freshData.json.props || currentData.props || {},
185
- metadata: freshData.json.metadata ?? currentData.metadata ?? null,
186
- notFound: freshData.json.notFound ?? false,
187
- error: freshData.json.error ?? false
188
- };
189
- window.dispatchEvent(new CustomEvent("fw-data-refresh", {
190
- detail: { data: freshData }
191
- }));
192
- }
193
- return freshData;
203
+ if (isRevalidating) {
204
+ const key = buildDataUrl(window.location.pathname + window.location.search);
205
+ const entry = dataCache.get(key);
206
+ if (entry && entry.status === "pending") {
207
+ return entry.promise;
208
+ }
209
+ }
210
+ isRevalidating = true;
211
+ try {
212
+ const pathname = window.location.pathname + window.location.search;
213
+ revalidatePath(pathname, true);
214
+ const freshData = await getRouteData(pathname, { revalidate: true });
215
+ if (window.__FW_DATA__ && freshData.ok && freshData.json) {
216
+ const currentData = window.__FW_DATA__;
217
+ if (freshData.json.layoutProps !== void 0 && freshData.json.layoutProps !== null) {
218
+ window.__FW_LAYOUT_PROPS__ = freshData.json.layoutProps;
219
+ }
220
+ let combinedProps = currentData.props || {};
221
+ if (freshData.json.layoutProps !== void 0 && freshData.json.layoutProps !== null) {
222
+ combinedProps = {
223
+ ...freshData.json.layoutProps,
224
+ ...freshData.json.pageProps ?? freshData.json.props ?? {}
225
+ };
226
+ } else if (freshData.json.pageProps !== void 0) {
227
+ const preservedLayoutProps = window.__FW_LAYOUT_PROPS__ || {};
228
+ combinedProps = {
229
+ ...preservedLayoutProps,
230
+ ...freshData.json.pageProps
231
+ };
232
+ } else if (freshData.json.props) {
233
+ combinedProps = freshData.json.props;
234
+ }
235
+ window.__FW_DATA__ = {
236
+ ...currentData,
237
+ pathname: pathname.split("?")[0],
238
+ params: freshData.json.params || currentData.params || {},
239
+ props: combinedProps,
240
+ metadata: freshData.json.metadata ?? currentData.metadata ?? null,
241
+ notFound: freshData.json.notFound ?? false,
242
+ error: freshData.json.error ?? false
243
+ };
244
+ window.dispatchEvent(new CustomEvent("fw-data-refresh", {
245
+ detail: { data: freshData }
246
+ }));
247
+ }
248
+ return freshData;
249
+ } finally {
250
+ isRevalidating = false;
251
+ }
194
252
  }
195
253
  function revalidateRouteData(url) {
196
254
  revalidatePath(url);
@@ -204,7 +262,7 @@ function prefetchRouteData(url) {
204
262
  }
205
263
  return;
206
264
  }
207
- const promise = fetchRouteDataOnce(url).then((value) => {
265
+ const promise = fetchRouteDataOnce(url, true).then((value) => {
208
266
  setCacheEntry(key, { status: "fulfilled", value });
209
267
  return value;
210
268
  }).catch((error) => {
@@ -220,7 +278,7 @@ async function getRouteData(url, options) {
220
278
  deleteCacheEntry(key);
221
279
  }
222
280
  const entry = dataCache.get(key);
223
- if (entry) {
281
+ if (entry && !options?.revalidate) {
224
282
  if (entry.status === "fulfilled") {
225
283
  updateLRU(key);
226
284
  return entry.value;
@@ -229,12 +287,29 @@ async function getRouteData(url, options) {
229
287
  return entry.promise;
230
288
  }
231
289
  }
232
- const promise = fetchRouteDataOnce(url).then((value) => {
233
- setCacheEntry(key, { status: "fulfilled", value });
290
+ const skipLayoutHooks = !options?.revalidate;
291
+ const currentEntry = dataCache.get(key);
292
+ if (currentEntry && !options?.revalidate) {
293
+ if (currentEntry.status === "fulfilled") {
294
+ updateLRU(key);
295
+ return currentEntry.value;
296
+ }
297
+ if (currentEntry.status === "pending") {
298
+ return currentEntry.promise;
299
+ }
300
+ }
301
+ const promise = fetchRouteDataOnce(url, skipLayoutHooks).then((value) => {
302
+ const entryAfterFetch = dataCache.get(key);
303
+ if (!entryAfterFetch || entryAfterFetch.status === "pending") {
304
+ setCacheEntry(key, { status: "fulfilled", value });
305
+ }
234
306
  return value;
235
307
  }).catch((error) => {
236
308
  console.error("[client][cache] Error fetching route data:", error);
237
- dataCache.set(key, { status: "rejected", error });
309
+ const entryAfterFetch = dataCache.get(key);
310
+ if (!entryAfterFetch || entryAfterFetch.status === "pending") {
311
+ dataCache.set(key, { status: "rejected", error });
312
+ }
238
313
  throw error;
239
314
  });
240
315
  dataCache.set(key, { status: "pending", promise });