@anaemia/core 0.0.1 → 0.1.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/config.d.ts +3 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/plugins/lightningcss.d.ts.map +1 -1
- package/dist/plugins/lightningcss.js +1 -2
- package/dist/runtime/context.browser.d.ts.map +1 -1
- package/dist/runtime/context.browser.js +3 -1
- package/dist/runtime/context.d.ts +5 -4
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/entry-client.jsx +1 -1
- package/dist/runtime/entry-server.d.ts.map +1 -1
- package/dist/runtime/entry-server.jsx +18 -29
- package/dist/runtime/resources.d.ts +1 -1
- package/dist/runtime/resources.d.ts.map +1 -1
- package/dist/runtime/resources.js +5 -3
- package/dist/runtime/route-data.d.ts +7 -6
- package/dist/runtime/route-data.d.ts.map +1 -1
- package/dist/runtime/route-data.js +5 -8
- package/dist/runtime/route-request.d.ts +1 -1
- package/dist/runtime/route-request.d.ts.map +1 -1
- package/dist/runtime/route-request.js +2 -1
- package/dist/runtime/rpc-client.d.ts +2 -2
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +12 -11
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/config.ts +3 -2
- package/src/index.ts +1 -6
- package/src/plugins/lightningcss.ts +7 -3
- package/src/runtime/context.browser.ts +3 -2
- package/src/runtime/context.ts +8 -13
- package/src/runtime/entry-client.tsx +1 -1
- package/src/runtime/entry-server.tsx +47 -47
- package/src/runtime/resources.ts +28 -16
- package/src/runtime/route-data.ts +24 -36
- package/src/runtime/route-request.ts +5 -4
- package/src/runtime/rpc-client.ts +44 -24
- package/src/runtime/webpack.d.ts +1 -1
- package/src/types.ts +7 -2
- package/test/integration/hmr.test.mjs +16 -22
package/src/runtime/context.ts
CHANGED
|
@@ -1,34 +1,29 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
export const ssrStorage = new AsyncLocalStorage<Map<string, any>>();
|
|
3
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
export const serverFunctionsRegistry = new Map<string, AnyFn>();
|
|
6
|
+
export const ssrStorage = new AsyncLocalStorage<Map<string, unknown>>();
|
|
7
|
+
(globalThis as unknown as Record<string, unknown>).__ANAEMIA_SERVER_STORAGE__ = ssrStorage;
|
|
7
8
|
|
|
8
|
-
export function runOnServer(backendFn:
|
|
9
|
+
export function runOnServer<T extends AnyFn>(backendFn: T, id?: string): T & { id: string } {
|
|
9
10
|
const hashId = id || "";
|
|
10
|
-
|
|
11
|
-
const rpcProxy = async function (...args: any[]) {
|
|
11
|
+
const rpcProxy = async function (...args: unknown[]) {
|
|
12
12
|
const result = await backendFn(...args);
|
|
13
|
-
|
|
14
13
|
const store = ssrStorage.getStore();
|
|
15
14
|
if (store && hashId) {
|
|
16
15
|
if (!store.has("__SERVER_FUNCTION_DATA__")) {
|
|
17
16
|
store.set("__SERVER_FUNCTION_DATA__", {});
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
const functionCache = store.get("__SERVER_FUNCTION_DATA__");
|
|
18
|
+
const functionCache = store.get("__SERVER_FUNCTION_DATA__") as Record<string, Record<string, unknown>>;
|
|
21
19
|
if (!functionCache[hashId]) {
|
|
22
20
|
functionCache[hashId] = {};
|
|
23
21
|
}
|
|
24
|
-
|
|
25
22
|
const paramKey = JSON.stringify(args);
|
|
26
23
|
functionCache[hashId][paramKey] = result;
|
|
27
24
|
}
|
|
28
|
-
|
|
29
25
|
return result;
|
|
30
26
|
};
|
|
31
|
-
|
|
32
27
|
rpcProxy.id = hashId;
|
|
33
|
-
return rpcProxy;
|
|
28
|
+
return rpcProxy as unknown as T & { id: string };
|
|
34
29
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hydrate, render } from "solid-js/web";
|
|
2
2
|
import { Router } from "@solidjs/router";
|
|
3
3
|
|
|
4
|
-
// @ts-
|
|
4
|
+
// @ts-expect-error - resolved by Rspack
|
|
5
5
|
import App, { preloadActiveClientRoute } from "anaemia-user-app";
|
|
6
6
|
|
|
7
7
|
const mountTarget = document.querySelector(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { serve } from "@hono/node-server";
|
|
3
|
+
import type { Context } from "hono";
|
|
3
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4
5
|
import { compress } from "hono/compress";
|
|
5
6
|
import { renderToStringAsync, generateHydrationScript } from "solid-js/web";
|
|
@@ -9,13 +10,13 @@ import fs from "node:fs";
|
|
|
9
10
|
import path from "path";
|
|
10
11
|
import type { StatusCode, RedirectStatusCode } from "hono/utils/http-status";
|
|
11
12
|
|
|
12
|
-
// @ts-
|
|
13
|
+
// @ts-expect-error - resolved by Rspack
|
|
13
14
|
import App from "anaemia-user-app";
|
|
14
15
|
|
|
15
|
-
// @ts-
|
|
16
|
+
// @ts-expect-error - resolved by Rspack
|
|
16
17
|
import { preloadActiveClientRoute, serverLoaderRegistry, serverGuardRegistry } from "anaemia-user-app";
|
|
17
18
|
|
|
18
|
-
// @ts-
|
|
19
|
+
// @ts-expect-error - resolved by Rspack
|
|
19
20
|
import { registerServerRoutes } from "__anaemia_server_routes__";
|
|
20
21
|
|
|
21
22
|
const port = Number(process.env.PORT) || 3000;
|
|
@@ -24,22 +25,34 @@ const isDev = process.env.NODE_ENV !== "production";
|
|
|
24
25
|
const devPort = Number(process.env.RSPACK_DEV_PORT) || 4445;
|
|
25
26
|
const devServerUrl = `http://localhost:${devPort}`;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
interface ChunkAssets {
|
|
29
|
+
js?: string[];
|
|
30
|
+
css?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RouteManifest {
|
|
34
|
+
routes: Array<{ urlPattern: string; chunkName: string; params: string[] }>;
|
|
35
|
+
chunks: Record<string, ChunkAssets>;
|
|
36
|
+
errors?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type SortedRoute = { urlPattern: string; chunkName: string; params: string[] };
|
|
40
|
+
let sortedRoutes: SortedRoute[] | null = null;
|
|
28
41
|
|
|
29
|
-
const ENTRY_TAG_REGEX = /(<([a-zA-Z0-9
|
|
42
|
+
const ENTRY_TAG_REGEX = /(<([a-zA-Z0-9-]+)[^>]*anaemia-entry[^>]*>)(.*?)(<\/\2>)/is;
|
|
30
43
|
|
|
31
44
|
const app = new Hono();
|
|
32
45
|
|
|
33
46
|
app.use("*", compress());
|
|
34
47
|
|
|
35
48
|
app.use("*", async (c, next) => {
|
|
36
|
-
const store = new Map<string,
|
|
49
|
+
const store = new Map<string, unknown>();
|
|
37
50
|
store.set("honoContext", c);
|
|
38
51
|
return await ssrStorage.run(store, next);
|
|
39
52
|
});
|
|
40
53
|
|
|
41
54
|
if (isDev) {
|
|
42
|
-
const devAssetProxy = async (c:
|
|
55
|
+
const devAssetProxy = async (c: Context) => {
|
|
43
56
|
const targetUrl = `${devServerUrl}${c.req.path}`;
|
|
44
57
|
try {
|
|
45
58
|
const response = await fetch(targetUrl);
|
|
@@ -53,7 +66,7 @@ if (isDev) {
|
|
|
53
66
|
c.header("Expires", "0");
|
|
54
67
|
|
|
55
68
|
return c.body(await response.arrayBuffer());
|
|
56
|
-
} catch
|
|
69
|
+
} catch {
|
|
57
70
|
return c.text("failed to connect to Rspack dev server asset bridge", 500);
|
|
58
71
|
}
|
|
59
72
|
};
|
|
@@ -96,8 +109,9 @@ app.post("/_rpc", async (c) => {
|
|
|
96
109
|
try {
|
|
97
110
|
const result = await serverFunctionsRegistry.get(functionId)!(...argumentsArray);
|
|
98
111
|
return c.json(result);
|
|
99
|
-
} catch (error
|
|
100
|
-
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
114
|
+
return c.json({ error: message }, 500);
|
|
101
115
|
}
|
|
102
116
|
});
|
|
103
117
|
|
|
@@ -108,12 +122,12 @@ app.use(async (c, next) => {
|
|
|
108
122
|
try {
|
|
109
123
|
const response = await fetch(targetUrl);
|
|
110
124
|
if (!response.ok) return c.text("hot update not found", 404);
|
|
111
|
-
|
|
125
|
+
|
|
112
126
|
const contentType = response.headers.get("content-type");
|
|
113
127
|
if (contentType) c.header("content-type", contentType);
|
|
114
|
-
|
|
128
|
+
|
|
115
129
|
c.header("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
116
|
-
|
|
130
|
+
|
|
117
131
|
return c.body(await response.arrayBuffer());
|
|
118
132
|
} catch {
|
|
119
133
|
return c.text("failed to fetch hot update", 500);
|
|
@@ -122,11 +136,10 @@ app.use(async (c, next) => {
|
|
|
122
136
|
await next();
|
|
123
137
|
});
|
|
124
138
|
|
|
125
|
-
|
|
126
139
|
registerServerRoutes(app);
|
|
127
140
|
|
|
128
141
|
let memoizedHtmlTemplate = "";
|
|
129
|
-
let memoizedManifest:
|
|
142
|
+
let memoizedManifest: RouteManifest | null = null;
|
|
130
143
|
|
|
131
144
|
const templatePath = path.resolve(process.cwd(), "./dist/client/index.html");
|
|
132
145
|
const manifestPath = path.resolve(process.cwd(), "./dist/route-manifest.json");
|
|
@@ -157,7 +170,7 @@ const loadManifestAndTemplate = async () => {
|
|
|
157
170
|
try {
|
|
158
171
|
if (fs.existsSync(templatePath)) memoizedHtmlTemplate = fs.readFileSync(templatePath, "utf-8");
|
|
159
172
|
if (fs.existsSync(manifestPath)) memoizedManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
160
|
-
} catch
|
|
173
|
+
} catch {
|
|
161
174
|
console.warn("build assets not fully initialized during bootstrapping cycle.");
|
|
162
175
|
}
|
|
163
176
|
}
|
|
@@ -192,9 +205,9 @@ async function runGuards(pattern: string, ctx: { params: Record<string, string>;
|
|
|
192
205
|
return null;
|
|
193
206
|
}
|
|
194
207
|
|
|
195
|
-
function matchRoute(manifest:
|
|
208
|
+
function matchRoute(manifest: RouteManifest, reqPath: string): RouteMatch {
|
|
196
209
|
if (!sortedRoutes) {
|
|
197
|
-
sortedRoutes = [...manifest.routes].sort((a
|
|
210
|
+
sortedRoutes = [...manifest.routes].sort((a, b) => {
|
|
198
211
|
const score = (pattern: string) => {
|
|
199
212
|
const segments = pattern.split("/").filter(Boolean);
|
|
200
213
|
return segments.reduce((acc, s) => {
|
|
@@ -229,8 +242,6 @@ function matchRoute(manifest: any, reqPath: string): RouteMatch {
|
|
|
229
242
|
};
|
|
230
243
|
}
|
|
231
244
|
|
|
232
|
-
// Look at your app.get("*") loop and update the processing logic:
|
|
233
|
-
|
|
234
245
|
app.get("*", async (c) => {
|
|
235
246
|
if (isDev) await loadManifestAndTemplate();
|
|
236
247
|
|
|
@@ -246,9 +257,8 @@ app.get("*", async (c) => {
|
|
|
246
257
|
let statusCode: StatusCode = matchedStatus;
|
|
247
258
|
const loaderArgs = { params, request: c.req.raw };
|
|
248
259
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
let htmlPayload = "";
|
|
260
|
+
const store = ssrStorage.getStore() || new Map<string, unknown>();
|
|
261
|
+
let htmlPayload;
|
|
252
262
|
|
|
253
263
|
if (targetPattern) {
|
|
254
264
|
try {
|
|
@@ -263,9 +273,6 @@ app.get("*", async (c) => {
|
|
|
263
273
|
}
|
|
264
274
|
}
|
|
265
275
|
|
|
266
|
-
// ─── THE ARCHITECTURE WRAPPER FIX ───
|
|
267
|
-
// We force both the awaitable loader execution AND the Solid rendering cycle
|
|
268
|
-
// to run explicitly inside a fresh execution slice of the tracking store.
|
|
269
276
|
try {
|
|
270
277
|
htmlPayload = await ssrStorage.run(store, async () => {
|
|
271
278
|
if (targetPattern) {
|
|
@@ -278,14 +285,13 @@ app.get("*", async (c) => {
|
|
|
278
285
|
|
|
279
286
|
await preloadActiveClientRoute(reqPath);
|
|
280
287
|
|
|
281
|
-
// Now when Solid calls $$executeClientRpc, the store is 100% active and tracked!
|
|
282
288
|
return await renderToStringAsync(() => (
|
|
283
289
|
<Router url={reqPath}>
|
|
284
290
|
<App />
|
|
285
291
|
</Router>
|
|
286
292
|
));
|
|
287
293
|
});
|
|
288
|
-
} catch (err
|
|
294
|
+
} catch (err) {
|
|
289
295
|
statusCode = 500;
|
|
290
296
|
console.error("[anaemia framework] runtime execution crash handled:", err);
|
|
291
297
|
|
|
@@ -293,7 +299,9 @@ app.get("*", async (c) => {
|
|
|
293
299
|
const error500Loader = error500Pattern ? serverLoaderRegistry.get(error500Pattern) : null;
|
|
294
300
|
|
|
295
301
|
if (error500Loader) {
|
|
296
|
-
const
|
|
302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
303
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
304
|
+
const runtimeContextPayload = { message, stack: isDev ? stack : undefined };
|
|
297
305
|
store.set("__LOADER_DATA__", runtimeContextPayload);
|
|
298
306
|
|
|
299
307
|
try {
|
|
@@ -308,16 +316,16 @@ app.get("*", async (c) => {
|
|
|
308
316
|
htmlPayload = `<h1>500 Internal Server Error</h1>`;
|
|
309
317
|
}
|
|
310
318
|
} else {
|
|
311
|
-
|
|
319
|
+
const stack = err instanceof Error ? err.stack : String(err);
|
|
320
|
+
htmlPayload = `<h1>500 Internal Server Error</h1><pre>${isDev ? stack : ""}</pre>`;
|
|
312
321
|
}
|
|
313
322
|
}
|
|
314
323
|
|
|
315
|
-
// ─── THE REMAINING INJECTIONS (Keep this exactly as you had it) ───
|
|
316
324
|
let assetScripts = "";
|
|
317
325
|
let assetStyles = "";
|
|
318
326
|
|
|
319
327
|
if (manifest.chunks) {
|
|
320
|
-
const processChunkAssets = (chunk:
|
|
328
|
+
const processChunkAssets = (chunk: ChunkAssets | undefined) => {
|
|
321
329
|
if (!chunk) return;
|
|
322
330
|
if (chunk.js) {
|
|
323
331
|
const jsSpecs = Array.isArray(chunk.js) ? chunk.js : [chunk.js];
|
|
@@ -338,33 +346,25 @@ app.get("*", async (c) => {
|
|
|
338
346
|
if (manifest.chunks["vendors"]) processChunkAssets(manifest.chunks["vendors"]);
|
|
339
347
|
if (activeChunk && activeChunk !== "client") processChunkAssets(manifest.chunks[activeChunk]);
|
|
340
348
|
}
|
|
341
|
-
|
|
349
|
+
|
|
342
350
|
const hydrationScript = generateHydrationScript();
|
|
343
351
|
const rawStorePayload = Object.fromEntries(store);
|
|
344
|
-
|
|
352
|
+
|
|
345
353
|
const finalHydrationStatePayload = {
|
|
346
354
|
__LOADER_DATA__: rawStorePayload.__LOADER_DATA__ || {},
|
|
347
|
-
__SERVER_FUNCTION_DATA__: rawStorePayload.__SERVER_FUNCTION_DATA__ || {}
|
|
355
|
+
__SERVER_FUNCTION_DATA__: rawStorePayload.__SERVER_FUNCTION_DATA__ || {},
|
|
348
356
|
};
|
|
349
357
|
|
|
350
|
-
const serializedData = JSON.stringify(finalHydrationStatePayload)
|
|
351
|
-
|
|
352
|
-
.replace(/</g, "\\u003c")
|
|
353
|
-
.replace(/>/g, "\\u003e")
|
|
354
|
-
.replace(/\//g, "\\u002f");
|
|
355
|
-
|
|
358
|
+
const serializedData = JSON.stringify(finalHydrationStatePayload).replace(/&/g, "\\u0026").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\//g, "\\u002f");
|
|
359
|
+
|
|
356
360
|
const dataScript = `<script id="__ANAEMIA_DATA__" type="application/json">${serializedData}</script>\n`;
|
|
357
361
|
|
|
358
|
-
const devNoCacheTag = isDev
|
|
359
|
-
? `<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">\n<meta http-equiv="Pragma" content="no-cache">\n<meta http-equiv="Expires" content="0">\n`
|
|
360
|
-
: "";
|
|
362
|
+
const devNoCacheTag = isDev ? `<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">\n<meta http-equiv="Pragma" content="no-cache">\n<meta http-equiv="Expires" content="0">\n` : "";
|
|
361
363
|
|
|
362
364
|
const combinedHeadInjections = `${devNoCacheTag}${assetStyles}${dataScript}${hydrationScript}`;
|
|
363
365
|
const sanitizedPayload = htmlPayload.trim();
|
|
364
366
|
|
|
365
|
-
let completeHtmlOutput = ENTRY_TAG_REGEX.test(template)
|
|
366
|
-
? template.replace(ENTRY_TAG_REGEX, (_, open, _tag, _inner, close) => `${open}${sanitizedPayload}${close}`)
|
|
367
|
-
: template.replace("</body>", () => `<div anaemia-entry>${sanitizedPayload}</div></body>`);
|
|
367
|
+
let completeHtmlOutput = ENTRY_TAG_REGEX.test(template) ? template.replace(ENTRY_TAG_REGEX, (_, open, _tag, _inner, close) => `${open}${sanitizedPayload}${close}`) : template.replace("</body>", () => `<div anaemia-entry>${sanitizedPayload}</div></body>`);
|
|
368
368
|
|
|
369
369
|
completeHtmlOutput = completeHtmlOutput.replace("<head>", `<head>${combinedHeadInjections}`);
|
|
370
370
|
completeHtmlOutput = completeHtmlOutput.replace("</body>", `${assetScripts}</body>`);
|
package/src/runtime/resources.ts
CHANGED
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
import { createResource, type ResourceOptions, type ResourceReturn } from "solid-js";
|
|
2
2
|
import { isServer } from "solid-js/web";
|
|
3
3
|
|
|
4
|
+
interface ServerStorage {
|
|
5
|
+
getStore?: () => Map<string, unknown> | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface AnaemiaGlobal {
|
|
9
|
+
__ANAEMIA_SERVER_STORAGE__?: ServerStorage;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FnCache = Record<string, unknown>;
|
|
13
|
+
type ServerFunctionData = Record<string, FnCache>;
|
|
14
|
+
|
|
4
15
|
export function createServerResource<Source, Return>(
|
|
5
16
|
source: () => Source,
|
|
6
17
|
serverFn: ((sourceData: Source) => Promise<Return>) & {
|
|
7
|
-
readHydrationCache?: (s:
|
|
18
|
+
readHydrationCache?: (s: Source) => Return | undefined;
|
|
8
19
|
id?: string;
|
|
9
20
|
},
|
|
10
21
|
options?: ResourceOptions<Return, Source>
|
|
11
22
|
): ResourceReturn<Return, unknown> {
|
|
12
23
|
if (isServer) {
|
|
13
|
-
let ssrInitialValue:
|
|
14
|
-
const store
|
|
15
|
-
(globalThis as any).__ANAEMIA_SERVER_STORAGE__?.getStore?.();
|
|
24
|
+
let ssrInitialValue: Return | undefined = undefined;
|
|
25
|
+
const store = (globalThis as unknown as AnaemiaGlobal).__ANAEMIA_SERVER_STORAGE__?.getStore?.();
|
|
16
26
|
|
|
17
27
|
if (store && serverFn.id) {
|
|
18
|
-
const
|
|
28
|
+
const fnData = store.get("__SERVER_FUNCTION_DATA__") as ServerFunctionData | undefined;
|
|
29
|
+
const fnCache = fnData?.[serverFn.id];
|
|
19
30
|
if (fnCache) {
|
|
20
|
-
const
|
|
21
|
-
|
|
31
|
+
const sourceValue = source();
|
|
32
|
+
const key = sourceValue === undefined ? JSON.stringify([]) : JSON.stringify([sourceValue]);
|
|
33
|
+
if (fnCache[key] !== undefined) ssrInitialValue = fnCache[key] as Return;
|
|
22
34
|
}
|
|
23
35
|
}
|
|
24
36
|
|
|
25
|
-
return createResource(source, serverFn
|
|
37
|
+
return createResource(source, serverFn, {
|
|
26
38
|
...options,
|
|
27
39
|
initialValue: ssrInitialValue !== undefined ? ssrInitialValue : options?.initialValue,
|
|
28
40
|
ssrLoadFrom: ssrInitialValue !== undefined ? "initial" : options?.ssrLoadFrom,
|
|
29
|
-
}
|
|
41
|
+
}) as ResourceReturn<Return, unknown>;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
let hydrationChecked = false;
|
|
33
|
-
|
|
34
|
-
const wrappedFetcher = (s: Source) => {
|
|
45
|
+
const wrappedFetcher = (s: Source): Promise<Return> => {
|
|
35
46
|
if (!hydrationChecked) {
|
|
36
47
|
hydrationChecked = true;
|
|
37
48
|
if (typeof serverFn.readHydrationCache === "function") {
|
|
@@ -39,11 +50,12 @@ export function createServerResource<Source, Return>(
|
|
|
39
50
|
if (cached !== undefined) return Promise.resolve(cached);
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
|
-
|
|
53
|
+
|
|
54
|
+
return s === undefined ? (serverFn as () => Promise<Return>)() : serverFn(s);
|
|
43
55
|
};
|
|
44
56
|
|
|
45
|
-
(wrappedFetcher as
|
|
46
|
-
(wrappedFetcher as
|
|
57
|
+
(wrappedFetcher as typeof serverFn).id = serverFn.id;
|
|
58
|
+
(wrappedFetcher as typeof serverFn).readHydrationCache = serverFn.readHydrationCache;
|
|
47
59
|
|
|
48
|
-
return createResource(source, wrappedFetcher
|
|
49
|
-
}
|
|
60
|
+
return createResource(source, wrappedFetcher, options) as ResourceReturn<Return, unknown>;
|
|
61
|
+
}
|
|
@@ -1,93 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
useContext,
|
|
4
|
-
createResource,
|
|
5
|
-
createComponent,
|
|
6
|
-
type JSX
|
|
7
|
-
} from "solid-js";
|
|
1
|
+
import { createContext, useContext, createResource, createComponent, type JSX } from "solid-js";
|
|
8
2
|
import { isServer } from "solid-js/web";
|
|
9
3
|
import { useParams, useLocation, type Params } from "@solidjs/router";
|
|
10
4
|
import type { Location } from "@solidjs/router";
|
|
11
5
|
import { ssrStorage } from "./context.js";
|
|
12
6
|
import { createRouteRequest } from "./route-request.js";
|
|
13
7
|
|
|
8
|
+
type LoaderArgs<TParams extends Params> = {
|
|
9
|
+
params: TParams;
|
|
10
|
+
location: Location;
|
|
11
|
+
request: Request;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
14
|
type RouteDataControllerProps<TParams extends Params = Params> = {
|
|
15
|
-
loader: (args:
|
|
16
|
-
params: TParams;
|
|
17
|
-
location: Location;
|
|
18
|
-
request: Request;
|
|
19
|
-
}) => any | Promise<any>;
|
|
15
|
+
loader: (args: LoaderArgs<TParams>) => unknown | Promise<unknown>;
|
|
20
16
|
children: JSX.Element;
|
|
21
17
|
};
|
|
22
18
|
|
|
23
|
-
type RouteDataContextValue<T =
|
|
19
|
+
type RouteDataContextValue<T = unknown> = {
|
|
24
20
|
data: () => T;
|
|
25
21
|
};
|
|
26
22
|
|
|
27
23
|
const RouteDataContext = createContext<RouteDataContextValue>();
|
|
24
|
+
|
|
28
25
|
let hasReadClientHydrationData = false;
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
interface AnaemiaHydrationData {
|
|
28
|
+
__LOADER_DATA__?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readSSRData(): unknown {
|
|
31
32
|
if (isServer) {
|
|
32
33
|
return ssrStorage.getStore()?.get("__LOADER_DATA__");
|
|
33
34
|
}
|
|
34
|
-
|
|
35
35
|
if (hasReadClientHydrationData) return undefined;
|
|
36
36
|
hasReadClientHydrationData = true;
|
|
37
|
-
|
|
38
37
|
const el = document.getElementById("__ANAEMIA_DATA__");
|
|
39
38
|
if (!el?.textContent) return undefined;
|
|
40
|
-
|
|
41
39
|
try {
|
|
42
|
-
return JSON.parse(el.textContent).__LOADER_DATA__;
|
|
40
|
+
return (JSON.parse(el.textContent) as AnaemiaHydrationData).__LOADER_DATA__;
|
|
43
41
|
} catch {
|
|
44
42
|
return undefined;
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
export function RouteDataController<TParams extends Params = Params>(
|
|
49
|
-
props: RouteDataControllerProps<TParams>
|
|
50
|
-
) {
|
|
46
|
+
export function RouteDataController<TParams extends Params = Params>(props: RouteDataControllerProps<TParams>) {
|
|
51
47
|
const params = useParams<TParams>();
|
|
52
48
|
const location = useLocation();
|
|
53
|
-
|
|
54
49
|
const ssrData = readSSRData();
|
|
55
50
|
|
|
56
51
|
const [resource] = createResource(
|
|
57
52
|
() => location.pathname,
|
|
58
53
|
() => {
|
|
59
|
-
if (isServer && ssrData !== undefined)
|
|
60
|
-
return ssrData;
|
|
61
|
-
}
|
|
62
|
-
|
|
54
|
+
if (isServer && ssrData !== undefined) return ssrData;
|
|
63
55
|
return props.loader({
|
|
64
56
|
params,
|
|
65
57
|
location,
|
|
66
|
-
request: createRouteRequest(location.pathname)
|
|
58
|
+
request: createRouteRequest(location.pathname),
|
|
67
59
|
});
|
|
68
60
|
},
|
|
69
61
|
{
|
|
70
62
|
initialValue: ssrData,
|
|
71
|
-
ssrLoadFrom: "initial"
|
|
63
|
+
ssrLoadFrom: "initial",
|
|
72
64
|
}
|
|
73
65
|
);
|
|
74
66
|
|
|
75
67
|
return createComponent(RouteDataContext.Provider, {
|
|
76
|
-
value: {
|
|
77
|
-
data: resource
|
|
78
|
-
},
|
|
68
|
+
value: { data: resource },
|
|
79
69
|
get children() {
|
|
80
70
|
return props.children;
|
|
81
|
-
}
|
|
71
|
+
},
|
|
82
72
|
});
|
|
83
73
|
}
|
|
84
74
|
|
|
85
|
-
export function useRouteData<T =
|
|
75
|
+
export function useRouteData<T = unknown>(): () => T {
|
|
86
76
|
const ctx = useContext(RouteDataContext);
|
|
87
|
-
|
|
88
77
|
if (!ctx) {
|
|
89
78
|
throw new Error("useRouteData must be used inside RouteDataController");
|
|
90
79
|
}
|
|
91
|
-
|
|
92
|
-
return ctx.data;
|
|
80
|
+
return ctx.data as () => T;
|
|
93
81
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { isServer } from "solid-js/web";
|
|
2
|
+
import type { Context } from "hono";
|
|
2
3
|
import { ssrStorage } from "./context.js";
|
|
3
4
|
|
|
4
|
-
export function createRouteRequest(pathname: string) {
|
|
5
|
+
export function createRouteRequest(pathname: string): Request {
|
|
5
6
|
if (isServer) {
|
|
6
|
-
const
|
|
7
|
+
const honoContext = ssrStorage.getStore()?.get("honoContext") as Context | undefined;
|
|
8
|
+
const request = honoContext?.req?.raw;
|
|
7
9
|
if (request) return request;
|
|
8
10
|
return new Request(new URL(pathname, "http://localhost").toString());
|
|
9
11
|
}
|
|
10
|
-
|
|
11
12
|
return new Request(new URL(pathname, window.location.origin).toString());
|
|
12
|
-
}
|
|
13
|
+
}
|
|
@@ -1,39 +1,63 @@
|
|
|
1
1
|
import { isServer } from "solid-js/web";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface CacheMatch {
|
|
4
|
+
matchingKey: string;
|
|
5
|
+
data: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ServerFunctionCache {
|
|
9
|
+
[hashId: string]: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AnaemiaClientCache {
|
|
13
|
+
__SERVER_FUNCTION_DATA__?: ServerFunctionCache;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AnaemiaServerStorage {
|
|
17
|
+
getStore?: () => Map<string, unknown> | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface AnaemiaGlobal {
|
|
21
|
+
__ANAEMIA_SERVER_STORAGE__?: AnaemiaServerStorage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let _clientCache: AnaemiaClientCache | null = null;
|
|
4
25
|
|
|
5
26
|
function ensureCacheInitialized() {
|
|
6
27
|
if (isServer || _clientCache) return;
|
|
7
28
|
const script = document.getElementById("__ANAEMIA_DATA__");
|
|
8
29
|
try {
|
|
9
|
-
_clientCache = JSON.parse(script?.textContent || "{}");
|
|
30
|
+
_clientCache = JSON.parse(script?.textContent || "{}") as AnaemiaClientCache;
|
|
10
31
|
} catch {
|
|
11
32
|
_clientCache = {};
|
|
12
33
|
}
|
|
13
34
|
}
|
|
14
35
|
|
|
15
|
-
function findLooseCacheMatch(
|
|
36
|
+
function findLooseCacheMatch(
|
|
37
|
+
serverFunctionData: Record<string, unknown>,
|
|
38
|
+
targetArg: string
|
|
39
|
+
): CacheMatch | undefined {
|
|
16
40
|
if (!serverFunctionData) return undefined;
|
|
17
|
-
|
|
18
41
|
const strictKey = JSON.stringify([targetArg]);
|
|
19
42
|
if (strictKey in serverFunctionData) {
|
|
20
43
|
return { matchingKey: strictKey, data: serverFunctionData[strictKey] };
|
|
21
44
|
}
|
|
22
|
-
|
|
23
45
|
const lookUpString = `["${targetArg}"`;
|
|
24
|
-
const matchedKey = Object.keys(serverFunctionData).find(key => key.startsWith(lookUpString));
|
|
25
|
-
|
|
46
|
+
const matchedKey = Object.keys(serverFunctionData).find((key) => key.startsWith(lookUpString));
|
|
26
47
|
return matchedKey ? { matchingKey: matchedKey, data: serverFunctionData[matchedKey] } : undefined;
|
|
27
48
|
}
|
|
28
49
|
|
|
50
|
+
function getServerStore(): Map<string, unknown> | undefined {
|
|
51
|
+
return (globalThis as unknown as AnaemiaGlobal).__ANAEMIA_SERVER_STORAGE__?.getStore?.();
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
export function $$executeClientRpc(hashId: string) {
|
|
30
55
|
const asyncRpcCall = async function (...args: unknown[]) {
|
|
31
56
|
if (isServer) {
|
|
32
|
-
const
|
|
33
|
-
const store = globalStorage?.getStore();
|
|
57
|
+
const store = getServerStore();
|
|
34
58
|
if (store) {
|
|
35
|
-
const functionCache = store.get("__SERVER_FUNCTION_DATA__");
|
|
36
|
-
if (functionCache
|
|
59
|
+
const functionCache = store.get("__SERVER_FUNCTION_DATA__") as ServerFunctionCache | undefined;
|
|
60
|
+
if (functionCache?.[hashId]) {
|
|
37
61
|
const match = findLooseCacheMatch(functionCache[hashId], args[0] as string);
|
|
38
62
|
if (match) return match.data;
|
|
39
63
|
}
|
|
@@ -42,12 +66,11 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
42
66
|
}
|
|
43
67
|
|
|
44
68
|
ensureCacheInitialized();
|
|
45
|
-
const serverFunctionData = _clientCache
|
|
46
|
-
const match = findLooseCacheMatch(serverFunctionData, args[0] as string);
|
|
47
|
-
|
|
69
|
+
const serverFunctionData = _clientCache?.__SERVER_FUNCTION_DATA__?.[hashId];
|
|
70
|
+
const match = findLooseCacheMatch(serverFunctionData ?? {}, args[0] as string);
|
|
48
71
|
if (match) {
|
|
49
72
|
const { matchingKey, data } = match;
|
|
50
|
-
delete serverFunctionData[matchingKey];
|
|
73
|
+
delete serverFunctionData![matchingKey];
|
|
51
74
|
return data;
|
|
52
75
|
}
|
|
53
76
|
|
|
@@ -56,20 +79,17 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
56
79
|
headers: { "Content-Type": "application/json" },
|
|
57
80
|
body: JSON.stringify(args),
|
|
58
81
|
});
|
|
59
|
-
|
|
60
82
|
if (!response.ok) throw new Error(`[anaemia] RPC execution failed: ${response.status}`);
|
|
61
|
-
return await response.json();
|
|
83
|
+
return await response.json() as unknown;
|
|
62
84
|
};
|
|
63
85
|
|
|
64
86
|
asyncRpcCall.id = hashId;
|
|
65
|
-
|
|
66
87
|
asyncRpcCall.readHydrationCache = function (...args: unknown[]) {
|
|
67
88
|
if (isServer) {
|
|
68
|
-
const
|
|
69
|
-
const store = globalStorage?.getStore();
|
|
89
|
+
const store = getServerStore();
|
|
70
90
|
if (store) {
|
|
71
|
-
const functionCache = store.get("__SERVER_FUNCTION_DATA__");
|
|
72
|
-
if (functionCache
|
|
91
|
+
const functionCache = store.get("__SERVER_FUNCTION_DATA__") as ServerFunctionCache | undefined;
|
|
92
|
+
if (functionCache?.[hashId]) {
|
|
73
93
|
const match = findLooseCacheMatch(functionCache[hashId], args[0] as string);
|
|
74
94
|
if (match) return match.data;
|
|
75
95
|
}
|
|
@@ -78,8 +98,8 @@ export function $$executeClientRpc(hashId: string) {
|
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
ensureCacheInitialized();
|
|
81
|
-
const serverFunctionData = _clientCache
|
|
82
|
-
const match = findLooseCacheMatch(serverFunctionData, args[0] as string);
|
|
101
|
+
const serverFunctionData = _clientCache?.__SERVER_FUNCTION_DATA__?.[hashId];
|
|
102
|
+
const match = findLooseCacheMatch(serverFunctionData ?? {}, args[0] as string);
|
|
83
103
|
return match ? match.data : undefined;
|
|
84
104
|
};
|
|
85
105
|
|