hadars 0.3.2 → 0.4.0
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/README.md +48 -17
- package/dist/cli.js +4 -0
- package/dist/cloudflare.cjs +2 -0
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js +2 -0
- package/dist/{hadars-CSWWhlQC.d.cts → hadars-CPplIz_z.d.cts} +17 -0
- package/dist/{hadars-CSWWhlQC.d.ts → hadars-CPplIz_z.d.ts} +17 -0
- package/dist/index.cjs +32 -59
- package/dist/index.d.cts +12 -8
- package/dist/index.d.ts +12 -8
- package/dist/index.js +32 -59
- package/dist/lambda.cjs +2 -0
- package/dist/lambda.d.cts +1 -1
- package/dist/lambda.d.ts +1 -1
- package/dist/lambda.js +2 -0
- package/dist/loader.cjs +2 -7
- package/dist/utils/Head.tsx +68 -116
- package/package.json +1 -1
- package/src/build.ts +2 -0
- package/src/cloudflare.ts +1 -0
- package/src/lambda.ts +1 -0
- package/src/types/hadars.ts +17 -0
- package/src/utils/Head.tsx +68 -116
- package/src/utils/loader.ts +5 -13
package/README.md
CHANGED
|
@@ -21,30 +21,30 @@ Bring your own router (or none), keep your components as plain React, and get SS
|
|
|
21
21
|
## Benchmarks
|
|
22
22
|
|
|
23
23
|
<!-- BENCHMARK_START -->
|
|
24
|
-
> Last run: 2026-
|
|
25
|
-
> hadars is **
|
|
24
|
+
> Last run: 2026-04-02 · 120s · 100 connections · Bun runtime
|
|
25
|
+
> hadars is **9.3x faster** in requests/sec
|
|
26
26
|
|
|
27
27
|
**Throughput** (autocannon, 120s)
|
|
28
28
|
|
|
29
29
|
| Metric | hadars | Next.js |
|
|
30
30
|
|---|---:|---:|
|
|
31
|
-
| Requests/sec | **
|
|
32
|
-
| Latency median | **
|
|
33
|
-
| Latency p99 | **
|
|
34
|
-
| Throughput | **
|
|
35
|
-
| Peak RSS |
|
|
36
|
-
| Avg RSS |
|
|
37
|
-
| Build time | 0.
|
|
31
|
+
| Requests/sec | **158** | 17 |
|
|
32
|
+
| Latency median | **634 ms** | 2754 ms |
|
|
33
|
+
| Latency p99 | **1031 ms** | 4140 ms |
|
|
34
|
+
| Throughput | **44.97** MB/s | 9.67 MB/s |
|
|
35
|
+
| Peak RSS | 1001.1 MB | **477.2 MB** |
|
|
36
|
+
| Avg RSS | 778.8 MB | **426.8 MB** |
|
|
37
|
+
| Build time | 0.6 s | 6.0 s |
|
|
38
38
|
|
|
39
39
|
**Page load** (Playwright · Chromium headless · median)
|
|
40
40
|
|
|
41
41
|
| Metric | hadars | Next.js |
|
|
42
42
|
|---|---:|---:|
|
|
43
|
-
| TTFB | **
|
|
44
|
-
| FCP | **
|
|
45
|
-
| DOMContentLoaded | **
|
|
46
|
-
| Load | **
|
|
47
|
-
| Peak RSS |
|
|
43
|
+
| TTFB | **17 ms** | 45 ms |
|
|
44
|
+
| FCP | **92 ms** | 140 ms |
|
|
45
|
+
| DOMContentLoaded | **37 ms** | 127 ms |
|
|
46
|
+
| Load | **118 ms** | 172 ms |
|
|
47
|
+
| Peak RSS | 479.5 MB | **295.5 MB** |
|
|
48
48
|
<!-- BENCHMARK_END -->
|
|
49
49
|
|
|
50
50
|
## Quick start
|
|
@@ -148,16 +148,28 @@ Fetch async data inside a component during SSR. The framework's render loop awai
|
|
|
148
148
|
import { useServerData } from 'hadars';
|
|
149
149
|
|
|
150
150
|
const UserCard = ({ userId }: { userId: string }) => {
|
|
151
|
-
const user = useServerData(
|
|
151
|
+
const user = useServerData(() => db.getUser(userId));
|
|
152
152
|
if (!user) return null; // undefined while pending on the first SSR pass
|
|
153
153
|
return <p>{user.name}</p>;
|
|
154
154
|
};
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
The cache key is derived automatically from the call-site's position in the component tree via `useId()` — no manual key is needed.
|
|
158
|
+
|
|
158
159
|
- **Server (SSR)** - calls `fn()`, awaits the result across render iterations, returns `undefined` until resolved
|
|
159
160
|
- **Client (hydration)** - reads the pre-resolved value from the hydration cache serialised by the server; `fn()` is never called in the browser
|
|
160
|
-
- **Client (navigation)** - when a component mounts during client-side navigation and its
|
|
161
|
+
- **Client (navigation)** - when a component mounts during client-side navigation and its data is not in the cache, hadars fires a single `GET <current-url>` with `Accept: application/json`; all `useServerData` calls within the same render are batched into one request and suspended via React Suspense until the server returns the JSON data map
|
|
162
|
+
|
|
163
|
+
### Options
|
|
164
|
+
|
|
165
|
+
| Option | Type | Default | Description |
|
|
166
|
+
|---|---|---|---|
|
|
167
|
+
| `cache` | `boolean` | `true` | When `false`, the cached value is evicted when the component unmounts so the next mount fetches fresh data from the server. Safe with React Strict Mode. |
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// Uptime changes every request — evict on unmount so re-mounting always fetches fresh
|
|
171
|
+
const uptime = useServerData(() => process.uptime(), { cache: false });
|
|
172
|
+
```
|
|
161
173
|
|
|
162
174
|
## Data lifecycle hooks
|
|
163
175
|
|
|
@@ -192,6 +204,7 @@ const UserCard = ({ userId }: { userId: string }) => {
|
|
|
192
204
|
| `paths` | `function` | - | Returns URL list to pre-render with `hadars export static`; receives `HadarsStaticContext` |
|
|
193
205
|
| `sources` | `array` | - | Gatsby-compatible source plugins; hadars infers a GraphQL schema from their nodes |
|
|
194
206
|
| `graphql` | `function` | - | Custom GraphQL executor passed to `paths()` and `getInitProps()` as `ctx.graphql` |
|
|
207
|
+
| `onError` | `function` | - | Called on every SSR render error; use to forward to Sentry, Datadog, etc. |
|
|
195
208
|
|
|
196
209
|
### moduleRules example
|
|
197
210
|
|
|
@@ -227,6 +240,24 @@ const config: HadarsOptions = {
|
|
|
227
240
|
export default config;
|
|
228
241
|
```
|
|
229
242
|
|
|
243
|
+
### Error monitoring example
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import * as Sentry from '@sentry/node';
|
|
247
|
+
import type { HadarsOptions } from 'hadars';
|
|
248
|
+
|
|
249
|
+
const config: HadarsOptions = {
|
|
250
|
+
entry: 'src/App.tsx',
|
|
251
|
+
onError: (err, req) => Sentry.captureException(err, {
|
|
252
|
+
extra: { url: req.url, method: req.method },
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export default config;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`onError` is called on every SSR render error in `dev()`, `run()`, Lambda, and Cloudflare adapters. The handler may be async — hadars fires it without awaiting so it never delays the error response to the browser.
|
|
260
|
+
|
|
230
261
|
## Static Export
|
|
231
262
|
|
|
232
263
|
> **Experimental.** Static export and Gatsby-compatible source plugins are new features. The API — including config shape, context object, and schema inference behaviour — may change in future releases without a major version bump.
|
package/dist/cli.js
CHANGED
|
@@ -2737,6 +2737,8 @@ var dev = async (options) => {
|
|
|
2737
2737
|
return buildSsrResponse(head, status, getAppBody, finalize, getPrecontentHtml);
|
|
2738
2738
|
} catch (err) {
|
|
2739
2739
|
console.error("[hadars] SSR render error:", err);
|
|
2740
|
+
options.onError?.(err, request)?.catch?.(() => {
|
|
2741
|
+
});
|
|
2740
2742
|
const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "<");
|
|
2741
2743
|
return new Response(`<!doctype html><pre style="white-space:pre-wrap">${msg}</pre>`, {
|
|
2742
2744
|
status: 500,
|
|
@@ -2896,6 +2898,8 @@ var run = async (options) => {
|
|
|
2896
2898
|
return buildSsrResponse(head, status, getAppBody, finalize, getPrecontentHtml);
|
|
2897
2899
|
} catch (err) {
|
|
2898
2900
|
console.error("[hadars] SSR render error:", err);
|
|
2901
|
+
options.onError?.(err, request)?.catch?.(() => {
|
|
2902
|
+
});
|
|
2899
2903
|
return new Response("Internal Server Error", { status: 500 });
|
|
2900
2904
|
}
|
|
2901
2905
|
};
|
package/dist/cloudflare.cjs
CHANGED
|
@@ -1387,6 +1387,8 @@ function createCloudflareHandler(options, bundled) {
|
|
|
1387
1387
|
});
|
|
1388
1388
|
} catch (err) {
|
|
1389
1389
|
console.error("[hadars] SSR render error:", err);
|
|
1390
|
+
options.onError?.(err, request)?.catch?.(() => {
|
|
1391
|
+
});
|
|
1390
1392
|
return new Response("Internal Server Error", { status: 500 });
|
|
1391
1393
|
}
|
|
1392
1394
|
};
|
package/dist/cloudflare.d.cts
CHANGED
package/dist/cloudflare.d.ts
CHANGED
package/dist/cloudflare.js
CHANGED
|
@@ -53,6 +53,8 @@ function createCloudflareHandler(options, bundled) {
|
|
|
53
53
|
});
|
|
54
54
|
} catch (err) {
|
|
55
55
|
console.error("[hadars] SSR render error:", err);
|
|
56
|
+
options.onError?.(err, request)?.catch?.(() => {
|
|
57
|
+
});
|
|
56
58
|
return new Response("Internal Server Error", { status: 500 });
|
|
57
59
|
}
|
|
58
60
|
};
|
|
@@ -257,6 +257,23 @@ interface HadarsOptions {
|
|
|
257
257
|
* ]
|
|
258
258
|
*/
|
|
259
259
|
sources?: HadarsSourceEntry[];
|
|
260
|
+
/**
|
|
261
|
+
* Called whenever an SSR render error is caught, in both `dev()` and `run()` mode
|
|
262
|
+
* as well as the Lambda and Cloudflare adapters.
|
|
263
|
+
*
|
|
264
|
+
* Use this to forward errors to your monitoring service (Sentry, Datadog, etc.)
|
|
265
|
+
* without affecting the response sent to the browser.
|
|
266
|
+
* The handler may be async — hadars fires it and does not await the result,
|
|
267
|
+
* so it never delays the error response.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* import * as Sentry from '@sentry/node';
|
|
271
|
+
* onError: (err, req) => Sentry.captureException(err, { extra: { url: req.url } })
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* onError: (err, req) => console.error('[myapp]', req.method, req.url, err)
|
|
275
|
+
*/
|
|
276
|
+
onError?: (err: Error, req: Request) => void | Promise<void>;
|
|
260
277
|
}
|
|
261
278
|
/**
|
|
262
279
|
* A Gatsby-compatible source plugin entry, matching the format used in
|
|
@@ -257,6 +257,23 @@ interface HadarsOptions {
|
|
|
257
257
|
* ]
|
|
258
258
|
*/
|
|
259
259
|
sources?: HadarsSourceEntry[];
|
|
260
|
+
/**
|
|
261
|
+
* Called whenever an SSR render error is caught, in both `dev()` and `run()` mode
|
|
262
|
+
* as well as the Lambda and Cloudflare adapters.
|
|
263
|
+
*
|
|
264
|
+
* Use this to forward errors to your monitoring service (Sentry, Datadog, etc.)
|
|
265
|
+
* without affecting the response sent to the browser.
|
|
266
|
+
* The handler may be async — hadars fires it and does not await the result,
|
|
267
|
+
* so it never delays the error response.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* import * as Sentry from '@sentry/node';
|
|
271
|
+
* onError: (err, req) => Sentry.captureException(err, { extra: { url: req.url } })
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* onError: (err, req) => console.error('[myapp]', req.method, req.url, err)
|
|
275
|
+
*/
|
|
276
|
+
onError?: (err: Error, req: Request) => void | Promise<void>;
|
|
260
277
|
}
|
|
261
278
|
/**
|
|
262
279
|
* A Gatsby-compatible source plugin entry, matching the format used in
|
package/dist/index.cjs
CHANGED
|
@@ -182,42 +182,46 @@ function getCtx() {
|
|
|
182
182
|
var clientServerDataCache = /* @__PURE__ */ new Map();
|
|
183
183
|
var pendingDataFetch = /* @__PURE__ */ new Map();
|
|
184
184
|
var fetchedPaths = /* @__PURE__ */ new Set();
|
|
185
|
-
var
|
|
186
|
-
var
|
|
187
|
-
function scheduleUnclaimedKeyCheck() {
|
|
188
|
-
if (unclaimedKeyCheckScheduled) return;
|
|
189
|
-
unclaimedKeyCheckScheduled = true;
|
|
190
|
-
setTimeout(() => {
|
|
191
|
-
unclaimedKeyCheckScheduled = false;
|
|
192
|
-
if (ssrInitialKeys && ssrInitialKeys.size > 0) {
|
|
193
|
-
console.warn(
|
|
194
|
-
`[hadars] useServerData: ${ssrInitialKeys.size} server-resolved key(s) were never claimed during client hydration: ${[...ssrInitialKeys].map((k) => JSON.stringify(k)).join(", ")}. This usually means the key passed to useServerData was different on the server than on the client (e.g. it contains Date.now(), Math.random(), or another value that changes between renders). Keys must be stable and deterministic.`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
ssrInitialKeys = null;
|
|
198
|
-
}, 0);
|
|
199
|
-
}
|
|
185
|
+
var _navValues = [];
|
|
186
|
+
var _navIdx = 0;
|
|
200
187
|
function initServerDataCache(data) {
|
|
201
188
|
clientServerDataCache.clear();
|
|
202
|
-
|
|
189
|
+
_navValues = [];
|
|
190
|
+
_navIdx = 0;
|
|
203
191
|
for (const [k, v] of Object.entries(data)) {
|
|
204
192
|
clientServerDataCache.set(k, v);
|
|
205
|
-
ssrInitialKeys.add(k);
|
|
206
193
|
}
|
|
207
194
|
}
|
|
208
|
-
function useServerData(
|
|
209
|
-
const cacheKey =
|
|
195
|
+
function useServerData(fn, options) {
|
|
196
|
+
const cacheKey = import_react.default.useId();
|
|
197
|
+
const evictTimerRef = import_react.default.useRef(null);
|
|
198
|
+
import_react.default.useEffect(() => {
|
|
199
|
+
if (options?.cache !== false) return;
|
|
200
|
+
if (evictTimerRef.current !== null) {
|
|
201
|
+
clearTimeout(evictTimerRef.current);
|
|
202
|
+
evictTimerRef.current = null;
|
|
203
|
+
}
|
|
204
|
+
return () => {
|
|
205
|
+
evictTimerRef.current = setTimeout(() => {
|
|
206
|
+
clientServerDataCache.delete(cacheKey);
|
|
207
|
+
evictTimerRef.current = null;
|
|
208
|
+
}, 0);
|
|
209
|
+
};
|
|
210
|
+
}, []);
|
|
210
211
|
if (typeof window !== "undefined") {
|
|
211
212
|
if (clientServerDataCache.has(cacheKey)) {
|
|
212
|
-
ssrInitialKeys?.delete(cacheKey);
|
|
213
213
|
return clientServerDataCache.get(cacheKey);
|
|
214
214
|
}
|
|
215
|
-
if (ssrInitialKeys !== null && ssrInitialKeys.size > 0) {
|
|
216
|
-
scheduleUnclaimedKeyCheck();
|
|
217
|
-
}
|
|
218
215
|
const pathKey = window.location.pathname + window.location.search;
|
|
219
216
|
if (fetchedPaths.has(pathKey)) {
|
|
220
|
-
|
|
217
|
+
if (_navIdx < _navValues.length) {
|
|
218
|
+
const value = _navValues[_navIdx++];
|
|
219
|
+
clientServerDataCache.set(cacheKey, value);
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
fetchedPaths.delete(pathKey);
|
|
223
|
+
_navValues = [];
|
|
224
|
+
_navIdx = 0;
|
|
221
225
|
}
|
|
222
226
|
if (!pendingDataFetch.has(pathKey)) {
|
|
223
227
|
let resolve;
|
|
@@ -236,9 +240,9 @@ function useServerData(key, fn) {
|
|
|
236
240
|
const res = await fetch(pathKey, { headers: { "Accept": "application/json" } });
|
|
237
241
|
if (res.ok) json = await res.json();
|
|
238
242
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
_navValues = Object.values(json?.serverData ?? {});
|
|
244
|
+
_navIdx = 0;
|
|
245
|
+
fetchedPaths.clear();
|
|
242
246
|
} finally {
|
|
243
247
|
fetchedPaths.add(pathKey);
|
|
244
248
|
pendingDataFetch.delete(pathKey);
|
|
@@ -250,12 +254,7 @@ function useServerData(key, fn) {
|
|
|
250
254
|
}
|
|
251
255
|
const unsuspend = globalThis.__hadarsUnsuspend;
|
|
252
256
|
if (!unsuspend) return void 0;
|
|
253
|
-
const _u = unsuspend;
|
|
254
|
-
if (!_u.pendingCreated) _u.pendingCreated = 0;
|
|
255
257
|
const existing = unsuspend.cache.get(cacheKey);
|
|
256
|
-
if (existing?.status === "fulfilled" && _u.lastPendingKey === cacheKey) {
|
|
257
|
-
_u.lastPendingKeyAccessed = true;
|
|
258
|
-
}
|
|
259
258
|
if (!existing) {
|
|
260
259
|
const result = fn();
|
|
261
260
|
const isThenable = result !== null && typeof result === "object" && typeof result.then === "function";
|
|
@@ -264,22 +263,6 @@ function useServerData(key, fn) {
|
|
|
264
263
|
unsuspend.cache.set(cacheKey, { status: "fulfilled", value });
|
|
265
264
|
return value;
|
|
266
265
|
}
|
|
267
|
-
if (_u.lastPendingKey != null && !_u.lastPendingKeyAccessed) {
|
|
268
|
-
const prev = unsuspend.cache.get(_u.lastPendingKey);
|
|
269
|
-
if (prev?.status === "fulfilled") {
|
|
270
|
-
throw new Error(
|
|
271
|
-
`[hadars] useServerData: key ${JSON.stringify(cacheKey)} is not stable between render passes. The previous pass resolved ${JSON.stringify(_u.lastPendingKey)} but it was not requested in this pass \u2014 the key is changing between renders. Avoid dynamic values in keys (e.g. Date.now() or Math.random()); use stable, deterministic identifiers instead.`
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
_u.pendingCreated++;
|
|
276
|
-
if (_u.pendingCreated > 100) {
|
|
277
|
-
throw new Error(
|
|
278
|
-
`[hadars] useServerData: more than 100 async keys created in a single render. This usually means a key is not stable between renders (e.g. it contains Date.now() or Math.random()). Currently offending key: ${JSON.stringify(cacheKey)}.`
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
_u.lastPendingKey = cacheKey;
|
|
282
|
-
_u.lastPendingKeyAccessed = false;
|
|
283
266
|
const promise = result.then(
|
|
284
267
|
(value) => {
|
|
285
268
|
unsuspend.cache.set(cacheKey, { status: "fulfilled", value });
|
|
@@ -297,18 +280,8 @@ function useServerData(key, fn) {
|
|
|
297
280
|
if (existing.status === "rejected") throw existing.reason;
|
|
298
281
|
return existing.value;
|
|
299
282
|
}
|
|
300
|
-
function toCacheKey(doc) {
|
|
301
|
-
if (typeof doc === "string") return doc.trim();
|
|
302
|
-
for (const def of doc?.definitions ?? []) {
|
|
303
|
-
if (def.kind === "OperationDefinition" && def.name?.value) {
|
|
304
|
-
return `op:${def.name.value}`;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return JSON.stringify(doc?.definitions ?? doc);
|
|
308
|
-
}
|
|
309
283
|
function useGraphQL(query, variables) {
|
|
310
|
-
|
|
311
|
-
return useServerData(key, async () => {
|
|
284
|
+
return useServerData(async () => {
|
|
312
285
|
const executor = globalThis.__hadarsGraphQL;
|
|
313
286
|
if (!executor) {
|
|
314
287
|
throw new Error(
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { b as HadarsDocumentNode } from './hadars-
|
|
2
|
-
export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-
|
|
1
|
+
import { b as HadarsDocumentNode } from './hadars-CPplIz_z.cjs';
|
|
2
|
+
export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-CPplIz_z.cjs';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
5
|
/** Call this before hydrating to seed the client cache from the server's data.
|
|
@@ -14,9 +14,8 @@ declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
|
14
14
|
* On the client the pre-resolved value is read from the hydration cache
|
|
15
15
|
* serialised into the page by the server, so no fetch is issued in the browser.
|
|
16
16
|
*
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
* unique within the page.
|
|
17
|
+
* The cache key is derived automatically from the call-site's position in the
|
|
18
|
+
* component tree via `useId()` — no manual key is required.
|
|
20
19
|
*
|
|
21
20
|
* `fn` may return a `Promise<T>` (async usage) or return `T` synchronously.
|
|
22
21
|
* The resolved value is serialised into `__serverData` and returned from cache
|
|
@@ -28,11 +27,16 @@ declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
|
28
27
|
* React Suspense until the server returns the JSON map of resolved values.
|
|
29
28
|
*
|
|
30
29
|
* @example
|
|
31
|
-
* const user = useServerData(
|
|
32
|
-
* const post = useServerData(
|
|
30
|
+
* const user = useServerData(() => db.getUser(id));
|
|
31
|
+
* const post = useServerData(() => db.getPost(postId));
|
|
33
32
|
* if (!user) return null; // undefined while pending on the first SSR pass
|
|
33
|
+
*
|
|
34
|
+
* // cache: false — evicts the entry on unmount so the next mount fetches fresh data
|
|
35
|
+
* const stats = useServerData(() => getServerStats(), { cache: false });
|
|
34
36
|
*/
|
|
35
|
-
declare function useServerData<T>(
|
|
37
|
+
declare function useServerData<T>(fn: () => Promise<T> | T, options?: {
|
|
38
|
+
cache?: boolean;
|
|
39
|
+
}): T | undefined;
|
|
36
40
|
/**
|
|
37
41
|
* Execute a GraphQL query server-side and return the result.
|
|
38
42
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { b as HadarsDocumentNode } from './hadars-
|
|
2
|
-
export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-
|
|
1
|
+
import { b as HadarsDocumentNode } from './hadars-CPplIz_z.js';
|
|
2
|
+
export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-CPplIz_z.js';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
5
|
/** Call this before hydrating to seed the client cache from the server's data.
|
|
@@ -14,9 +14,8 @@ declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
|
14
14
|
* On the client the pre-resolved value is read from the hydration cache
|
|
15
15
|
* serialised into the page by the server, so no fetch is issued in the browser.
|
|
16
16
|
*
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
* unique within the page.
|
|
17
|
+
* The cache key is derived automatically from the call-site's position in the
|
|
18
|
+
* component tree via `useId()` — no manual key is required.
|
|
20
19
|
*
|
|
21
20
|
* `fn` may return a `Promise<T>` (async usage) or return `T` synchronously.
|
|
22
21
|
* The resolved value is serialised into `__serverData` and returned from cache
|
|
@@ -28,11 +27,16 @@ declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
|
28
27
|
* React Suspense until the server returns the JSON map of resolved values.
|
|
29
28
|
*
|
|
30
29
|
* @example
|
|
31
|
-
* const user = useServerData(
|
|
32
|
-
* const post = useServerData(
|
|
30
|
+
* const user = useServerData(() => db.getUser(id));
|
|
31
|
+
* const post = useServerData(() => db.getPost(postId));
|
|
33
32
|
* if (!user) return null; // undefined while pending on the first SSR pass
|
|
33
|
+
*
|
|
34
|
+
* // cache: false — evicts the entry on unmount so the next mount fetches fresh data
|
|
35
|
+
* const stats = useServerData(() => getServerStats(), { cache: false });
|
|
34
36
|
*/
|
|
35
|
-
declare function useServerData<T>(
|
|
37
|
+
declare function useServerData<T>(fn: () => Promise<T> | T, options?: {
|
|
38
|
+
cache?: boolean;
|
|
39
|
+
}): T | undefined;
|
|
36
40
|
/**
|
|
37
41
|
* Execute a GraphQL query server-side and return the result.
|
|
38
42
|
*
|
package/dist/index.js
CHANGED
|
@@ -142,42 +142,46 @@ function getCtx() {
|
|
|
142
142
|
var clientServerDataCache = /* @__PURE__ */ new Map();
|
|
143
143
|
var pendingDataFetch = /* @__PURE__ */ new Map();
|
|
144
144
|
var fetchedPaths = /* @__PURE__ */ new Set();
|
|
145
|
-
var
|
|
146
|
-
var
|
|
147
|
-
function scheduleUnclaimedKeyCheck() {
|
|
148
|
-
if (unclaimedKeyCheckScheduled) return;
|
|
149
|
-
unclaimedKeyCheckScheduled = true;
|
|
150
|
-
setTimeout(() => {
|
|
151
|
-
unclaimedKeyCheckScheduled = false;
|
|
152
|
-
if (ssrInitialKeys && ssrInitialKeys.size > 0) {
|
|
153
|
-
console.warn(
|
|
154
|
-
`[hadars] useServerData: ${ssrInitialKeys.size} server-resolved key(s) were never claimed during client hydration: ${[...ssrInitialKeys].map((k) => JSON.stringify(k)).join(", ")}. This usually means the key passed to useServerData was different on the server than on the client (e.g. it contains Date.now(), Math.random(), or another value that changes between renders). Keys must be stable and deterministic.`
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
ssrInitialKeys = null;
|
|
158
|
-
}, 0);
|
|
159
|
-
}
|
|
145
|
+
var _navValues = [];
|
|
146
|
+
var _navIdx = 0;
|
|
160
147
|
function initServerDataCache(data) {
|
|
161
148
|
clientServerDataCache.clear();
|
|
162
|
-
|
|
149
|
+
_navValues = [];
|
|
150
|
+
_navIdx = 0;
|
|
163
151
|
for (const [k, v] of Object.entries(data)) {
|
|
164
152
|
clientServerDataCache.set(k, v);
|
|
165
|
-
ssrInitialKeys.add(k);
|
|
166
153
|
}
|
|
167
154
|
}
|
|
168
|
-
function useServerData(
|
|
169
|
-
const cacheKey =
|
|
155
|
+
function useServerData(fn, options) {
|
|
156
|
+
const cacheKey = React.useId();
|
|
157
|
+
const evictTimerRef = React.useRef(null);
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
if (options?.cache !== false) return;
|
|
160
|
+
if (evictTimerRef.current !== null) {
|
|
161
|
+
clearTimeout(evictTimerRef.current);
|
|
162
|
+
evictTimerRef.current = null;
|
|
163
|
+
}
|
|
164
|
+
return () => {
|
|
165
|
+
evictTimerRef.current = setTimeout(() => {
|
|
166
|
+
clientServerDataCache.delete(cacheKey);
|
|
167
|
+
evictTimerRef.current = null;
|
|
168
|
+
}, 0);
|
|
169
|
+
};
|
|
170
|
+
}, []);
|
|
170
171
|
if (typeof window !== "undefined") {
|
|
171
172
|
if (clientServerDataCache.has(cacheKey)) {
|
|
172
|
-
ssrInitialKeys?.delete(cacheKey);
|
|
173
173
|
return clientServerDataCache.get(cacheKey);
|
|
174
174
|
}
|
|
175
|
-
if (ssrInitialKeys !== null && ssrInitialKeys.size > 0) {
|
|
176
|
-
scheduleUnclaimedKeyCheck();
|
|
177
|
-
}
|
|
178
175
|
const pathKey = window.location.pathname + window.location.search;
|
|
179
176
|
if (fetchedPaths.has(pathKey)) {
|
|
180
|
-
|
|
177
|
+
if (_navIdx < _navValues.length) {
|
|
178
|
+
const value = _navValues[_navIdx++];
|
|
179
|
+
clientServerDataCache.set(cacheKey, value);
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
fetchedPaths.delete(pathKey);
|
|
183
|
+
_navValues = [];
|
|
184
|
+
_navIdx = 0;
|
|
181
185
|
}
|
|
182
186
|
if (!pendingDataFetch.has(pathKey)) {
|
|
183
187
|
let resolve;
|
|
@@ -196,9 +200,9 @@ function useServerData(key, fn) {
|
|
|
196
200
|
const res = await fetch(pathKey, { headers: { "Accept": "application/json" } });
|
|
197
201
|
if (res.ok) json = await res.json();
|
|
198
202
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
_navValues = Object.values(json?.serverData ?? {});
|
|
204
|
+
_navIdx = 0;
|
|
205
|
+
fetchedPaths.clear();
|
|
202
206
|
} finally {
|
|
203
207
|
fetchedPaths.add(pathKey);
|
|
204
208
|
pendingDataFetch.delete(pathKey);
|
|
@@ -210,12 +214,7 @@ function useServerData(key, fn) {
|
|
|
210
214
|
}
|
|
211
215
|
const unsuspend = globalThis.__hadarsUnsuspend;
|
|
212
216
|
if (!unsuspend) return void 0;
|
|
213
|
-
const _u = unsuspend;
|
|
214
|
-
if (!_u.pendingCreated) _u.pendingCreated = 0;
|
|
215
217
|
const existing = unsuspend.cache.get(cacheKey);
|
|
216
|
-
if (existing?.status === "fulfilled" && _u.lastPendingKey === cacheKey) {
|
|
217
|
-
_u.lastPendingKeyAccessed = true;
|
|
218
|
-
}
|
|
219
218
|
if (!existing) {
|
|
220
219
|
const result = fn();
|
|
221
220
|
const isThenable = result !== null && typeof result === "object" && typeof result.then === "function";
|
|
@@ -224,22 +223,6 @@ function useServerData(key, fn) {
|
|
|
224
223
|
unsuspend.cache.set(cacheKey, { status: "fulfilled", value });
|
|
225
224
|
return value;
|
|
226
225
|
}
|
|
227
|
-
if (_u.lastPendingKey != null && !_u.lastPendingKeyAccessed) {
|
|
228
|
-
const prev = unsuspend.cache.get(_u.lastPendingKey);
|
|
229
|
-
if (prev?.status === "fulfilled") {
|
|
230
|
-
throw new Error(
|
|
231
|
-
`[hadars] useServerData: key ${JSON.stringify(cacheKey)} is not stable between render passes. The previous pass resolved ${JSON.stringify(_u.lastPendingKey)} but it was not requested in this pass \u2014 the key is changing between renders. Avoid dynamic values in keys (e.g. Date.now() or Math.random()); use stable, deterministic identifiers instead.`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
_u.pendingCreated++;
|
|
236
|
-
if (_u.pendingCreated > 100) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`[hadars] useServerData: more than 100 async keys created in a single render. This usually means a key is not stable between renders (e.g. it contains Date.now() or Math.random()). Currently offending key: ${JSON.stringify(cacheKey)}.`
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
_u.lastPendingKey = cacheKey;
|
|
242
|
-
_u.lastPendingKeyAccessed = false;
|
|
243
226
|
const promise = result.then(
|
|
244
227
|
(value) => {
|
|
245
228
|
unsuspend.cache.set(cacheKey, { status: "fulfilled", value });
|
|
@@ -257,18 +240,8 @@ function useServerData(key, fn) {
|
|
|
257
240
|
if (existing.status === "rejected") throw existing.reason;
|
|
258
241
|
return existing.value;
|
|
259
242
|
}
|
|
260
|
-
function toCacheKey(doc) {
|
|
261
|
-
if (typeof doc === "string") return doc.trim();
|
|
262
|
-
for (const def of doc?.definitions ?? []) {
|
|
263
|
-
if (def.kind === "OperationDefinition" && def.name?.value) {
|
|
264
|
-
return `op:${def.name.value}`;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return JSON.stringify(doc?.definitions ?? doc);
|
|
268
|
-
}
|
|
269
243
|
function useGraphQL(query, variables) {
|
|
270
|
-
|
|
271
|
-
return useServerData(key, async () => {
|
|
244
|
+
return useServerData(async () => {
|
|
272
245
|
const executor = globalThis.__hadarsGraphQL;
|
|
273
246
|
if (!executor) {
|
|
274
247
|
throw new Error(
|
package/dist/lambda.cjs
CHANGED
|
@@ -1510,6 +1510,8 @@ function createLambdaHandler(options, bundled) {
|
|
|
1510
1510
|
});
|
|
1511
1511
|
} catch (err) {
|
|
1512
1512
|
console.error("[hadars] SSR render error:", err);
|
|
1513
|
+
options.onError?.(err, request)?.catch?.(() => {
|
|
1514
|
+
});
|
|
1513
1515
|
return new Response("Internal Server Error", { status: 500 });
|
|
1514
1516
|
}
|
|
1515
1517
|
};
|
package/dist/lambda.d.cts
CHANGED
package/dist/lambda.d.ts
CHANGED
package/dist/lambda.js
CHANGED
|
@@ -178,6 +178,8 @@ function createLambdaHandler(options, bundled) {
|
|
|
178
178
|
});
|
|
179
179
|
} catch (err) {
|
|
180
180
|
console.error("[hadars] SSR render error:", err);
|
|
181
|
+
options.onError?.(err, request)?.catch?.(() => {
|
|
182
|
+
});
|
|
181
183
|
return new Response("Internal Server Error", { status: 500 });
|
|
182
184
|
}
|
|
183
185
|
};
|
package/dist/loader.cjs
CHANGED
|
@@ -55,8 +55,8 @@ function swcTransform(swc, source, isServer, resourcePath) {
|
|
|
55
55
|
const name = callee.value;
|
|
56
56
|
if (!isServer && name === "useServerData") {
|
|
57
57
|
const args2 = node.arguments;
|
|
58
|
-
if (!args2 || args2.length <
|
|
59
|
-
const fnArg = args2[
|
|
58
|
+
if (!args2 || args2.length < 1) return;
|
|
59
|
+
const fnArg = args2[0].expression ?? args2[0];
|
|
60
60
|
replacements.push({
|
|
61
61
|
start: fnArg.span.start - fileOffset,
|
|
62
62
|
end: fnArg.span.end - fileOffset,
|
|
@@ -187,13 +187,8 @@ function stripUseServerDataFns(source) {
|
|
|
187
187
|
let match;
|
|
188
188
|
CALL_RE.lastIndex = 0;
|
|
189
189
|
while ((match = CALL_RE.exec(source)) !== null) {
|
|
190
|
-
const callStart = match.index;
|
|
191
190
|
let i = match.index + match[0].length;
|
|
192
191
|
while (i < source.length && /\s/.test(source[i])) i++;
|
|
193
|
-
i = scanExpressionEnd(source, i);
|
|
194
|
-
if (i >= source.length || source[i] !== ",") continue;
|
|
195
|
-
i++;
|
|
196
|
-
while (i < source.length && /\s/.test(source[i])) i++;
|
|
197
192
|
const fnStart = i;
|
|
198
193
|
const fnEnd = scanExpressionEnd(source, i);
|
|
199
194
|
if (fnEnd <= fnStart) continue;
|