@c9up/aurora 0.1.16 → 0.1.17
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/browser.d.ts +50 -2
- package/dist/browser.js +90 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/rpc.d.ts +6 -45
- package/dist/rpc.js +19 -87
- package/dist/server/renderPage.d.ts +12 -0
- package/dist/server/renderPage.js +23 -0
- package/package.json +10 -1
- package/src/browser.ts +119 -3
- package/src/index.ts +7 -9
- package/src/rpc.ts +30 -125
- package/src/server/renderPage.ts +48 -0
package/dist/browser.d.ts
CHANGED
|
@@ -131,16 +131,64 @@ export interface CookieOptions {
|
|
|
131
131
|
/** Restrict to HTTPS. */
|
|
132
132
|
secure?: boolean;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Install the SSR cookie seed (`name → value`). Called by `renderPage` from its
|
|
136
|
+
* `cookies` allowlist; the hydrate bootstrap does NOT call it (the browser reads
|
|
137
|
+
* `document.cookie`). Replaces any previous seed.
|
|
138
|
+
*/
|
|
139
|
+
export declare function setCookieStore(values: Record<string, string>): void;
|
|
140
|
+
/** The currently-installed SSR cookie seed (mainly for tests/introspection). */
|
|
141
|
+
export declare function getCookieStore(): Record<string, string>;
|
|
134
142
|
/**
|
|
135
143
|
* SSR-safe cookie accessor — the one store {@link WebStorage} can't cover, for
|
|
136
|
-
* values the server also reads.
|
|
137
|
-
*
|
|
144
|
+
* values the server also reads. In the browser reads/writes hit
|
|
145
|
+
* `document.cookie`; during SSR reads come from the {@link setCookieStore} seed
|
|
146
|
+
* and writes are a no-op. Names and values are URL-encoded.
|
|
138
147
|
*/
|
|
139
148
|
export declare const cookie: {
|
|
140
149
|
get(name: string): string | null;
|
|
141
150
|
set(name: string, value: string, options?: CookieOptions): void;
|
|
142
151
|
remove(name: string, options?: Pick<CookieOptions, "path" | "domain">): void;
|
|
143
152
|
};
|
|
153
|
+
/**
|
|
154
|
+
* Maps a typed value to and from the string a cookie stores. Pass one to
|
|
155
|
+
* {@link cookieState} for non-string state (booleans, enums, JSON).
|
|
156
|
+
*/
|
|
157
|
+
export interface CookieCodec<T> {
|
|
158
|
+
/** Parse the raw cookie string into `T`. */
|
|
159
|
+
parse(raw: string): T;
|
|
160
|
+
/** Serialize `T` into the raw cookie string. */
|
|
161
|
+
serialize(value: T): string;
|
|
162
|
+
}
|
|
163
|
+
/** Codec for a boolean cookie, stored as `"1"` / `"0"`. */
|
|
164
|
+
export declare const booleanCookie: CookieCodec<boolean>;
|
|
165
|
+
/**
|
|
166
|
+
* Codec for a JSON-serializable value. A parse failure (malformed cookie) is
|
|
167
|
+
* surfaced by returning `fallback`, so a tampered cookie never throws mid-render.
|
|
168
|
+
*/
|
|
169
|
+
export declare function jsonCookie<T>(fallback: T): CookieCodec<T>;
|
|
170
|
+
/**
|
|
171
|
+
* A {@link Signal} backed by a cookie — the isomorphic counterpart to
|
|
172
|
+
* {@link persistedSignal}. It is seeded from the cookie (the
|
|
173
|
+
* {@link setCookieStore} request seed during SSR, `document.cookie` in the
|
|
174
|
+
* browser) and persists every change back to the cookie via a reactive
|
|
175
|
+
* `effect`. Because the same seed is visible on both sides, a page renders the
|
|
176
|
+
* SAME markup server- and client-side — no hydration mismatch, no flash of the
|
|
177
|
+
* default state (e.g. a sidebar that animates open→collapsed on every load).
|
|
178
|
+
*
|
|
179
|
+
* Use {@link cookieSignal} for string state; pass a {@link CookieCodec} here for
|
|
180
|
+
* booleans/enums/JSON (e.g. {@link booleanCookie}). Created inside a component's
|
|
181
|
+
* setup, the persistence effect is disposed with it.
|
|
182
|
+
*
|
|
183
|
+
* Read it at the TOP of a page (before any `await`) so the SSR seed is current —
|
|
184
|
+
* see {@link setCookieStore}.
|
|
185
|
+
*/
|
|
186
|
+
export declare function cookieState<T>(name: string, initial: T, codec: CookieCodec<T>, options?: CookieOptions): Signal<T>;
|
|
187
|
+
/**
|
|
188
|
+
* A {@link Signal} backed by a string cookie — {@link cookieState} with an
|
|
189
|
+
* identity codec, for the common case where the value is already a string.
|
|
190
|
+
*/
|
|
191
|
+
export declare function cookieSignal(name: string, initial: string, options?: CookieOptions): Signal<string>;
|
|
144
192
|
/** Async clipboard access. Methods return `false`/`null` when unavailable. */
|
|
145
193
|
export declare const clipboard: {
|
|
146
194
|
/** Copy `text`. Returns whether it succeeded. */
|
package/dist/browser.js
CHANGED
|
@@ -289,15 +289,43 @@ export function queryParam(key) {
|
|
|
289
289
|
}
|
|
290
290
|
return sig;
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* SSR seed of the request's cookies — a `name → value` map installed by
|
|
294
|
+
* `renderPage` (server-side) before a page renders, so {@link cookie.get} and
|
|
295
|
+
* {@link cookieSignal} can read the SAME values during SSR that the browser
|
|
296
|
+
* will read from `document.cookie` after hydration. Without it the server has
|
|
297
|
+
* no view of the request cookies and renders default UI state → a flash /
|
|
298
|
+
* mismatch on hydration (the classic collapsed-sidebar flicker).
|
|
299
|
+
*
|
|
300
|
+
* Module-global by necessity (the page factory reads it ambiently). It is set
|
|
301
|
+
* synchronously immediately before the synchronous render, so read your cookie
|
|
302
|
+
* signals at the TOP of a page (before any `await`) to avoid a cross-request
|
|
303
|
+
* race under concurrent async page factories. In the browser it is unused —
|
|
304
|
+
* {@link cookie.get} reads `document.cookie` directly there.
|
|
305
|
+
*/
|
|
306
|
+
let cookieSeed = {};
|
|
307
|
+
/**
|
|
308
|
+
* Install the SSR cookie seed (`name → value`). Called by `renderPage` from its
|
|
309
|
+
* `cookies` allowlist; the hydrate bootstrap does NOT call it (the browser reads
|
|
310
|
+
* `document.cookie`). Replaces any previous seed.
|
|
311
|
+
*/
|
|
312
|
+
export function setCookieStore(values) {
|
|
313
|
+
cookieSeed = { ...values };
|
|
314
|
+
}
|
|
315
|
+
/** The currently-installed SSR cookie seed (mainly for tests/introspection). */
|
|
316
|
+
export function getCookieStore() {
|
|
317
|
+
return { ...cookieSeed };
|
|
318
|
+
}
|
|
292
319
|
/**
|
|
293
320
|
* SSR-safe cookie accessor — the one store {@link WebStorage} can't cover, for
|
|
294
|
-
* values the server also reads.
|
|
295
|
-
*
|
|
321
|
+
* values the server also reads. In the browser reads/writes hit
|
|
322
|
+
* `document.cookie`; during SSR reads come from the {@link setCookieStore} seed
|
|
323
|
+
* and writes are a no-op. Names and values are URL-encoded.
|
|
296
324
|
*/
|
|
297
325
|
export const cookie = {
|
|
298
326
|
get(name) {
|
|
299
327
|
if (typeof document === "undefined")
|
|
300
|
-
return null;
|
|
328
|
+
return cookieSeed[name] ?? null;
|
|
301
329
|
const prefix = `${encodeURIComponent(name)}=`;
|
|
302
330
|
for (const part of document.cookie.split("; ")) {
|
|
303
331
|
if (part.startsWith(prefix)) {
|
|
@@ -330,6 +358,65 @@ export const cookie = {
|
|
|
330
358
|
this.set(name, "", { ...options, maxAge: 0, expires: new Date(0) });
|
|
331
359
|
},
|
|
332
360
|
};
|
|
361
|
+
/** Codec for a boolean cookie, stored as `"1"` / `"0"`. */
|
|
362
|
+
export const booleanCookie = {
|
|
363
|
+
parse: (raw) => raw === "1" || raw === "true",
|
|
364
|
+
serialize: (value) => (value ? "1" : "0"),
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Codec for a JSON-serializable value. A parse failure (malformed cookie) is
|
|
368
|
+
* surfaced by returning `fallback`, so a tampered cookie never throws mid-render.
|
|
369
|
+
*/
|
|
370
|
+
export function jsonCookie(fallback) {
|
|
371
|
+
return {
|
|
372
|
+
parse: (raw) => {
|
|
373
|
+
try {
|
|
374
|
+
return JSON.parse(raw);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return fallback;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
serialize: (value) => JSON.stringify(value),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* A {@link Signal} backed by a cookie — the isomorphic counterpart to
|
|
385
|
+
* {@link persistedSignal}. It is seeded from the cookie (the
|
|
386
|
+
* {@link setCookieStore} request seed during SSR, `document.cookie` in the
|
|
387
|
+
* browser) and persists every change back to the cookie via a reactive
|
|
388
|
+
* `effect`. Because the same seed is visible on both sides, a page renders the
|
|
389
|
+
* SAME markup server- and client-side — no hydration mismatch, no flash of the
|
|
390
|
+
* default state (e.g. a sidebar that animates open→collapsed on every load).
|
|
391
|
+
*
|
|
392
|
+
* Use {@link cookieSignal} for string state; pass a {@link CookieCodec} here for
|
|
393
|
+
* booleans/enums/JSON (e.g. {@link booleanCookie}). Created inside a component's
|
|
394
|
+
* setup, the persistence effect is disposed with it.
|
|
395
|
+
*
|
|
396
|
+
* Read it at the TOP of a page (before any `await`) so the SSR seed is current —
|
|
397
|
+
* see {@link setCookieStore}.
|
|
398
|
+
*/
|
|
399
|
+
export function cookieState(name, initial, codec, options = {}) {
|
|
400
|
+
const raw = cookie.get(name);
|
|
401
|
+
const sig = signal(raw === null ? initial : codec.parse(raw));
|
|
402
|
+
// Mirror every change back to the cookie. Runs once immediately (a no-op
|
|
403
|
+
// write during SSR, where `cookie.set` bails) then on each change.
|
|
404
|
+
effect(() => {
|
|
405
|
+
cookie.set(name, codec.serialize(sig()), options);
|
|
406
|
+
});
|
|
407
|
+
return sig;
|
|
408
|
+
}
|
|
409
|
+
const stringCookie = {
|
|
410
|
+
parse: (raw) => raw,
|
|
411
|
+
serialize: (value) => value,
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* A {@link Signal} backed by a string cookie — {@link cookieState} with an
|
|
415
|
+
* identity codec, for the common case where the value is already a string.
|
|
416
|
+
*/
|
|
417
|
+
export function cookieSignal(name, initial, options = {}) {
|
|
418
|
+
return cookieState(name, initial, stringCookie, options);
|
|
419
|
+
}
|
|
333
420
|
// ─── Clipboard & Web Share ───────────────────────────────────────────
|
|
334
421
|
/** Async clipboard access. Methods return `false`/`null` when unavailable. */
|
|
335
422
|
export const clipboard = {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { CookieOptions, PersistedSignalOptions, ShareData, StorageArea, WebStorageOptions, WindowSize, } from "./browser.js";
|
|
2
|
-
export { back, clipboard, cookie, forward, hash, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
1
|
+
export type { CookieCodec, CookieOptions, PersistedSignalOptions, ShareData, StorageArea, WebStorageOptions, WindowSize, } from "./browser.js";
|
|
2
|
+
export { back, booleanCookie, clipboard, cookie, cookieSignal, cookieState, forward, getCookieStore, hash, jsonCookie, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, setCookieStore, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
3
3
|
export { type ClassValue, clsx, cn, twMerge } from "./cn.js";
|
|
4
4
|
export type { Command } from "./command.js";
|
|
5
5
|
export { command } from "./command.js";
|
|
@@ -19,7 +19,6 @@ export { DEFAULT_LIVE_EVENT_PATH, type LiveHttpContext, type LiveHttpRouter, typ
|
|
|
19
19
|
export { batch, effect, isSignal, memo, onCleanup, type ReadSignal, type Signal, signal, untrack, } from "./reactive.js";
|
|
20
20
|
export { type Disposer, render } from "./render.js";
|
|
21
21
|
export { type AuroraHttpContext, type AuroraResponse, type AuroraRouteConfig, auroraRoute, } from "./route.js";
|
|
22
|
-
export { createRpcClient, isRpcError, type RpcCall, type RpcClient, type RpcClientOptions, RpcError, type RpcResult, } from "./rpc.js";
|
|
23
22
|
export { renderToString } from "./ssr.js";
|
|
24
23
|
export type { TemplateResult } from "./types.js";
|
|
25
24
|
export { getRouteManifest, setRouteManifest, urlFor } from "./url.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { back, clipboard, cookie, forward, hash, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
1
|
+
export { back, booleanCookie, clipboard, cookie, cookieSignal, cookieState, forward, getCookieStore, hash, jsonCookie, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, setCookieStore, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
2
2
|
export { clsx, cn, twMerge } from "./cn.js";
|
|
3
3
|
export { command } from "./command.js";
|
|
4
4
|
export { component, onMount, onUnmount } from "./component.js";
|
|
@@ -15,6 +15,5 @@ export { DEFAULT_LIVE_EVENT_PATH, wireLiveEvents, } from "./liveServer.js";
|
|
|
15
15
|
export { batch, effect, isSignal, memo, onCleanup, signal, untrack, } from "./reactive.js";
|
|
16
16
|
export { render } from "./render.js";
|
|
17
17
|
export { auroraRoute, } from "./route.js";
|
|
18
|
-
export { createRpcClient, isRpcError, RpcError, } from "./rpc.js";
|
|
19
18
|
export { renderToString } from "./ssr.js";
|
|
20
19
|
export { getRouteManifest, setRouteManifest, urlFor } from "./url.js";
|
package/dist/rpc.d.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint. `@c9up/ream`'s
|
|
3
|
-
* RpcProvider mounts `POST /rpc` and speaks JSON-RPC 2.0 (single + batch); this
|
|
4
|
-
* client builds on aurora's {@link HttpClient}, inheriting its base URL, auth
|
|
5
|
-
* headers, and timeouts.
|
|
6
|
-
*
|
|
7
|
-
* const rpc = createRpcClient() // POST /rpc, same-origin
|
|
8
|
-
* const result = await rpc.call('task.validate', { id }) // typed via call<T>()
|
|
9
|
-
* const user = await rpc.call('user.find', { id }, isUser) // validated, cast-free
|
|
10
|
-
*
|
|
11
|
-
* Pairs with aurora's `command()` for reactive calls:
|
|
12
|
-
* const validate = command((p) => rpc.call('task.validate', p))
|
|
13
|
-
*/
|
|
14
1
|
import { HttpClient } from "./http.js";
|
|
2
|
+
export { isRpcError, type RpcCall, type RpcCallOptions, type RpcClient, RpcError, type RpcResult, } from "@c9up/comet";
|
|
3
|
+
import type { RpcClient } from "@c9up/comet";
|
|
15
4
|
export interface RpcClientOptions {
|
|
16
5
|
/** Endpoint path. Default `/rpc` (matches RpcProvider's default). */
|
|
17
6
|
url?: string;
|
|
@@ -20,36 +9,8 @@ export interface RpcClientOptions {
|
|
|
20
9
|
/** Default headers — only used when no `http` client is supplied. */
|
|
21
10
|
headers?: Record<string, string>;
|
|
22
11
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
constructor(code: number, message: string, data?: unknown);
|
|
28
|
-
}
|
|
29
|
-
/** Type guard for {@link RpcError}. */
|
|
30
|
-
export declare function isRpcError(value: unknown): value is RpcError;
|
|
31
|
-
/** One call in a batch. `parse` optionally validates that call's result (cast-free). */
|
|
32
|
-
export interface RpcCall<T = unknown> {
|
|
33
|
-
method: string;
|
|
34
|
-
params?: unknown;
|
|
35
|
-
parse?: (data: unknown) => T;
|
|
36
|
-
}
|
|
37
|
-
/** A settled batch entry — the result, or the JSON-RPC error for that call. */
|
|
38
|
-
export type RpcResult<T = unknown> = {
|
|
39
|
-
ok: true;
|
|
40
|
-
value: T;
|
|
41
|
-
} | {
|
|
42
|
-
ok: false;
|
|
43
|
-
error: RpcError;
|
|
44
|
-
};
|
|
45
|
-
export interface RpcClient {
|
|
46
|
-
/**
|
|
47
|
-
* Call one method. Returns the result, or throws {@link RpcError} on a
|
|
48
|
-
* JSON-RPC error. Pass `parse` to validate the result at runtime (and skip
|
|
49
|
-
* the unchecked `T` assertion).
|
|
50
|
-
*/
|
|
51
|
-
call<T = unknown>(method: string, params?: unknown, parse?: (data: unknown) => T): Promise<T>;
|
|
52
|
-
/** Send a JSON-RPC batch. Returns one settled entry per call, in request order. */
|
|
53
|
-
batch(calls: RpcCall[]): Promise<RpcResult[]>;
|
|
54
|
-
}
|
|
12
|
+
/**
|
|
13
|
+
* Create a JSON-RPC client bound to aurora's HttpClient transport. Inherits the
|
|
14
|
+
* supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts.
|
|
15
|
+
*/
|
|
55
16
|
export declare function createRpcClient(options?: RpcClientOptions): RpcClient;
|
package/dist/rpc.js
CHANGED
|
@@ -1,97 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint — aurora's thin binding
|
|
3
|
+
* over the agnostic {@link https://github.com/C9up/comet | @c9up/comet} client.
|
|
4
|
+
* It wires aurora's {@link HttpClient} (base URL, auth headers, timeouts) as
|
|
5
|
+
* comet's transport, and re-exports the protocol surface so call sites keep
|
|
6
|
+
* importing everything from `@c9up/aurora`.
|
|
6
7
|
*
|
|
7
|
-
* const rpc = createRpcClient()
|
|
8
|
-
* const result = await rpc.call('task.validate', { id })
|
|
9
|
-
* const user = await rpc.call('user.find', { id }, isUser) // validated, cast-free
|
|
8
|
+
* const rpc = createRpcClient() // POST /rpc, same-origin
|
|
9
|
+
* const result = await rpc.call('task.validate', { id }) // typed via call<T>()
|
|
10
|
+
* const user = await rpc.call('user.find', { id }, { parse: isUser }) // validated, cast-free
|
|
11
|
+
* await rpc.call('slow.op', p, { signal: ac.signal }) // abortable
|
|
10
12
|
*
|
|
11
13
|
* Pairs with aurora's `command()` for reactive calls:
|
|
12
14
|
* const validate = command((p) => rpc.call('task.validate', p))
|
|
13
15
|
*/
|
|
16
|
+
import { createRpcClient as createCometRpcClient } from "@c9up/comet";
|
|
14
17
|
import { HttpClient } from "./http.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
super(message);
|
|
21
|
-
this.name = "RpcError";
|
|
22
|
-
this.code = code;
|
|
23
|
-
this.data = data;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/** Type guard for {@link RpcError}. */
|
|
27
|
-
export function isRpcError(value) {
|
|
28
|
-
return value instanceof RpcError;
|
|
29
|
-
}
|
|
30
|
-
function isObject(value) {
|
|
31
|
-
return typeof value === "object" && value !== null;
|
|
32
|
-
}
|
|
33
|
-
/** Turn a JSON-RPC `error` member into an {@link RpcError}. */
|
|
34
|
-
function toRpcError(error) {
|
|
35
|
-
if (isObject(error) &&
|
|
36
|
-
typeof error.code === "number" &&
|
|
37
|
-
typeof error.message === "string") {
|
|
38
|
-
return new RpcError(error.code, error.message, error.data);
|
|
39
|
-
}
|
|
40
|
-
return new RpcError(-32603, "Malformed JSON-RPC error envelope", error);
|
|
41
|
-
}
|
|
18
|
+
export { isRpcError, RpcError, } from "@c9up/comet";
|
|
19
|
+
/**
|
|
20
|
+
* Create a JSON-RPC client bound to aurora's HttpClient transport. Inherits the
|
|
21
|
+
* supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts.
|
|
22
|
+
*/
|
|
42
23
|
export function createRpcClient(options = {}) {
|
|
43
24
|
const http = options.http ?? new HttpClient({ headers: options.headers });
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const id = ++nextId;
|
|
49
|
-
const res = await http.post(url, {
|
|
50
|
-
jsonrpc: "2.0",
|
|
51
|
-
method,
|
|
52
|
-
params,
|
|
53
|
-
id,
|
|
54
|
-
});
|
|
55
|
-
if (!isObject(res)) {
|
|
56
|
-
throw new RpcError(-32603, `Malformed JSON-RPC response for "${method}"`);
|
|
57
|
-
}
|
|
58
|
-
if (res.error !== undefined)
|
|
59
|
-
throw toRpcError(res.error);
|
|
60
|
-
// Result boundary — the same unchecked `T` assertion HttpClient uses,
|
|
61
|
-
// with `parse` as the cast-free, runtime-validated escape hatch.
|
|
62
|
-
return parse ? parse(res.result) : res.result;
|
|
63
|
-
},
|
|
64
|
-
async batch(calls) {
|
|
65
|
-
if (calls.length === 0)
|
|
66
|
-
return [];
|
|
67
|
-
const requests = calls.map((c, index) => ({
|
|
68
|
-
jsonrpc: "2.0",
|
|
69
|
-
method: c.method,
|
|
70
|
-
params: c.params,
|
|
71
|
-
id: index, // index = request position; responses are matched back by id
|
|
72
|
-
}));
|
|
73
|
-
const res = await http.post(url, requests);
|
|
74
|
-
if (!Array.isArray(res)) {
|
|
75
|
-
throw new RpcError(-32603, "Malformed JSON-RPC batch response");
|
|
76
|
-
}
|
|
77
|
-
const byId = new Map();
|
|
78
|
-
for (const item of res)
|
|
79
|
-
if (isObject(item))
|
|
80
|
-
byId.set(item.id, item);
|
|
81
|
-
return calls.map((c, index) => {
|
|
82
|
-
const envelope = byId.get(index);
|
|
83
|
-
if (!envelope) {
|
|
84
|
-
return {
|
|
85
|
-
ok: false,
|
|
86
|
-
error: new RpcError(-32603, `No response for "${c.method}"`),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
if (envelope.error !== undefined) {
|
|
90
|
-
return { ok: false, error: toRpcError(envelope.error) };
|
|
91
|
-
}
|
|
92
|
-
const value = c.parse ? c.parse(envelope.result) : envelope.result;
|
|
93
|
-
return { ok: true, value };
|
|
94
|
-
});
|
|
95
|
-
},
|
|
96
|
-
};
|
|
25
|
+
return createCometRpcClient({
|
|
26
|
+
url: options.url,
|
|
27
|
+
transport: (url, body, { signal }) => http.post(url, body, { signal }),
|
|
28
|
+
});
|
|
97
29
|
}
|
|
@@ -66,5 +66,17 @@ export interface RenderPageOptions {
|
|
|
66
66
|
* in SSR and the browser. Omit if the app doesn't use `urlFor`.
|
|
67
67
|
*/
|
|
68
68
|
routes?: Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* Allowlist of cookie names to seed into the SSR cookie store so a page can
|
|
71
|
+
* read them during render via aurora's `cookieSignal` / `cookie.get` — the
|
|
72
|
+
* server then renders the SAME UI state the browser will (no hydration flash
|
|
73
|
+
* of the default, e.g. a sidebar animating open→collapsed on every load).
|
|
74
|
+
*
|
|
75
|
+
* Only the NAMED cookies are read (from `ctx.request`); they are NOT
|
|
76
|
+
* serialized into the page — the browser reads them from `document.cookie`.
|
|
77
|
+
* NEVER list a session / signed / encrypted / `httpOnly` cookie here: those
|
|
78
|
+
* must stay server-only. Use plain, JS-readable cookies for UI state.
|
|
79
|
+
*/
|
|
80
|
+
cookies?: string[];
|
|
69
81
|
}
|
|
70
82
|
export declare function renderPage<P>(ctx: RenderHttpContext, pages: Pages, name: string, props: P, options?: RenderPageOptions): Promise<void>;
|
|
@@ -22,13 +22,36 @@
|
|
|
22
22
|
* </script>
|
|
23
23
|
* </body>
|
|
24
24
|
*/
|
|
25
|
+
import { setCookieStore } from "../browser.js";
|
|
25
26
|
import { renderToString } from "../ssr.js";
|
|
26
27
|
import { setRouteManifest } from "../url.js";
|
|
28
|
+
function isCookieReadable(request) {
|
|
29
|
+
return (typeof request === "object" &&
|
|
30
|
+
request !== null &&
|
|
31
|
+
"cookie" in request &&
|
|
32
|
+
typeof request.cookie === "function");
|
|
33
|
+
}
|
|
34
|
+
/** Read the allowlisted cookies off the request into a `name → value` seed. */
|
|
35
|
+
function readRequestCookies(request, names) {
|
|
36
|
+
if (!isCookieReadable(request))
|
|
37
|
+
return {};
|
|
38
|
+
const seed = {};
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const value = request.cookie(name);
|
|
41
|
+
if (value !== null)
|
|
42
|
+
seed[name] = value;
|
|
43
|
+
}
|
|
44
|
+
return seed;
|
|
45
|
+
}
|
|
27
46
|
export async function renderPage(ctx, pages, name, props, options = {}) {
|
|
28
47
|
// Install the route manifest BEFORE rendering so a page calling `urlFor`
|
|
29
48
|
// during SSR resolves against the same map the client will get.
|
|
30
49
|
if (options.routes)
|
|
31
50
|
setRouteManifest(options.routes);
|
|
51
|
+
// Seed the request's UI cookies so the page reads the SAME state server-side
|
|
52
|
+
// that the browser will after hydration. Set synchronously right before the
|
|
53
|
+
// (synchronous) render — read cookie signals at the top of the page.
|
|
54
|
+
setCookieStore(options.cookies ? readRequestCookies(ctx.request, options.cookies) : {});
|
|
32
55
|
const factory = await pages.resolve(name);
|
|
33
56
|
// The factory must be invoked the SAME way client-side for hydrate
|
|
34
57
|
// to find matching slots — `Page(props)` is the contract.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,17 +34,26 @@
|
|
|
34
34
|
"./server": {
|
|
35
35
|
"types": "./dist/server.d.ts",
|
|
36
36
|
"import": "./dist/server.js"
|
|
37
|
+
},
|
|
38
|
+
"./rpc": {
|
|
39
|
+
"types": "./dist/rpc.d.ts",
|
|
40
|
+
"import": "./dist/rpc.js"
|
|
37
41
|
}
|
|
38
42
|
},
|
|
39
43
|
"peerDependencies": {
|
|
44
|
+
"@c9up/comet": "^0.1.0",
|
|
40
45
|
"@c9up/ream": "^0.1.0"
|
|
41
46
|
},
|
|
42
47
|
"peerDependenciesMeta": {
|
|
48
|
+
"@c9up/comet": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
43
51
|
"@c9up/ream": {
|
|
44
52
|
"optional": true
|
|
45
53
|
}
|
|
46
54
|
},
|
|
47
55
|
"devDependencies": {
|
|
56
|
+
"@c9up/comet": "^0.1.0",
|
|
48
57
|
"@types/node": "^22.19.15",
|
|
49
58
|
"@vitest/browser": "4.1.6",
|
|
50
59
|
"@vitest/browser-playwright": "^4.1.9",
|
package/src/browser.ts
CHANGED
|
@@ -384,14 +384,45 @@ export interface CookieOptions {
|
|
|
384
384
|
secure?: boolean;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* SSR seed of the request's cookies — a `name → value` map installed by
|
|
389
|
+
* `renderPage` (server-side) before a page renders, so {@link cookie.get} and
|
|
390
|
+
* {@link cookieSignal} can read the SAME values during SSR that the browser
|
|
391
|
+
* will read from `document.cookie` after hydration. Without it the server has
|
|
392
|
+
* no view of the request cookies and renders default UI state → a flash /
|
|
393
|
+
* mismatch on hydration (the classic collapsed-sidebar flicker).
|
|
394
|
+
*
|
|
395
|
+
* Module-global by necessity (the page factory reads it ambiently). It is set
|
|
396
|
+
* synchronously immediately before the synchronous render, so read your cookie
|
|
397
|
+
* signals at the TOP of a page (before any `await`) to avoid a cross-request
|
|
398
|
+
* race under concurrent async page factories. In the browser it is unused —
|
|
399
|
+
* {@link cookie.get} reads `document.cookie` directly there.
|
|
400
|
+
*/
|
|
401
|
+
let cookieSeed: Record<string, string> = {};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Install the SSR cookie seed (`name → value`). Called by `renderPage` from its
|
|
405
|
+
* `cookies` allowlist; the hydrate bootstrap does NOT call it (the browser reads
|
|
406
|
+
* `document.cookie`). Replaces any previous seed.
|
|
407
|
+
*/
|
|
408
|
+
export function setCookieStore(values: Record<string, string>): void {
|
|
409
|
+
cookieSeed = { ...values };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The currently-installed SSR cookie seed (mainly for tests/introspection). */
|
|
413
|
+
export function getCookieStore(): Record<string, string> {
|
|
414
|
+
return { ...cookieSeed };
|
|
415
|
+
}
|
|
416
|
+
|
|
387
417
|
/**
|
|
388
418
|
* SSR-safe cookie accessor — the one store {@link WebStorage} can't cover, for
|
|
389
|
-
* values the server also reads.
|
|
390
|
-
*
|
|
419
|
+
* values the server also reads. In the browser reads/writes hit
|
|
420
|
+
* `document.cookie`; during SSR reads come from the {@link setCookieStore} seed
|
|
421
|
+
* and writes are a no-op. Names and values are URL-encoded.
|
|
391
422
|
*/
|
|
392
423
|
export const cookie = {
|
|
393
424
|
get(name: string): string | null {
|
|
394
|
-
if (typeof document === "undefined") return null;
|
|
425
|
+
if (typeof document === "undefined") return cookieSeed[name] ?? null;
|
|
395
426
|
const prefix = `${encodeURIComponent(name)}=`;
|
|
396
427
|
for (const part of document.cookie.split("; ")) {
|
|
397
428
|
if (part.startsWith(prefix)) {
|
|
@@ -422,6 +453,91 @@ export const cookie = {
|
|
|
422
453
|
},
|
|
423
454
|
};
|
|
424
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Maps a typed value to and from the string a cookie stores. Pass one to
|
|
458
|
+
* {@link cookieState} for non-string state (booleans, enums, JSON).
|
|
459
|
+
*/
|
|
460
|
+
export interface CookieCodec<T> {
|
|
461
|
+
/** Parse the raw cookie string into `T`. */
|
|
462
|
+
parse(raw: string): T;
|
|
463
|
+
/** Serialize `T` into the raw cookie string. */
|
|
464
|
+
serialize(value: T): string;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Codec for a boolean cookie, stored as `"1"` / `"0"`. */
|
|
468
|
+
export const booleanCookie: CookieCodec<boolean> = {
|
|
469
|
+
parse: (raw) => raw === "1" || raw === "true",
|
|
470
|
+
serialize: (value) => (value ? "1" : "0"),
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Codec for a JSON-serializable value. A parse failure (malformed cookie) is
|
|
475
|
+
* surfaced by returning `fallback`, so a tampered cookie never throws mid-render.
|
|
476
|
+
*/
|
|
477
|
+
export function jsonCookie<T>(fallback: T): CookieCodec<T> {
|
|
478
|
+
return {
|
|
479
|
+
parse: (raw) => {
|
|
480
|
+
try {
|
|
481
|
+
return JSON.parse(raw);
|
|
482
|
+
} catch {
|
|
483
|
+
return fallback;
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
serialize: (value) => JSON.stringify(value),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* A {@link Signal} backed by a cookie — the isomorphic counterpart to
|
|
492
|
+
* {@link persistedSignal}. It is seeded from the cookie (the
|
|
493
|
+
* {@link setCookieStore} request seed during SSR, `document.cookie` in the
|
|
494
|
+
* browser) and persists every change back to the cookie via a reactive
|
|
495
|
+
* `effect`. Because the same seed is visible on both sides, a page renders the
|
|
496
|
+
* SAME markup server- and client-side — no hydration mismatch, no flash of the
|
|
497
|
+
* default state (e.g. a sidebar that animates open→collapsed on every load).
|
|
498
|
+
*
|
|
499
|
+
* Use {@link cookieSignal} for string state; pass a {@link CookieCodec} here for
|
|
500
|
+
* booleans/enums/JSON (e.g. {@link booleanCookie}). Created inside a component's
|
|
501
|
+
* setup, the persistence effect is disposed with it.
|
|
502
|
+
*
|
|
503
|
+
* Read it at the TOP of a page (before any `await`) so the SSR seed is current —
|
|
504
|
+
* see {@link setCookieStore}.
|
|
505
|
+
*/
|
|
506
|
+
export function cookieState<T>(
|
|
507
|
+
name: string,
|
|
508
|
+
initial: T,
|
|
509
|
+
codec: CookieCodec<T>,
|
|
510
|
+
options: CookieOptions = {},
|
|
511
|
+
): Signal<T> {
|
|
512
|
+
const raw = cookie.get(name);
|
|
513
|
+
const sig = signal<T>(raw === null ? initial : codec.parse(raw));
|
|
514
|
+
|
|
515
|
+
// Mirror every change back to the cookie. Runs once immediately (a no-op
|
|
516
|
+
// write during SSR, where `cookie.set` bails) then on each change.
|
|
517
|
+
effect(() => {
|
|
518
|
+
cookie.set(name, codec.serialize(sig()), options);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return sig;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const stringCookie: CookieCodec<string> = {
|
|
525
|
+
parse: (raw) => raw,
|
|
526
|
+
serialize: (value) => value,
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A {@link Signal} backed by a string cookie — {@link cookieState} with an
|
|
531
|
+
* identity codec, for the common case where the value is already a string.
|
|
532
|
+
*/
|
|
533
|
+
export function cookieSignal(
|
|
534
|
+
name: string,
|
|
535
|
+
initial: string,
|
|
536
|
+
options: CookieOptions = {},
|
|
537
|
+
): Signal<string> {
|
|
538
|
+
return cookieState(name, initial, stringCookie, options);
|
|
539
|
+
}
|
|
540
|
+
|
|
425
541
|
// ─── Clipboard & Web Share ───────────────────────────────────────────
|
|
426
542
|
|
|
427
543
|
/** Async clipboard access. Methods return `false`/`null` when unavailable. */
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// this barrel is what lets a browser bundle import the client primitives without
|
|
6
6
|
// the bundler dragging Node built-ins through the import graph.
|
|
7
7
|
export type {
|
|
8
|
+
CookieCodec,
|
|
8
9
|
CookieOptions,
|
|
9
10
|
PersistedSignalOptions,
|
|
10
11
|
ShareData,
|
|
@@ -14,10 +15,15 @@ export type {
|
|
|
14
15
|
} from "./browser.js";
|
|
15
16
|
export {
|
|
16
17
|
back,
|
|
18
|
+
booleanCookie,
|
|
17
19
|
clipboard,
|
|
18
20
|
cookie,
|
|
21
|
+
cookieSignal,
|
|
22
|
+
cookieState,
|
|
19
23
|
forward,
|
|
24
|
+
getCookieStore,
|
|
20
25
|
hash,
|
|
26
|
+
jsonCookie,
|
|
21
27
|
mediaQuery,
|
|
22
28
|
navigate,
|
|
23
29
|
online,
|
|
@@ -28,6 +34,7 @@ export {
|
|
|
28
34
|
reload,
|
|
29
35
|
replace,
|
|
30
36
|
session,
|
|
37
|
+
setCookieStore,
|
|
31
38
|
share,
|
|
32
39
|
storage,
|
|
33
40
|
visibility,
|
|
@@ -116,15 +123,6 @@ export {
|
|
|
116
123
|
type AuroraRouteConfig,
|
|
117
124
|
auroraRoute,
|
|
118
125
|
} from "./route.js";
|
|
119
|
-
export {
|
|
120
|
-
createRpcClient,
|
|
121
|
-
isRpcError,
|
|
122
|
-
type RpcCall,
|
|
123
|
-
type RpcClient,
|
|
124
|
-
type RpcClientOptions,
|
|
125
|
-
RpcError,
|
|
126
|
-
type RpcResult,
|
|
127
|
-
} from "./rpc.js";
|
|
128
126
|
export { renderToString } from "./ssr.js";
|
|
129
127
|
export type { TemplateResult } from "./types.js";
|
|
130
128
|
export { getRouteManifest, setRouteManifest, urlFor } from "./url.js";
|
package/src/rpc.ts
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint — aurora's thin binding
|
|
3
|
+
* over the agnostic {@link https://github.com/C9up/comet | @c9up/comet} client.
|
|
4
|
+
* It wires aurora's {@link HttpClient} (base URL, auth headers, timeouts) as
|
|
5
|
+
* comet's transport, and re-exports the protocol surface so call sites keep
|
|
6
|
+
* importing everything from `@c9up/aurora`.
|
|
6
7
|
*
|
|
7
|
-
* const rpc = createRpcClient()
|
|
8
|
-
* const result = await rpc.call('task.validate', { id })
|
|
9
|
-
* const user = await rpc.call('user.find', { id }, isUser) // validated, cast-free
|
|
8
|
+
* const rpc = createRpcClient() // POST /rpc, same-origin
|
|
9
|
+
* const result = await rpc.call('task.validate', { id }) // typed via call<T>()
|
|
10
|
+
* const user = await rpc.call('user.find', { id }, { parse: isUser }) // validated, cast-free
|
|
11
|
+
* await rpc.call('slow.op', p, { signal: ac.signal }) // abortable
|
|
10
12
|
*
|
|
11
13
|
* Pairs with aurora's `command()` for reactive calls:
|
|
12
14
|
* const validate = command((p) => rpc.call('task.validate', p))
|
|
13
15
|
*/
|
|
16
|
+
import { createRpcClient as createCometRpcClient } from "@c9up/comet";
|
|
14
17
|
import { HttpClient } from "./http.js";
|
|
15
18
|
|
|
19
|
+
export {
|
|
20
|
+
isRpcError,
|
|
21
|
+
type RpcCall,
|
|
22
|
+
type RpcCallOptions,
|
|
23
|
+
type RpcClient,
|
|
24
|
+
RpcError,
|
|
25
|
+
type RpcResult,
|
|
26
|
+
} from "@c9up/comet";
|
|
27
|
+
|
|
28
|
+
import type { RpcClient } from "@c9up/comet";
|
|
29
|
+
|
|
16
30
|
export interface RpcClientOptions {
|
|
17
31
|
/** Endpoint path. Default `/rpc` (matches RpcProvider's default). */
|
|
18
32
|
url?: string;
|
|
@@ -22,124 +36,15 @@ export interface RpcClientOptions {
|
|
|
22
36
|
headers?: Record<string, string>;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
constructor(code: number, message: string, data?: unknown) {
|
|
30
|
-
super(message);
|
|
31
|
-
this.name = "RpcError";
|
|
32
|
-
this.code = code;
|
|
33
|
-
this.data = data;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Type guard for {@link RpcError}. */
|
|
38
|
-
export function isRpcError(value: unknown): value is RpcError {
|
|
39
|
-
return value instanceof RpcError;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** One call in a batch. `parse` optionally validates that call's result (cast-free). */
|
|
43
|
-
export interface RpcCall<T = unknown> {
|
|
44
|
-
method: string;
|
|
45
|
-
params?: unknown;
|
|
46
|
-
parse?: (data: unknown) => T;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** A settled batch entry — the result, or the JSON-RPC error for that call. */
|
|
50
|
-
export type RpcResult<T = unknown> =
|
|
51
|
-
| { ok: true; value: T }
|
|
52
|
-
| { ok: false; error: RpcError };
|
|
53
|
-
|
|
54
|
-
export interface RpcClient {
|
|
55
|
-
/**
|
|
56
|
-
* Call one method. Returns the result, or throws {@link RpcError} on a
|
|
57
|
-
* JSON-RPC error. Pass `parse` to validate the result at runtime (and skip
|
|
58
|
-
* the unchecked `T` assertion).
|
|
59
|
-
*/
|
|
60
|
-
call<T = unknown>(
|
|
61
|
-
method: string,
|
|
62
|
-
params?: unknown,
|
|
63
|
-
parse?: (data: unknown) => T,
|
|
64
|
-
): Promise<T>;
|
|
65
|
-
/** Send a JSON-RPC batch. Returns one settled entry per call, in request order. */
|
|
66
|
-
batch(calls: RpcCall[]): Promise<RpcResult[]>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
70
|
-
return typeof value === "object" && value !== null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Turn a JSON-RPC `error` member into an {@link RpcError}. */
|
|
74
|
-
function toRpcError(error: unknown): RpcError {
|
|
75
|
-
if (
|
|
76
|
-
isObject(error) &&
|
|
77
|
-
typeof error.code === "number" &&
|
|
78
|
-
typeof error.message === "string"
|
|
79
|
-
) {
|
|
80
|
-
return new RpcError(error.code, error.message, error.data);
|
|
81
|
-
}
|
|
82
|
-
return new RpcError(-32603, "Malformed JSON-RPC error envelope", error);
|
|
83
|
-
}
|
|
84
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Create a JSON-RPC client bound to aurora's HttpClient transport. Inherits the
|
|
41
|
+
* supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts.
|
|
42
|
+
*/
|
|
85
43
|
export function createRpcClient(options: RpcClientOptions = {}): RpcClient {
|
|
86
44
|
const http = options.http ?? new HttpClient({ headers: options.headers });
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
method: string,
|
|
93
|
-
params?: unknown,
|
|
94
|
-
parse?: (data: unknown) => T,
|
|
95
|
-
): Promise<T> {
|
|
96
|
-
const id = ++nextId;
|
|
97
|
-
const res = await http.post<unknown>(url, {
|
|
98
|
-
jsonrpc: "2.0",
|
|
99
|
-
method,
|
|
100
|
-
params,
|
|
101
|
-
id,
|
|
102
|
-
});
|
|
103
|
-
if (!isObject(res)) {
|
|
104
|
-
throw new RpcError(
|
|
105
|
-
-32603,
|
|
106
|
-
`Malformed JSON-RPC response for "${method}"`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
if (res.error !== undefined) throw toRpcError(res.error);
|
|
110
|
-
// Result boundary — the same unchecked `T` assertion HttpClient uses,
|
|
111
|
-
// with `parse` as the cast-free, runtime-validated escape hatch.
|
|
112
|
-
return parse ? parse(res.result) : (res.result as T);
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
async batch(calls: RpcCall[]): Promise<RpcResult[]> {
|
|
116
|
-
if (calls.length === 0) return [];
|
|
117
|
-
const requests = calls.map((c, index) => ({
|
|
118
|
-
jsonrpc: "2.0",
|
|
119
|
-
method: c.method,
|
|
120
|
-
params: c.params,
|
|
121
|
-
id: index, // index = request position; responses are matched back by id
|
|
122
|
-
}));
|
|
123
|
-
const res = await http.post<unknown>(url, requests);
|
|
124
|
-
if (!Array.isArray(res)) {
|
|
125
|
-
throw new RpcError(-32603, "Malformed JSON-RPC batch response");
|
|
126
|
-
}
|
|
127
|
-
const byId = new Map<unknown, Record<string, unknown>>();
|
|
128
|
-
for (const item of res) if (isObject(item)) byId.set(item.id, item);
|
|
129
|
-
return calls.map((c, index) => {
|
|
130
|
-
const envelope = byId.get(index);
|
|
131
|
-
if (!envelope) {
|
|
132
|
-
return {
|
|
133
|
-
ok: false,
|
|
134
|
-
error: new RpcError(-32603, `No response for "${c.method}"`),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (envelope.error !== undefined) {
|
|
138
|
-
return { ok: false, error: toRpcError(envelope.error) };
|
|
139
|
-
}
|
|
140
|
-
const value = c.parse ? c.parse(envelope.result) : envelope.result;
|
|
141
|
-
return { ok: true, value };
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
};
|
|
45
|
+
return createCometRpcClient({
|
|
46
|
+
url: options.url,
|
|
47
|
+
transport: (url, body, { signal }) =>
|
|
48
|
+
http.post<unknown>(url, body, { signal }),
|
|
49
|
+
});
|
|
145
50
|
}
|
package/src/server/renderPage.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* </body>
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { setCookieStore } from "../browser.js";
|
|
26
27
|
import type { Pages } from "../Pages.js";
|
|
27
28
|
import { renderToString } from "../ssr.js";
|
|
28
29
|
import { setRouteManifest } from "../url.js";
|
|
@@ -71,6 +72,46 @@ export interface RenderPageOptions {
|
|
|
71
72
|
* in SSR and the browser. Omit if the app doesn't use `urlFor`.
|
|
72
73
|
*/
|
|
73
74
|
routes?: Record<string, string>;
|
|
75
|
+
/**
|
|
76
|
+
* Allowlist of cookie names to seed into the SSR cookie store so a page can
|
|
77
|
+
* read them during render via aurora's `cookieSignal` / `cookie.get` — the
|
|
78
|
+
* server then renders the SAME UI state the browser will (no hydration flash
|
|
79
|
+
* of the default, e.g. a sidebar animating open→collapsed on every load).
|
|
80
|
+
*
|
|
81
|
+
* Only the NAMED cookies are read (from `ctx.request`); they are NOT
|
|
82
|
+
* serialized into the page — the browser reads them from `document.cookie`.
|
|
83
|
+
* NEVER list a session / signed / encrypted / `httpOnly` cookie here: those
|
|
84
|
+
* must stay server-only. Use plain, JS-readable cookies for UI state.
|
|
85
|
+
*/
|
|
86
|
+
cookies?: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A request that can read a cookie by name — the structural slice we need. */
|
|
90
|
+
interface CookieReadableRequest {
|
|
91
|
+
cookie(name: string): string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isCookieReadable(request: unknown): request is CookieReadableRequest {
|
|
95
|
+
return (
|
|
96
|
+
typeof request === "object" &&
|
|
97
|
+
request !== null &&
|
|
98
|
+
"cookie" in request &&
|
|
99
|
+
typeof request.cookie === "function"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Read the allowlisted cookies off the request into a `name → value` seed. */
|
|
104
|
+
function readRequestCookies(
|
|
105
|
+
request: unknown,
|
|
106
|
+
names: string[],
|
|
107
|
+
): Record<string, string> {
|
|
108
|
+
if (!isCookieReadable(request)) return {};
|
|
109
|
+
const seed: Record<string, string> = {};
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
const value = request.cookie(name);
|
|
112
|
+
if (value !== null) seed[name] = value;
|
|
113
|
+
}
|
|
114
|
+
return seed;
|
|
74
115
|
}
|
|
75
116
|
|
|
76
117
|
export async function renderPage<P>(
|
|
@@ -84,6 +125,13 @@ export async function renderPage<P>(
|
|
|
84
125
|
// during SSR resolves against the same map the client will get.
|
|
85
126
|
if (options.routes) setRouteManifest(options.routes);
|
|
86
127
|
|
|
128
|
+
// Seed the request's UI cookies so the page reads the SAME state server-side
|
|
129
|
+
// that the browser will after hydration. Set synchronously right before the
|
|
130
|
+
// (synchronous) render — read cookie signals at the top of the page.
|
|
131
|
+
setCookieStore(
|
|
132
|
+
options.cookies ? readRequestCookies(ctx.request, options.cookies) : {},
|
|
133
|
+
);
|
|
134
|
+
|
|
87
135
|
const factory = await pages.resolve(name);
|
|
88
136
|
// The factory must be invoked the SAME way client-side for hydrate
|
|
89
137
|
// to find matching slots — `Page(props)` is the contract.
|