@webjsdev/core 0.7.17 → 0.7.18

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/index-browser.js CHANGED
@@ -52,6 +52,10 @@ export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.
52
52
  // Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
53
53
  export { optimistic } from './src/optimistic.js';
54
54
 
55
+ // SSR action-seed consumer (#472): the generated RPC stub reads a seed on its
56
+ // first call so async-render hydration does not re-fetch the SSR'd data.
57
+ export { takeSeed, scanSeeds, SEED_MISS } from './src/action-seed-client.js';
58
+
55
59
  // Directives, also available via '@webjsdev/core/directives'. The full
56
60
  // lit-html-parity set is re-exported here so the dist browser bundle (which
57
61
  // the `@webjsdev/core/directives` subpath collapses onto in dist mode) carries
package/index.d.ts CHANGED
@@ -91,6 +91,7 @@ export { Task, TaskStatus } from './src/task.js';
91
91
  // src/*.d.ts (#388 fixed the index.d.ts drift from index.js).
92
92
  export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
93
93
  export { cspNonce, setCspNonceProvider } from './src/csp-nonce.js';
94
+ export { takeSeed, scanSeeds, SEED_MISS } from './src/action-seed-client.js';
94
95
  export { stringify, parse, serialize, deserialize } from './src/serialize.js';
95
96
  export { WebjsFrame } from './src/webjs-frame.js';
96
97
  export { WebjsStream, renderStream } from './src/webjs-stream.js';
package/index.js CHANGED
@@ -34,6 +34,10 @@ export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.
34
34
  // Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
35
35
  export { optimistic } from './src/optimistic.js';
36
36
 
37
+ // SSR action-seed consumer (#472): the generated RPC stub reads a seed on its
38
+ // first call so async-render hydration does not re-fetch. Inert server-side.
39
+ export { takeSeed, scanSeeds, SEED_MISS } from './src/action-seed-client.js';
40
+
37
41
  // Directives, also available via '@webjsdev/core/directives'. The full
38
42
  // lit-html-parity set is re-exported so the bare specifier exposes the same
39
43
  // directive surface in Node as the browser bundle does.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/core",
3
- "version": "0.7.17",
3
+ "version": "0.7.18",
4
4
  "type": "module",
5
5
  "description": "webjs core runtime - html/css tags, WebComponent base, isomorphic renderers",
6
6
  "types": "./index.d.ts",
@@ -0,0 +1,17 @@
1
+ /** Client-side SSR action-seed consumer (#472). Inert server-side. */
2
+
3
+ /** Returned by `takeSeed` when no seed matches; distinct from any real value. */
4
+ export const SEED_MISS: unique symbol;
5
+
6
+ /**
7
+ * Merge any seeds found under `root` (or the whole document) into the global
8
+ * consume-once store, removing the carriers. Reads the page-level
9
+ * `#__webjs-seeds` JSON block and per-element `[data-webjs-seed]` carriers.
10
+ */
11
+ export function scanSeeds(root?: ParentNode): void;
12
+
13
+ /**
14
+ * Look up and CONSUME the seed for an action call, or return `SEED_MISS`.
15
+ * Keyed `hash/fn/argsKey`; the first call lazily scans the initial document.
16
+ */
17
+ export function takeSeed(hash: string, fnName: string, argsKey: string): unknown;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Client-side SSR action-seed consumer (#472).
3
+ *
4
+ * The server (`@webjsdev/server`'s `action-seed.js`) serializes each action
5
+ * result invoked during SSR into a `<script type="application/json"
6
+ * id="__webjs-seeds">` block (and, for streamed regions, per-element
7
+ * `data-webjs-seed` attributes). On the client, the generated RPC stub
8
+ * (`actions.js`) calls `takeSeed(hash, fn, argsKey)` on its FIRST invocation: a
9
+ * hit resolves the action synchronously with the SSR value (no RPC, no flicker
10
+ * on hydration); a miss falls through to the normal RPC.
11
+ *
12
+ * Consume-once: a hit removes the seed, so a later refetch / arg-change always
13
+ * goes to the network (the seed is a first-paint optimization, not a cache).
14
+ * Keyed `hash/fn/stringify(args)`, where `hash` and `stringify` match exactly
15
+ * what the server emitted, so distinct components and distinct args each map to
16
+ * their own seed. A miss is always safe (it just re-fetches).
17
+ *
18
+ * This module is imported by the generated stub via the bare `@webjsdev/core`
19
+ * specifier; on the server it is inert (its only DOM access is inside
20
+ * `scanSeeds`, never called server-side).
21
+ */
22
+
23
+ import { parse } from './serialize.js';
24
+
25
+ /** Global consume-once seed store: `key -> value`. */
26
+ const seeds = new Map();
27
+
28
+ /** Returned by `takeSeed` when no seed matches; distinct from any real value. */
29
+ export const SEED_MISS = Symbol('webjs.seed.miss');
30
+
31
+ /** One-time eager scan guard (the initial-load document is scanned lazily). */
32
+ let scannedInitial = false;
33
+
34
+ /**
35
+ * Merge any seeds found under `root` into the global store, then remove the
36
+ * carriers so a re-scan (a streamed boundary, a soft navigation) never
37
+ * re-ingests stale data. Reads both the page-level `#__webjs-seeds` JSON block
38
+ * and per-element `[data-webjs-seed]` carriers. Idempotent and fail-open: a
39
+ * malformed payload is skipped, never thrown.
40
+ * @param {ParentNode & { querySelectorAll?: Function }} [root]
41
+ */
42
+ export function scanSeeds(root) {
43
+ const scope = root || (typeof document !== 'undefined' ? document : null);
44
+ if (!scope || typeof scope.querySelectorAll !== 'function') return;
45
+ // Page-level JSON block(s).
46
+ for (const el of scope.querySelectorAll('script[type="application/json"]#__webjs-seeds, script[type="application/json"][data-webjs-seeds]')) {
47
+ ingest(el.textContent, el);
48
+ }
49
+ // Per-element carriers (streamed boundaries / future per-component seeding).
50
+ for (const el of scope.querySelectorAll('[data-webjs-seed]')) {
51
+ ingest(el.getAttribute('data-webjs-seed'), el, () => el.removeAttribute('data-webjs-seed'));
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Parse one serialized seed payload and merge it (first write wins, so an
57
+ * already-consumed or earlier seed is never clobbered by a later duplicate),
58
+ * then run `cleanup` (or remove the element).
59
+ * @param {string | null} raw
60
+ * @param {Element} el
61
+ * @param {() => void} [cleanup]
62
+ */
63
+ function ingest(raw, el, cleanup) {
64
+ if (raw) {
65
+ try {
66
+ const obj = parse(raw);
67
+ if (obj && typeof obj === 'object') {
68
+ for (const k in obj) if (!seeds.has(k)) seeds.set(k, obj[k]);
69
+ }
70
+ } catch {
71
+ // Malformed payload: ignore, the stub re-fetches.
72
+ }
73
+ }
74
+ if (cleanup) cleanup();
75
+ else el.remove?.();
76
+ }
77
+
78
+ /**
79
+ * Look up and CONSUME the seed for an action call. Returns the seeded value
80
+ * (removing it) on a hit, or `SEED_MISS` when there is none. The first call
81
+ * lazily scans the initial document, so the boot path needs no wiring; the
82
+ * router calls `scanSeeds(subtree)` for content that arrives later.
83
+ * @param {string} hash the action file's hash (the RPC endpoint hash)
84
+ * @param {string} fnName the exported action name
85
+ * @param {string} argsKey `stringify(args)`, computed by the stub with the same
86
+ * serializer the server used, so identical args produce an identical key
87
+ * @returns {unknown | typeof SEED_MISS}
88
+ */
89
+ export function takeSeed(hash, fnName, argsKey) {
90
+ if (!scannedInitial) {
91
+ scannedInitial = true;
92
+ scanSeeds();
93
+ }
94
+ const key = `${hash}/${fnName}/${argsKey}`;
95
+ if (seeds.has(key)) {
96
+ const v = seeds.get(key);
97
+ seeds.delete(key);
98
+ return v;
99
+ }
100
+ return SEED_MISS;
101
+ }
102
+
103
+ /** Test seam: drop all seeds and reset the lazy-scan guard. */
104
+ export function __resetSeeds() {
105
+ seeds.clear();
106
+ scannedInitial = false;
107
+ }
@@ -11,6 +11,9 @@ import { renderStream } from './webjs-stream.js';
11
11
  // Register <webjs-suspense> (the element-level streaming boundary, #471) so it
12
12
  // is layout-neutral and available for the progressive soft-nav streaming apply.
13
13
  import './webjs-suspense.js';
14
+ // Ingest SSR action seeds (#472) from an incoming soft-nav document before its
15
+ // components hydrate, so a navigated async component resolves from the seed.
16
+ import { scanSeeds } from './action-seed-client.js';
14
17
 
15
18
  /** The content type a content-negotiated stream-action response carries (#248). */
16
19
  const STREAM_MIME = 'text/vnd.webjs-stream.html';
@@ -2067,6 +2070,13 @@ function upgradeCustomElementsInRange(range) {
2067
2070
  }
2068
2071
 
2069
2072
  function applySwap(doc, frameId, revalidating, href, incomingBuild) {
2073
+ // SSR action seeding (#472): ingest any seed payload the incoming page
2074
+ // carries BEFORE its components are grafted into the live DOM and upgrade, so
2075
+ // a soft-navigated async component resolves from the seed instead of
2076
+ // re-fetching. Scanning `doc` (the detached parse) also strips the seed
2077
+ // carriers, so the inert payload never lands in the live document.
2078
+ try { scanSeeds(doc); } catch { /* seeding is best-effort */ }
2079
+
2070
2080
  // Any clean swap (no importmap mismatch, including cache restores
2071
2081
  // and frame swaps where we don't even run the mismatch check) is a
2072
2082
  // signal that the user successfully navigated, so clear the reload
@@ -109,6 +109,15 @@ export interface WebjsConfig {
109
109
  */
110
110
  elide?: boolean;
111
111
 
112
+ /**
113
+ * SSR action-result seeding switch (#472). Default `true`. When on, a
114
+ * `'use server'` action's SSR result is serialized into the page so an
115
+ * `async render()` component does not re-fetch it over RPC on hydration. Set
116
+ * to `false` to disable (the client re-fetches as before). The `WEBJS_SEED`
117
+ * env override wins over this.
118
+ */
119
+ seed?: boolean;
120
+
112
121
  /** Per-path response-header rules, shaped like Next's. */
113
122
  headers?: WebjsHeaderRule[];
114
123