@tanstack/start-static-server-functions 1.121.0-alpha.28
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/LICENSE +21 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/staticFunctionMiddleware.d.ts +1 -0
- package/dist/esm/staticFunctionMiddleware.js +98 -0
- package/dist/esm/staticFunctionMiddleware.js.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +1 -0
- package/src/staticFunctionMiddleware.ts +185 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-present Tanner Linsley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { staticFunctionMiddleware } from './staticFunctionMiddleware.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const staticFunctionMiddleware: import('@tanstack/start-client-core').FunctionMiddlewareAfterServer<unknown, undefined, undefined, undefined, undefined, undefined, import('@tanstack/start-client-core').ServerFnResponseType>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createMiddleware, startSerializer } from "@tanstack/start-client-core";
|
|
2
|
+
async function sha1Hash(message) {
|
|
3
|
+
const msgBuffer = new TextEncoder().encode(message);
|
|
4
|
+
const hashBuffer = await crypto.subtle.digest("SHA-1", msgBuffer);
|
|
5
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
6
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7
|
+
return hashHex;
|
|
8
|
+
}
|
|
9
|
+
const getStaticCacheUrl = async (opts) => {
|
|
10
|
+
const filename = await sha1Hash(`${opts.functionId}__${opts.hash}`);
|
|
11
|
+
return `/__tsr/staticServerFnCache/${filename}.json`;
|
|
12
|
+
};
|
|
13
|
+
const jsonToFilenameSafeString = (json) => {
|
|
14
|
+
const sortedKeysReplacer = (key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort().reduce((acc, curr) => {
|
|
15
|
+
acc[curr] = value[curr];
|
|
16
|
+
return acc;
|
|
17
|
+
}, {}) : value;
|
|
18
|
+
const jsonString = JSON.stringify(json ?? "", sortedKeysReplacer);
|
|
19
|
+
return jsonString.replace(/[/\\?%*:|"<>]/g, "-").replace(/\s+/g, "_");
|
|
20
|
+
};
|
|
21
|
+
const staticClientCache = typeof document !== "undefined" ? /* @__PURE__ */ new Map() : null;
|
|
22
|
+
const serverFnStaticCache = {
|
|
23
|
+
getItem: async (ctx) => {
|
|
24
|
+
if (typeof document === "undefined") {
|
|
25
|
+
const hash = jsonToFilenameSafeString(ctx.data);
|
|
26
|
+
const url = await getStaticCacheUrl({ functionId: ctx.functionId, hash });
|
|
27
|
+
const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR;
|
|
28
|
+
const { promises: fs } = await import("node:fs");
|
|
29
|
+
const path = await import("node:path");
|
|
30
|
+
const filePath = path.join(publicUrl, url);
|
|
31
|
+
const [cachedResult, readError] = await fs.readFile(filePath, "utf-8").then((c) => [startSerializer.parse(c), null]).catch((e) => [null, e]);
|
|
32
|
+
if (readError && readError.code !== "ENOENT") {
|
|
33
|
+
throw readError;
|
|
34
|
+
}
|
|
35
|
+
return cachedResult;
|
|
36
|
+
}
|
|
37
|
+
return void 0;
|
|
38
|
+
},
|
|
39
|
+
setItem: async ({ data, functionId, response }) => {
|
|
40
|
+
const { promises: fs } = await import("node:fs");
|
|
41
|
+
const path = await import("node:path");
|
|
42
|
+
const hash = jsonToFilenameSafeString(data);
|
|
43
|
+
const url = await getStaticCacheUrl({ functionId, hash });
|
|
44
|
+
const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR;
|
|
45
|
+
const filePath = path.join(publicUrl, url);
|
|
46
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
47
|
+
await fs.writeFile(
|
|
48
|
+
filePath,
|
|
49
|
+
startSerializer.stringify({
|
|
50
|
+
result: response.result,
|
|
51
|
+
context: response.context.sendContext
|
|
52
|
+
}),
|
|
53
|
+
"utf-8"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const fetchItem = async ({
|
|
58
|
+
data,
|
|
59
|
+
functionId
|
|
60
|
+
}) => {
|
|
61
|
+
const hash = jsonToFilenameSafeString(data);
|
|
62
|
+
const url = await getStaticCacheUrl({ functionId, hash });
|
|
63
|
+
let result = staticClientCache?.get(url);
|
|
64
|
+
result = await fetch(url, {
|
|
65
|
+
method: "GET"
|
|
66
|
+
}).then((r) => r.text()).then((d) => startSerializer.parse(d));
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
const staticFunctionMiddleware = createMiddleware({ type: "function" }).client(async (ctx) => {
|
|
70
|
+
if (process.env.NODE_ENV === "production" && // do not run this during SSR on the server
|
|
71
|
+
typeof document !== "undefined") {
|
|
72
|
+
const response = await fetchItem({
|
|
73
|
+
functionId: ctx.functionId,
|
|
74
|
+
data: ctx.data
|
|
75
|
+
});
|
|
76
|
+
if (response) {
|
|
77
|
+
return {
|
|
78
|
+
result: response.result,
|
|
79
|
+
context: { ...ctx.context, ...response.context }
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return ctx.next();
|
|
84
|
+
}).server(async (ctx) => {
|
|
85
|
+
const response = await ctx.next();
|
|
86
|
+
if (process.env.NODE_ENV === "production") {
|
|
87
|
+
await serverFnStaticCache.setItem({
|
|
88
|
+
functionId: ctx.functionId,
|
|
89
|
+
response: { result: response.result, context: ctx },
|
|
90
|
+
data: ctx.data
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return response;
|
|
94
|
+
});
|
|
95
|
+
export {
|
|
96
|
+
staticFunctionMiddleware
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=staticFunctionMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staticFunctionMiddleware.js","sources":["../../src/staticFunctionMiddleware.ts"],"sourcesContent":["import { createMiddleware, startSerializer } from '@tanstack/start-client-core'\n\ntype StaticCachedResult = {\n result: any\n context: any\n}\n\ntype ServerFnStaticCache = {\n getItem: (opts: {\n functionId: string\n data: any\n }) => StaticCachedResult | Promise<StaticCachedResult | undefined>\n setItem: (opts: {\n functionId: string\n data: any\n response: StaticCachedResult\n }) => Promise<void>\n}\n\n/**\n * This is a simple hash function for generating a hash from a string to make the filenames shorter.\n *\n * It is not cryptographically secure (as its using SHA-1) and should not be used for any security purposes.\n *\n * It is only used to generate a hash for the static cache filenames.\n *\n * @param message - The input string to hash.\n * @returns A promise that resolves to the SHA-1 hash of the input string in hexadecimal format.\n *\n * @example\n * ```typescript\n * const hash = await sha1Hash(\"hello\");\n * console.log(hash); // Outputs the SHA-1 hash of \"hello\" -> \"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d\"\n * ```\n */\nasync function sha1Hash(message: string): Promise<string> {\n // Encode the string as UTF-8\n const msgBuffer = new TextEncoder().encode(message)\n\n // Hash the message\n const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer)\n\n // Convert the ArrayBuffer to a string\n const hashArray = Array.from(new Uint8Array(hashBuffer))\n const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')\n return hashHex\n}\n\nconst getStaticCacheUrl = async (opts: {\n functionId: string\n hash: string\n}) => {\n const filename = await sha1Hash(`${opts.functionId}__${opts.hash}`)\n return `/__tsr/staticServerFnCache/${filename}.json`\n}\n\nconst jsonToFilenameSafeString = (json: any) => {\n // Custom replacer to sort keys\n const sortedKeysReplacer = (key: string, value: any) =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? Object.keys(value)\n .sort()\n .reduce((acc: any, curr: string) => {\n acc[curr] = value[curr]\n return acc\n }, {})\n : value\n\n // Convert JSON to string with sorted keys\n const jsonString = JSON.stringify(json ?? '', sortedKeysReplacer)\n\n // Replace characters invalid in filenames\n return jsonString\n .replace(/[/\\\\?%*:|\"<>]/g, '-') // Replace invalid characters with a dash\n .replace(/\\s+/g, '_') // Optionally replace whitespace with underscores\n}\n\nconst staticClientCache =\n typeof document !== 'undefined' ? new Map<string, any>() : null\n\nconst serverFnStaticCache: ServerFnStaticCache = {\n getItem: async (ctx) => {\n if (typeof document === 'undefined') {\n const hash = jsonToFilenameSafeString(ctx.data)\n const url = await getStaticCacheUrl({ functionId: ctx.functionId, hash })\n const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR!\n\n // Use fs instead of fetch to read from filesystem\n const { promises: fs } = await import('node:fs')\n const path = await import('node:path')\n const filePath = path.join(publicUrl, url)\n\n const [cachedResult, readError] = await fs\n .readFile(filePath, 'utf-8')\n .then((c) => [startSerializer.parse(c), null])\n .catch((e) => [null, e])\n\n if (readError && readError.code !== 'ENOENT') {\n throw readError\n }\n\n return cachedResult as StaticCachedResult\n }\n\n return undefined\n },\n setItem: async ({ data, functionId, response }) => {\n const { promises: fs } = await import('node:fs')\n const path = await import('node:path')\n\n const hash = jsonToFilenameSafeString(data)\n const url = await getStaticCacheUrl({ functionId, hash })\n const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR!\n const filePath = path.join(publicUrl, url)\n\n // Ensure the directory exists\n await fs.mkdir(path.dirname(filePath), { recursive: true })\n\n // Store the result with fs\n await fs.writeFile(\n filePath,\n startSerializer.stringify({\n result: response.result,\n context: response.context.sendContext,\n }),\n 'utf-8',\n )\n },\n}\n\nconst fetchItem = async ({\n data,\n functionId,\n}: {\n data: any\n functionId: string\n}) => {\n const hash = jsonToFilenameSafeString(data)\n const url = await getStaticCacheUrl({ functionId, hash })\n\n let result: any = staticClientCache?.get(url)\n\n result = await fetch(url, {\n method: 'GET',\n })\n .then((r) => r.text())\n .then((d) => startSerializer.parse(d))\n\n return result\n}\n\nexport const staticFunctionMiddleware = createMiddleware({ type: 'function' })\n .client(async (ctx) => {\n if (\n process.env.NODE_ENV === 'production' &&\n // do not run this during SSR on the server\n typeof document !== 'undefined'\n ) {\n const response = await fetchItem({\n functionId: ctx.functionId,\n data: ctx.data,\n })\n\n if (response) {\n return {\n result: response.result,\n context: { ...(ctx as any).context, ...response.context },\n } as any\n }\n }\n return ctx.next()\n })\n .server(async (ctx) => {\n const response = await ctx.next()\n\n if (process.env.NODE_ENV === 'production') {\n await serverFnStaticCache.setItem({\n functionId: ctx.functionId,\n response: { result: (response as any).result, context: ctx },\n data: ctx.data,\n })\n }\n\n return response\n })\n"],"names":[],"mappings":";AAmCA,eAAe,SAAS,SAAkC;AAExD,QAAM,YAAY,IAAI,cAAc,OAAO,OAAO;AAGlD,QAAM,aAAa,MAAM,OAAO,OAAO,OAAO,SAAS,SAAS;AAGhE,QAAM,YAAY,MAAM,KAAK,IAAI,WAAW,UAAU,CAAC;AACvD,QAAM,UAAU,UAAU,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC7E,SAAO;AACT;AAEA,MAAM,oBAAoB,OAAO,SAG3B;AACJ,QAAM,WAAW,MAAM,SAAS,GAAG,KAAK,UAAU,KAAK,KAAK,IAAI,EAAE;AAClE,SAAO,8BAA8B,QAAQ;AAC/C;AAEA,MAAM,2BAA2B,CAAC,SAAc;AAE9C,QAAM,qBAAqB,CAAC,KAAa,UACvC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IACtD,OAAO,KAAK,KAAK,EACd,OACA,OAAO,CAAC,KAAU,SAAiB;AAClC,QAAI,IAAI,IAAI,MAAM,IAAI;AACtB,WAAO;AAAA,EACT,GAAG,CAAA,CAAE,IACP;AAGN,QAAM,aAAa,KAAK,UAAU,QAAQ,IAAI,kBAAkB;AAGhE,SAAO,WACJ,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,QAAQ,GAAG;AACxB;AAEA,MAAM,oBACJ,OAAO,aAAa,cAAc,oBAAI,QAAqB;AAE7D,MAAM,sBAA2C;AAAA,EAC/C,SAAS,OAAO,QAAQ;AACtB,QAAI,OAAO,aAAa,aAAa;AACnC,YAAM,OAAO,yBAAyB,IAAI,IAAI;AAC9C,YAAM,MAAM,MAAM,kBAAkB,EAAE,YAAY,IAAI,YAAY,MAAM;AACxE,YAAM,YAAY,QAAQ,IAAI;AAG9B,YAAM,EAAE,UAAU,OAAO,MAAM,OAAO,SAAS;AAC/C,YAAM,OAAO,MAAM,OAAO,WAAW;AACrC,YAAM,WAAW,KAAK,KAAK,WAAW,GAAG;AAEzC,YAAM,CAAC,cAAc,SAAS,IAAI,MAAM,GACrC,SAAS,UAAU,OAAO,EAC1B,KAAK,CAAC,MAAM,CAAC,gBAAgB,MAAM,CAAC,GAAG,IAAI,CAAC,EAC5C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAEzB,UAAI,aAAa,UAAU,SAAS,UAAU;AAC5C,cAAM;AAAA,MACR;AAEA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAAA,EACA,SAAS,OAAO,EAAE,MAAM,YAAY,eAAe;AACjD,UAAM,EAAE,UAAU,OAAO,MAAM,OAAO,SAAS;AAC/C,UAAM,OAAO,MAAM,OAAO,WAAW;AAErC,UAAM,OAAO,yBAAyB,IAAI;AAC1C,UAAM,MAAM,MAAM,kBAAkB,EAAE,YAAY,MAAM;AACxD,UAAM,YAAY,QAAQ,IAAI;AAC9B,UAAM,WAAW,KAAK,KAAK,WAAW,GAAG;AAGzC,UAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,MAAM;AAG1D,UAAM,GAAG;AAAA,MACP;AAAA,MACA,gBAAgB,UAAU;AAAA,QACxB,QAAQ,SAAS;AAAA,QACjB,SAAS,SAAS,QAAQ;AAAA,MAAA,CAC3B;AAAA,MACD;AAAA,IAAA;AAAA,EAEJ;AACF;AAEA,MAAM,YAAY,OAAO;AAAA,EACvB;AAAA,EACA;AACF,MAGM;AACJ,QAAM,OAAO,yBAAyB,IAAI;AAC1C,QAAM,MAAM,MAAM,kBAAkB,EAAE,YAAY,MAAM;AAExD,MAAI,SAAc,mBAAmB,IAAI,GAAG;AAE5C,WAAS,MAAM,MAAM,KAAK;AAAA,IACxB,QAAQ;AAAA,EAAA,CACT,EACE,KAAK,CAAC,MAAM,EAAE,KAAA,CAAM,EACpB,KAAK,CAAC,MAAM,gBAAgB,MAAM,CAAC,CAAC;AAEvC,SAAO;AACT;AAEO,MAAM,2BAA2B,iBAAiB,EAAE,MAAM,YAAY,EAC1E,OAAO,OAAO,QAAQ;AACrB,MACE,QAAQ,IAAI,aAAa;AAAA,EAEzB,OAAO,aAAa,aACpB;AACA,UAAM,WAAW,MAAM,UAAU;AAAA,MAC/B,YAAY,IAAI;AAAA,MAChB,MAAM,IAAI;AAAA,IAAA,CACX;AAED,QAAI,UAAU;AACZ,aAAO;AAAA,QACL,QAAQ,SAAS;AAAA,QACjB,SAAS,EAAE,GAAI,IAAY,SAAS,GAAG,SAAS,QAAA;AAAA,MAAQ;AAAA,IAE5D;AAAA,EACF;AACA,SAAO,IAAI,KAAA;AACb,CAAC,EACA,OAAO,OAAO,QAAQ;AACrB,QAAM,WAAW,MAAM,IAAI,KAAA;AAE3B,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAM,oBAAoB,QAAQ;AAAA,MAChC,YAAY,IAAI;AAAA,MAChB,UAAU,EAAE,QAAS,SAAiB,QAAQ,SAAS,IAAA;AAAA,MACvD,MAAM,IAAI;AAAA,IAAA,CACX;AAAA,EACH;AAEA,SAAO;AACT,CAAC;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/start-static-server-functions",
|
|
3
|
+
"version": "1.121.0-alpha.28",
|
|
4
|
+
"description": "Modern and scalable routing for React applications",
|
|
5
|
+
"author": "Tanner Linsley",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/TanStack/router.git",
|
|
10
|
+
"directory": "packages/start-static-server-functions"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://tanstack.com/start",
|
|
13
|
+
"funding": {
|
|
14
|
+
"type": "github",
|
|
15
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react",
|
|
19
|
+
"location",
|
|
20
|
+
"router",
|
|
21
|
+
"routing",
|
|
22
|
+
"async",
|
|
23
|
+
"async router",
|
|
24
|
+
"typescript"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"types": "dist/esm/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/esm/index.d.ts",
|
|
32
|
+
"default": "./dist/esm/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=12"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@tanstack/start-client-core": "1.121.0-alpha.28"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {}
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { staticFunctionMiddleware } from './staticFunctionMiddleware'
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createMiddleware, startSerializer } from '@tanstack/start-client-core'
|
|
2
|
+
|
|
3
|
+
type StaticCachedResult = {
|
|
4
|
+
result: any
|
|
5
|
+
context: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type ServerFnStaticCache = {
|
|
9
|
+
getItem: (opts: {
|
|
10
|
+
functionId: string
|
|
11
|
+
data: any
|
|
12
|
+
}) => StaticCachedResult | Promise<StaticCachedResult | undefined>
|
|
13
|
+
setItem: (opts: {
|
|
14
|
+
functionId: string
|
|
15
|
+
data: any
|
|
16
|
+
response: StaticCachedResult
|
|
17
|
+
}) => Promise<void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This is a simple hash function for generating a hash from a string to make the filenames shorter.
|
|
22
|
+
*
|
|
23
|
+
* It is not cryptographically secure (as its using SHA-1) and should not be used for any security purposes.
|
|
24
|
+
*
|
|
25
|
+
* It is only used to generate a hash for the static cache filenames.
|
|
26
|
+
*
|
|
27
|
+
* @param message - The input string to hash.
|
|
28
|
+
* @returns A promise that resolves to the SHA-1 hash of the input string in hexadecimal format.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const hash = await sha1Hash("hello");
|
|
33
|
+
* console.log(hash); // Outputs the SHA-1 hash of "hello" -> "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
async function sha1Hash(message: string): Promise<string> {
|
|
37
|
+
// Encode the string as UTF-8
|
|
38
|
+
const msgBuffer = new TextEncoder().encode(message)
|
|
39
|
+
|
|
40
|
+
// Hash the message
|
|
41
|
+
const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer)
|
|
42
|
+
|
|
43
|
+
// Convert the ArrayBuffer to a string
|
|
44
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
45
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
46
|
+
return hashHex
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getStaticCacheUrl = async (opts: {
|
|
50
|
+
functionId: string
|
|
51
|
+
hash: string
|
|
52
|
+
}) => {
|
|
53
|
+
const filename = await sha1Hash(`${opts.functionId}__${opts.hash}`)
|
|
54
|
+
return `/__tsr/staticServerFnCache/${filename}.json`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const jsonToFilenameSafeString = (json: any) => {
|
|
58
|
+
// Custom replacer to sort keys
|
|
59
|
+
const sortedKeysReplacer = (key: string, value: any) =>
|
|
60
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
61
|
+
? Object.keys(value)
|
|
62
|
+
.sort()
|
|
63
|
+
.reduce((acc: any, curr: string) => {
|
|
64
|
+
acc[curr] = value[curr]
|
|
65
|
+
return acc
|
|
66
|
+
}, {})
|
|
67
|
+
: value
|
|
68
|
+
|
|
69
|
+
// Convert JSON to string with sorted keys
|
|
70
|
+
const jsonString = JSON.stringify(json ?? '', sortedKeysReplacer)
|
|
71
|
+
|
|
72
|
+
// Replace characters invalid in filenames
|
|
73
|
+
return jsonString
|
|
74
|
+
.replace(/[/\\?%*:|"<>]/g, '-') // Replace invalid characters with a dash
|
|
75
|
+
.replace(/\s+/g, '_') // Optionally replace whitespace with underscores
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const staticClientCache =
|
|
79
|
+
typeof document !== 'undefined' ? new Map<string, any>() : null
|
|
80
|
+
|
|
81
|
+
const serverFnStaticCache: ServerFnStaticCache = {
|
|
82
|
+
getItem: async (ctx) => {
|
|
83
|
+
if (typeof document === 'undefined') {
|
|
84
|
+
const hash = jsonToFilenameSafeString(ctx.data)
|
|
85
|
+
const url = await getStaticCacheUrl({ functionId: ctx.functionId, hash })
|
|
86
|
+
const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR!
|
|
87
|
+
|
|
88
|
+
// Use fs instead of fetch to read from filesystem
|
|
89
|
+
const { promises: fs } = await import('node:fs')
|
|
90
|
+
const path = await import('node:path')
|
|
91
|
+
const filePath = path.join(publicUrl, url)
|
|
92
|
+
|
|
93
|
+
const [cachedResult, readError] = await fs
|
|
94
|
+
.readFile(filePath, 'utf-8')
|
|
95
|
+
.then((c) => [startSerializer.parse(c), null])
|
|
96
|
+
.catch((e) => [null, e])
|
|
97
|
+
|
|
98
|
+
if (readError && readError.code !== 'ENOENT') {
|
|
99
|
+
throw readError
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return cachedResult as StaticCachedResult
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined
|
|
106
|
+
},
|
|
107
|
+
setItem: async ({ data, functionId, response }) => {
|
|
108
|
+
const { promises: fs } = await import('node:fs')
|
|
109
|
+
const path = await import('node:path')
|
|
110
|
+
|
|
111
|
+
const hash = jsonToFilenameSafeString(data)
|
|
112
|
+
const url = await getStaticCacheUrl({ functionId, hash })
|
|
113
|
+
const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR!
|
|
114
|
+
const filePath = path.join(publicUrl, url)
|
|
115
|
+
|
|
116
|
+
// Ensure the directory exists
|
|
117
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
118
|
+
|
|
119
|
+
// Store the result with fs
|
|
120
|
+
await fs.writeFile(
|
|
121
|
+
filePath,
|
|
122
|
+
startSerializer.stringify({
|
|
123
|
+
result: response.result,
|
|
124
|
+
context: response.context.sendContext,
|
|
125
|
+
}),
|
|
126
|
+
'utf-8',
|
|
127
|
+
)
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fetchItem = async ({
|
|
132
|
+
data,
|
|
133
|
+
functionId,
|
|
134
|
+
}: {
|
|
135
|
+
data: any
|
|
136
|
+
functionId: string
|
|
137
|
+
}) => {
|
|
138
|
+
const hash = jsonToFilenameSafeString(data)
|
|
139
|
+
const url = await getStaticCacheUrl({ functionId, hash })
|
|
140
|
+
|
|
141
|
+
let result: any = staticClientCache?.get(url)
|
|
142
|
+
|
|
143
|
+
result = await fetch(url, {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
})
|
|
146
|
+
.then((r) => r.text())
|
|
147
|
+
.then((d) => startSerializer.parse(d))
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const staticFunctionMiddleware = createMiddleware({ type: 'function' })
|
|
153
|
+
.client(async (ctx) => {
|
|
154
|
+
if (
|
|
155
|
+
process.env.NODE_ENV === 'production' &&
|
|
156
|
+
// do not run this during SSR on the server
|
|
157
|
+
typeof document !== 'undefined'
|
|
158
|
+
) {
|
|
159
|
+
const response = await fetchItem({
|
|
160
|
+
functionId: ctx.functionId,
|
|
161
|
+
data: ctx.data,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (response) {
|
|
165
|
+
return {
|
|
166
|
+
result: response.result,
|
|
167
|
+
context: { ...(ctx as any).context, ...response.context },
|
|
168
|
+
} as any
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return ctx.next()
|
|
172
|
+
})
|
|
173
|
+
.server(async (ctx) => {
|
|
174
|
+
const response = await ctx.next()
|
|
175
|
+
|
|
176
|
+
if (process.env.NODE_ENV === 'production') {
|
|
177
|
+
await serverFnStaticCache.setItem({
|
|
178
|
+
functionId: ctx.functionId,
|
|
179
|
+
response: { result: (response as any).result, context: ctx },
|
|
180
|
+
data: ctx.data,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return response
|
|
185
|
+
})
|