hadars 0.1.9 → 0.1.11

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/cli.js CHANGED
@@ -149,48 +149,48 @@ async function getStaticMarkupRenderer() {
149
149
  }
150
150
  return _renderToStaticMarkup;
151
151
  }
152
- var getHeadHtml = (seoData, renderToStaticMarkup) => {
153
- const metaEntries = Object.entries(seoData.meta);
154
- const linkEntries = Object.entries(seoData.link);
155
- const styleEntries = Object.entries(seoData.style);
156
- const scriptEntries = Object.entries(seoData.script);
157
- return renderToStaticMarkup(
158
- /* @__PURE__ */ jsxs(Fragment, { children: [
159
- /* @__PURE__ */ jsx("title", { children: seoData.title }),
160
- metaEntries.map(([id, options]) => /* @__PURE__ */ jsx(
161
- "meta",
162
- {
163
- id,
164
- ...options
165
- },
166
- id
167
- )),
168
- linkEntries.map(([id, options]) => /* @__PURE__ */ jsx(
169
- "link",
170
- {
171
- id,
172
- ...options
173
- },
174
- id
175
- )),
176
- styleEntries.map(([id, options]) => /* @__PURE__ */ jsx(
177
- "style",
178
- {
179
- id,
180
- ...options
181
- },
182
- id
183
- )),
184
- scriptEntries.map(([id, options]) => /* @__PURE__ */ jsx(
185
- "script",
186
- {
187
- id,
188
- ...options
189
- },
190
- id
191
- ))
192
- ] })
193
- );
152
+ var ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
153
+ var escAttr = (s) => s.replace(/[&<>"]/g, (c) => ESC[c]);
154
+ var escText = (s) => s.replace(/[&<>]/g, (c) => ESC[c]);
155
+ var ATTR = {
156
+ className: "class",
157
+ htmlFor: "for",
158
+ httpEquiv: "http-equiv",
159
+ charSet: "charset",
160
+ crossOrigin: "crossorigin",
161
+ noModule: "nomodule",
162
+ referrerPolicy: "referrerpolicy",
163
+ fetchPriority: "fetchpriority"
164
+ };
165
+ function renderHeadTag(tag, id, opts, selfClose = false) {
166
+ let attrs = ` id="${escAttr(id)}"`;
167
+ let inner = "";
168
+ for (const [k, v] of Object.entries(opts)) {
169
+ if (k === "key" || k === "children")
170
+ continue;
171
+ if (k === "dangerouslySetInnerHTML") {
172
+ inner = v.__html ?? "";
173
+ continue;
174
+ }
175
+ const attr = ATTR[k] ?? k;
176
+ if (v === true)
177
+ attrs += ` ${attr}`;
178
+ else if (v !== false && v != null)
179
+ attrs += ` ${attr}="${escAttr(String(v))}"`;
180
+ }
181
+ return selfClose ? `<${tag}${attrs}>` : `<${tag}${attrs}>${inner}</${tag}>`;
182
+ }
183
+ var getHeadHtml = (seoData) => {
184
+ let html = `<title>${escText(seoData.title ?? "")}</title>`;
185
+ for (const [id, opts] of Object.entries(seoData.meta))
186
+ html += renderHeadTag("meta", id, opts, true);
187
+ for (const [id, opts] of Object.entries(seoData.link))
188
+ html += renderHeadTag("link", id, opts, true);
189
+ for (const [id, opts] of Object.entries(seoData.style))
190
+ html += renderHeadTag("style", id, opts);
191
+ for (const [id, opts] of Object.entries(seoData.script))
192
+ html += renderHeadTag("script", id, opts);
193
+ return html;
194
194
  };
195
195
  var getReactResponse = async (req, opts) => {
196
196
  const App = opts.document.body;
@@ -233,18 +233,17 @@ var getReactResponse = async (req, opts) => {
233
233
  if (unsuspend.hasPending)
234
234
  await processUnsuspend();
235
235
  } while (unsuspend.hasPending && ++iters < 25);
236
- props = getAfterRenderProps ? await getAfterRenderProps(props, html) : props;
237
- try {
238
- globalThis.__hadarsUnsuspend = unsuspend;
239
- renderToStaticMarkup(
240
- /* @__PURE__ */ jsx(App, { ...{
241
- ...props,
242
- location: req.location,
243
- context
244
- } })
245
- );
246
- } finally {
247
- globalThis.__hadarsUnsuspend = null;
236
+ if (unsuspend.hasPending) {
237
+ console.warn("[hadars] SSR render loop hit the 25-iteration cap \u2014 some useServerData values may not be resolved. Check for data dependencies that are never fulfilled.");
238
+ }
239
+ if (getAfterRenderProps) {
240
+ props = await getAfterRenderProps(props, html);
241
+ try {
242
+ globalThis.__hadarsUnsuspend = unsuspend;
243
+ renderToStaticMarkup(/* @__PURE__ */ jsx(App, { ...{ ...props, location: req.location, context } }));
244
+ } finally {
245
+ globalThis.__hadarsUnsuspend = null;
246
+ }
248
247
  }
249
248
  const serverData = {};
250
249
  for (const [k, v] of unsuspend.cache) {
@@ -268,7 +267,7 @@ var getReactResponse = async (req, opts) => {
268
267
  return {
269
268
  ReactPage,
270
269
  status: context.head.status,
271
- headHtml: getHeadHtml(context.head, renderToStaticMarkup),
270
+ headHtml: getHeadHtml(context.head),
272
271
  renderPayload: {
273
272
  appProps: { ...props, location: req.location, context },
274
273
  clientProps
@@ -619,7 +618,45 @@ function nodeReadableToWebStream(readable) {
619
618
  });
620
619
  }
621
620
  var noopCtx = { upgrade: () => false };
621
+ var COMPRESSIBLE_RE = /\b(?:text\/|application\/(?:json|javascript|xml)|image\/svg\+xml)/;
622
+ function withCompression(handler) {
623
+ return async (req, ctx) => {
624
+ const res = await handler(req, ctx);
625
+ if (!res?.body)
626
+ return res;
627
+ if (!COMPRESSIBLE_RE.test(res.headers.get("Content-Type") ?? ""))
628
+ return res;
629
+ if (res.headers.has("Content-Encoding"))
630
+ return res;
631
+ const accept = req.headers.get("Accept-Encoding") ?? "";
632
+ const encoding = accept.includes("br") ? "br" : accept.includes("gzip") ? "gzip" : null;
633
+ if (!encoding)
634
+ return res;
635
+ try {
636
+ const compressed = res.body.pipeThrough(new globalThis.CompressionStream(encoding));
637
+ const headers = new Headers(res.headers);
638
+ headers.set("Content-Encoding", encoding);
639
+ headers.delete("Content-Length");
640
+ return new Response(compressed, { status: res.status, statusText: res.statusText, headers });
641
+ } catch {
642
+ return res;
643
+ }
644
+ };
645
+ }
646
+ function withRequestLogging(handler) {
647
+ return async (req, ctx) => {
648
+ const start = performance.now();
649
+ const res = await handler(req, ctx);
650
+ const ms = Math.round(performance.now() - start);
651
+ const status = res?.status ?? 404;
652
+ const path2 = new URL(req.url).pathname;
653
+ console.log(`[hadars] ${req.method} ${path2} ${status} ${ms}ms`);
654
+ return res;
655
+ };
656
+ }
622
657
  async function serve(port, fetchHandler, websocket) {
658
+ fetchHandler = withCompression(fetchHandler);
659
+ fetchHandler = withRequestLogging(fetchHandler);
623
660
  if (isBun) {
624
661
  globalThis.Bun.serve({
625
662
  port,
@@ -740,6 +777,7 @@ import { RspackDevServer } from "@rspack/dev-server";
740
777
  import pathMod3 from "node:path";
741
778
  import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
742
779
  import { createRequire as createRequire2 } from "node:module";
780
+ import crypto from "node:crypto";
743
781
  import fs from "node:fs/promises";
744
782
  import { existsSync as existsSync2 } from "node:fs";
745
783
  import os from "node:os";
@@ -766,42 +804,52 @@ var RenderWorkerPool = class {
766
804
  workerPending = /* @__PURE__ */ new Map();
767
805
  nextId = 0;
768
806
  rrIndex = 0;
807
+ _Worker = null;
808
+ _workerPath = "";
809
+ _ssrBundlePath = "";
769
810
  constructor(workerPath, size, ssrBundlePath) {
770
811
  this._init(workerPath, size, ssrBundlePath);
771
812
  }
772
813
  _init(workerPath, size, ssrBundlePath) {
814
+ this._workerPath = workerPath;
815
+ this._ssrBundlePath = ssrBundlePath;
773
816
  import("node:worker_threads").then(({ Worker }) => {
774
- for (let i = 0; i < size; i++) {
775
- const w = new Worker(workerPath, { workerData: { ssrBundlePath } });
776
- this.workerPending.set(w, /* @__PURE__ */ new Set());
777
- w.on("message", (msg) => {
778
- const { id, html, headHtml, status, error } = msg;
779
- const p = this.pending.get(id);
780
- if (!p)
781
- return;
782
- this.pending.delete(id);
783
- this.workerPending.get(w)?.delete(id);
784
- if (error)
785
- p.reject(new Error(error));
786
- else
787
- p.resolve({ html, headHtml, status });
788
- });
789
- w.on("error", (err) => {
790
- console.error("[hadars] Render worker error:", err);
791
- this._handleWorkerDeath(w, err);
792
- });
793
- w.on("exit", (code) => {
794
- if (code !== 0) {
795
- console.error(`[hadars] Render worker exited with code ${code}`);
796
- this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
797
- }
798
- });
799
- this.workers.push(w);
800
- }
817
+ this._Worker = Worker;
818
+ for (let i = 0; i < size; i++)
819
+ this._spawnWorker();
801
820
  }).catch((err) => {
802
821
  console.error("[hadars] Failed to initialise render worker pool:", err);
803
822
  });
804
823
  }
824
+ _spawnWorker() {
825
+ if (!this._Worker)
826
+ return;
827
+ const w = new this._Worker(this._workerPath, { workerData: { ssrBundlePath: this._ssrBundlePath } });
828
+ this.workerPending.set(w, /* @__PURE__ */ new Set());
829
+ w.on("message", (msg) => {
830
+ const { id, html, headHtml, status, error } = msg;
831
+ const p = this.pending.get(id);
832
+ if (!p)
833
+ return;
834
+ this.pending.delete(id);
835
+ this.workerPending.get(w)?.delete(id);
836
+ if (error)
837
+ p.reject(new Error(error));
838
+ else
839
+ p.resolve({ html, headHtml, status });
840
+ });
841
+ w.on("error", (err) => {
842
+ console.error("[hadars] Render worker error:", err);
843
+ this._handleWorkerDeath(w, err);
844
+ });
845
+ w.on("exit", (code) => {
846
+ if (code !== 0) {
847
+ console.error(`[hadars] Render worker exited with code ${code}`);
848
+ this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
849
+ }
850
+ });
851
+ this.workers.push(w);
852
+ }
805
853
  _handleWorkerDeath(w, err) {
806
854
  const idx = this.workers.indexOf(w);
807
855
  if (idx !== -1)
@@ -817,6 +865,8 @@ var RenderWorkerPool = class {
817
865
  }
818
866
  this.workerPending.delete(w);
819
867
  }
868
+ console.log("[hadars] Spawning replacement render worker");
869
+ this._spawnWorker();
820
870
  }
821
871
  nextWorker() {
822
872
  if (this.workers.length === 0)
@@ -906,6 +956,74 @@ var makePrecontentHtmlGetter = (htmlFilePromise) => {
906
956
  return [preHead + headHtml + postHead, postContent];
907
957
  };
908
958
  };
959
+ async function transformStream(data, stream) {
960
+ const writer = stream.writable.getWriter();
961
+ writer.write(data);
962
+ writer.close();
963
+ const chunks = [];
964
+ const reader = stream.readable.getReader();
965
+ while (true) {
966
+ const { done, value } = await reader.read();
967
+ if (done)
968
+ break;
969
+ chunks.push(value);
970
+ }
971
+ const total = chunks.reduce((n, c) => n + c.length, 0);
972
+ const out = new Uint8Array(total);
973
+ let offset = 0;
974
+ for (const c of chunks) {
975
+ out.set(c, offset);
976
+ offset += c.length;
977
+ }
978
+ return out;
979
+ }
980
+ var gzipCompress = (d) => transformStream(d, new globalThis.CompressionStream("gzip"));
981
+ var gzipDecompress = (d) => transformStream(d, new globalThis.DecompressionStream("gzip"));
982
+ function createRenderCache(opts, handler) {
983
+ const store = /* @__PURE__ */ new Map();
984
+ return async (req, ctx) => {
985
+ const hadarsReq = parseRequest(req);
986
+ const cacheOpts = await opts(hadarsReq);
987
+ const key = cacheOpts?.key ?? null;
988
+ if (key != null) {
989
+ const entry = store.get(key);
990
+ if (entry) {
991
+ if (entry.expiresAt == null || Date.now() < entry.expiresAt) {
992
+ const accept = req.headers.get("Accept-Encoding") ?? "";
993
+ if (accept.includes("gzip")) {
994
+ return new Response(entry.body.buffer, { status: entry.status, headers: entry.headers });
995
+ }
996
+ const plain = await gzipDecompress(entry.body);
997
+ const headers = entry.headers.filter(([k]) => k.toLowerCase() !== "content-encoding");
998
+ return new Response(plain.buffer, { status: entry.status, headers });
999
+ }
1000
+ store.delete(key);
1001
+ }
1002
+ }
1003
+ const res = await handler(req, ctx);
1004
+ if (key != null && res) {
1005
+ const ttl = cacheOpts?.ttl;
1006
+ res.clone().arrayBuffer().then(async (buf) => {
1007
+ const body = await gzipCompress(new Uint8Array(buf));
1008
+ const headers = [];
1009
+ res.headers.forEach((v, k) => {
1010
+ if (k.toLowerCase() !== "content-encoding" && k.toLowerCase() !== "content-length") {
1011
+ headers.push([k, v]);
1012
+ }
1013
+ });
1014
+ headers.push(["content-encoding", "gzip"]);
1015
+ store.set(key, {
1016
+ body,
1017
+ status: res.status,
1018
+ headers,
1019
+ expiresAt: ttl != null ? Date.now() + ttl : null
1020
+ });
1021
+ }).catch(() => {
1022
+ });
1023
+ }
1024
+ return res;
1025
+ };
1026
+ }
909
1027
  var SSR_FILENAME = "index.ssr.js";
910
1028
  var __dirname2 = process.cwd();
911
1029
  var getSuffix = (mode) => mode === "development" ? `?v=${Date.now()}` : "";
@@ -968,7 +1086,7 @@ var dev = async (options) => {
968
1086
  }
969
1087
  const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
970
1088
  await fs.writeFile(tmpFilePath, clientScript);
971
- let ssrBuildId = Date.now();
1089
+ let ssrBuildId = crypto.randomBytes(4).toString("hex");
972
1090
  const clientCompiler = createClientCompiler(tmpFilePath, {
973
1091
  target: "web",
974
1092
  output: {
@@ -1083,7 +1201,7 @@ var dev = async (options) => {
1083
1201
  } catch (e) {
1084
1202
  }
1085
1203
  if (chunk.includes(rebuildMarker)) {
1086
- ssrBuildId = Date.now();
1204
+ ssrBuildId = crypto.randomBytes(4).toString("hex");
1087
1205
  console.log("[hadars] SSR bundle updated, build id:", ssrBuildId);
1088
1206
  }
1089
1207
  }
@@ -1137,23 +1255,32 @@ var dev = async (options) => {
1137
1255
  return projectRes;
1138
1256
  const ssrComponentPath = pathMod3.join(__dirname2, HadarsFolder, SSR_FILENAME);
1139
1257
  const importPath = pathToFileURL2(ssrComponentPath).href + `?t=${ssrBuildId}`;
1140
- const {
1141
- default: Component,
1142
- getInitProps,
1143
- getAfterRenderProps,
1144
- getFinalProps
1145
- } = await import(importPath);
1146
- const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1147
- document: {
1148
- body: Component,
1149
- lang: "en",
1258
+ try {
1259
+ const {
1260
+ default: Component,
1150
1261
  getInitProps,
1151
1262
  getAfterRenderProps,
1152
1263
  getFinalProps
1153
- }
1154
- });
1155
- const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1156
- return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
1264
+ } = await import(importPath);
1265
+ const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1266
+ document: {
1267
+ body: Component,
1268
+ lang: "en",
1269
+ getInitProps,
1270
+ getAfterRenderProps,
1271
+ getFinalProps
1272
+ }
1273
+ });
1274
+ const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1275
+ return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
1276
+ } catch (err) {
1277
+ console.error("[hadars] SSR render error:", err);
1278
+ const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "&lt;");
1279
+ return new Response(`<!doctype html><pre style="white-space:pre-wrap">${msg}</pre>`, {
1280
+ status: 500,
1281
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1282
+ });
1283
+ }
1157
1284
  }, options.websocket);
1158
1285
  };
1159
1286
  var build = async (options) => {
@@ -1236,7 +1363,7 @@ var run = async (options) => {
1236
1363
  const getPrecontentHtml = makePrecontentHtmlGetter(
1237
1364
  fs.readFile(pathMod3.join(__dirname2, StaticPath, "out.html"), "utf-8")
1238
1365
  );
1239
- await serve(port, async (req, ctx) => {
1366
+ const runHandler = async (req, ctx) => {
1240
1367
  const request = parseRequest(req);
1241
1368
  if (handler) {
1242
1369
  const res = await handler(request);
@@ -1273,33 +1400,43 @@ var run = async (options) => {
1273
1400
  const componentPath = pathToFileURL2(
1274
1401
  pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME)
1275
1402
  ).href;
1276
- const {
1277
- default: Component,
1278
- getInitProps,
1279
- getAfterRenderProps,
1280
- getFinalProps
1281
- } = await import(componentPath);
1282
- if (renderPool) {
1283
- const serialReq = await serializeRequest(request);
1284
- const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
1285
- const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
1286
- return new Response(precontentHtml + html + postContent, {
1287
- headers: { "Content-Type": "text/html; charset=utf-8" },
1288
- status: wStatus
1289
- });
1290
- }
1291
- const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1292
- document: {
1293
- body: Component,
1294
- lang: "en",
1403
+ try {
1404
+ const {
1405
+ default: Component,
1295
1406
  getInitProps,
1296
1407
  getAfterRenderProps,
1297
1408
  getFinalProps
1409
+ } = await import(componentPath);
1410
+ if (renderPool) {
1411
+ const serialReq = await serializeRequest(request);
1412
+ const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
1413
+ const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
1414
+ return new Response(precontentHtml + html + postContent, {
1415
+ headers: { "Content-Type": "text/html; charset=utf-8" },
1416
+ status: wStatus
1417
+ });
1298
1418
  }
1299
- });
1300
- const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1301
- return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
1302
- }, options.websocket);
1419
+ const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1420
+ document: {
1421
+ body: Component,
1422
+ lang: "en",
1423
+ getInitProps,
1424
+ getAfterRenderProps,
1425
+ getFinalProps
1426
+ }
1427
+ });
1428
+ const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1429
+ return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
1430
+ } catch (err) {
1431
+ console.error("[hadars] SSR render error:", err);
1432
+ return new Response("Internal Server Error", { status: 500 });
1433
+ }
1434
+ };
1435
+ await serve(
1436
+ port,
1437
+ options.cache ? createRenderCache(options.cache, runHandler) : runHandler,
1438
+ options.websocket
1439
+ );
1303
1440
  };
1304
1441
 
1305
1442
  // cli-lib.ts
package/dist/index.cjs CHANGED
@@ -172,12 +172,13 @@ var AppProviderCSR = import_react.default.memo(({ children }) => {
172
172
  var useApp = () => import_react.default.useContext(AppContext);
173
173
  var clientServerDataCache = /* @__PURE__ */ new Map();
174
174
  function initServerDataCache(data) {
175
+ clientServerDataCache.clear();
175
176
  for (const [k, v] of Object.entries(data)) {
176
177
  clientServerDataCache.set(k, v);
177
178
  }
178
179
  }
179
180
  function useServerData(key, fn) {
180
- const cacheKey = Array.isArray(key) ? key.join("\0") : key;
181
+ const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
181
182
  if (typeof window !== "undefined") {
182
183
  if (clientServerDataCache.has(cacheKey)) {
183
184
  return clientServerDataCache.get(cacheKey);
package/dist/index.d.ts CHANGED
@@ -92,6 +92,29 @@ interface HadarsOptions {
92
92
  * Has no effect on the SSR bundle or dev mode.
93
93
  */
94
94
  optimization?: Record<string, unknown>;
95
+ /**
96
+ * SSR response cache for `run()` mode. Has no effect in `dev()` mode.
97
+ *
98
+ * Receives the incoming request and should return `{ key, ttl? }` to cache
99
+ * the response, or `null`/`undefined` to skip caching for that request.
100
+ * `ttl` is the time-to-live in milliseconds; omit for entries that never expire.
101
+ * The function may be async.
102
+ *
103
+ * @example
104
+ * // Cache every page by pathname (no per-user personalisation):
105
+ * cache: (req) => ({ key: req.pathname })
106
+ *
107
+ * @example
108
+ * // Cache with a per-route TTL, skip authenticated requests:
109
+ * cache: (req) => req.cookies.session ? null : { key: req.pathname, ttl: 60_000 }
110
+ */
111
+ cache?: (req: HadarsRequest) => {
112
+ key: string;
113
+ ttl?: number;
114
+ } | null | undefined | Promise<{
115
+ key: string;
116
+ ttl?: number;
117
+ } | null | undefined>;
95
118
  }
96
119
  type SwcPluginItem = string | [string, Record<string, unknown>] | {
97
120
  path: string;
@@ -106,7 +129,8 @@ interface HadarsRequest extends Request {
106
129
  }
107
130
 
108
131
  /** Call this before hydrating to seed the client cache from the server's data.
109
- * Invoked automatically by the hadars client bootstrap. */
132
+ * Invoked automatically by the hadars client bootstrap.
133
+ * Always clears the existing cache before populating — call with `{}` to just clear. */
110
134
  declare function initServerDataCache(data: Record<string, unknown>): void;
111
135
  /**
112
136
  * Fetch async data on the server during SSR. Returns `undefined` on the first
package/dist/index.js CHANGED
@@ -132,12 +132,13 @@ var AppProviderCSR = React.memo(({ children }) => {
132
132
  var useApp = () => React.useContext(AppContext);
133
133
  var clientServerDataCache = /* @__PURE__ */ new Map();
134
134
  function initServerDataCache(data) {
135
+ clientServerDataCache.clear();
135
136
  for (const [k, v] of Object.entries(data)) {
136
137
  clientServerDataCache.set(k, v);
137
138
  }
138
139
  }
139
140
  function useServerData(key, fn) {
140
- const cacheKey = Array.isArray(key) ? key.join("\0") : key;
141
+ const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
141
142
  if (typeof window !== "undefined") {
142
143
  if (clientServerDataCache.has(cacheKey)) {
143
144
  return clientServerDataCache.get(cacheKey);