@solidjs/web 2.0.0-beta.22 → 2.0.0-beta.23

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.
Files changed (42) hide show
  1. package/dist/dev.cjs +25 -1
  2. package/dist/dev.js +25 -2
  3. package/dist/server.cjs +79 -33
  4. package/dist/server.js +79 -34
  5. package/dist/web.cjs +25 -1
  6. package/dist/web.js +25 -2
  7. package/frames/dist/client.cjs +1442 -0
  8. package/frames/dist/client.js +1430 -0
  9. package/frames/dist/server.cjs +1705 -0
  10. package/frames/dist/server.js +1694 -0
  11. package/frames/package.json +30 -0
  12. package/package.json +78 -5
  13. package/serialization/dist/serialization.cjs +83 -0
  14. package/serialization/dist/serialization.js +82 -1
  15. package/serialization/types/index.d.ts +12 -0
  16. package/serialization/types-cjs/index.d.cts +12 -0
  17. package/server-functions/dist/client.cjs +82 -55
  18. package/server-functions/dist/client.js +83 -56
  19. package/server-functions/dist/server.cjs +20 -3
  20. package/server-functions/dist/server.js +20 -3
  21. package/types/client.d.ts +8 -0
  22. package/types/core.d.ts +2 -1
  23. package/types/frames/client.d.ts +53 -0
  24. package/types/frames/frame-client.d.ts +205 -0
  25. package/types/frames/frame-sink.d.ts +145 -0
  26. package/types/frames/frame-transport.d.ts +105 -0
  27. package/types/frames/serializer.d.ts +151 -0
  28. package/types/frames/server.d.ts +21 -0
  29. package/types/serializer.d.ts +12 -0
  30. package/types/server-functions/client.d.ts +24 -0
  31. package/types/server.d.ts +2 -0
  32. package/types-cjs/client.d.cts +8 -0
  33. package/types-cjs/core.d.cts +2 -1
  34. package/types-cjs/frames/client.d.cts +53 -0
  35. package/types-cjs/frames/frame-client.d.cts +205 -0
  36. package/types-cjs/frames/frame-sink.d.cts +145 -0
  37. package/types-cjs/frames/frame-transport.d.cts +105 -0
  38. package/types-cjs/frames/serializer.d.cts +151 -0
  39. package/types-cjs/frames/server.d.cts +21 -0
  40. package/types-cjs/serializer.d.cts +12 -0
  41. package/types-cjs/server-functions/client.d.cts +24 -0
  42. package/types-cjs/server.d.cts +2 -0
@@ -1,4 +1,4 @@
1
- import { fromCrossJSON, Feature, toCrossJSONStream } from 'seroval';
1
+ import { fromCrossJSON, Feature } from 'seroval';
2
2
  import { AbortSignalPlugin, CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLSearchParamsPlugin, URLPlugin } from 'seroval-plugins/web';
3
3
 
4
4
  Feature.AggregateError | Feature.BigIntTypedArray;
@@ -21,19 +21,6 @@ function resolveCodecOptions({
21
21
  depthLimit: depthLimit === undefined ? JSON_CODEC_DEPTH_LIMIT : depthLimit
22
22
  };
23
23
  }
24
- function serializeJSON(value, {
25
- onParse,
26
- onDone,
27
- onError,
28
- ...codecOptions
29
- }) {
30
- return toCrossJSONStream(value, {
31
- onParse,
32
- onDone,
33
- onError,
34
- ...resolveCodecOptions(codecOptions)
35
- });
36
- }
37
24
  function createJSONDeserializer(options) {
38
25
  const refs = new Map();
39
26
  const resolved = resolveCodecOptions(options);
@@ -51,9 +38,6 @@ const codecConfig = {
51
38
  function configureServerFunctionsCodec(codec) {
52
39
  codecConfig.codec = codec;
53
40
  }
54
- function getServerFunctionsCodec() {
55
- return codecConfig.codec;
56
- }
57
41
  const flightConfig = {
58
42
  consumer: undefined
59
43
  };
@@ -120,7 +104,8 @@ const BodyFormat = {
120
104
  Blob: "4",
121
105
  File: "5",
122
106
  ArrayBuffer: "6",
123
- Uint8Array: "7"
107
+ Uint8Array: "7",
108
+ Json: "8"
124
109
  };
125
110
  function getHeadersAndBody(body) {
126
111
  switch (true) {
@@ -190,6 +175,8 @@ async function extractBody(source, codecOptions) {
190
175
  switch (true) {
191
176
  case format === BodyFormat.Serialized:
192
177
  return await deserializeStream(clone, codecOptions);
178
+ case format === BodyFormat.Json:
179
+ return JSON.parse(await clone.text());
193
180
  case format === BodyFormat.String:
194
181
  return await clone.text();
195
182
  case format === BodyFormat.File:
@@ -212,17 +199,6 @@ async function extractBody(source, codecOptions) {
212
199
  }
213
200
  return undefined;
214
201
  }
215
- function createChunk(data) {
216
- const encodeData = new TextEncoder().encode(data);
217
- const bytes = encodeData.length;
218
- const baseHex = bytes.toString(16);
219
- const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex;
220
- const head = new TextEncoder().encode(`;0x${totalHex};`);
221
- const chunk = new Uint8Array(12 + bytes);
222
- chunk.set(head);
223
- chunk.set(encodeData, 12);
224
- return chunk;
225
- }
226
202
  class ChunkReader {
227
203
  constructor(stream) {
228
204
  this.reader = stream.getReader();
@@ -279,28 +255,6 @@ class ChunkReader {
279
255
  }
280
256
  }
281
257
  }
282
- function serializeStream(value, codecOptions) {
283
- return new ReadableStream({
284
- start(controller) {
285
- serializeJSON(value, {
286
- ...codecOptions,
287
- onParse(node) {
288
- controller.enqueue(createChunk(JSON.stringify(node)));
289
- },
290
- onDone() {
291
- controller.close();
292
- },
293
- onError(error) {
294
- controller.error(error);
295
- }
296
- });
297
- }
298
- });
299
- }
300
- async function serializeString(value, codecOptions) {
301
- const response = new Response(serializeStream(value, codecOptions));
302
- return await response.text();
303
- }
304
258
  async function deserializeStream(source, codecOptions) {
305
259
  if (!source.body) {
306
260
  throw new Error("missing body");
@@ -324,16 +278,43 @@ async function decodeResponse(response, codecOptions) {
324
278
 
325
279
  const config = {
326
280
  endpoint: "/_server",
327
- prepareRequest: undefined
281
+ prepareRequest: undefined,
282
+ responseHandler: undefined,
283
+ serializeArgs: undefined
328
284
  };
285
+ function isJSONSafe(value) {
286
+ if (value === null) return true;
287
+ const t = typeof value;
288
+ if (t === "string" || t === "boolean") return true;
289
+ if (t === "number") return Number.isFinite(value);
290
+ if (t !== "object") return false;
291
+ if (Array.isArray(value)) {
292
+ for (const v of value) if (!isJSONSafe(v)) return false;
293
+ return true;
294
+ }
295
+ const proto = Object.getPrototypeOf(value);
296
+ if (proto !== Object.prototype && proto !== null) return false;
297
+ for (const k in value) if (!isJSONSafe(value[k])) return false;
298
+ return true;
299
+ }
300
+ function serializeArguments(args) {
301
+ if (!config.serializeArgs) {
302
+ throw new Error("Server function arguments are sent as JSON by default and these " + "arguments are not JSON-serializable. Call enableRichArguments() " + "(from the server-functions rich-args entry) once at startup to " + "send Dates, Maps, Sets, typed arrays, etc. through the codec — or " + "pass a single Blob/FormData/File argument, which has a native " + "HTTP encoding.");
303
+ }
304
+ return config.serializeArgs(args);
305
+ }
329
306
  function configureServerFunctionsClient({
330
307
  endpoint,
331
308
  codec,
332
- prepareRequest
309
+ prepareRequest,
310
+ responseHandler,
311
+ serializeArgs
333
312
  } = {}) {
334
313
  if (endpoint !== undefined) config.endpoint = endpoint;
335
314
  if (codec !== undefined) configureServerFunctionsCodec(codec);
336
315
  if (prepareRequest !== undefined) config.prepareRequest = prepareRequest;
316
+ if (responseHandler !== undefined) config.responseHandler = responseHandler;
317
+ if (serializeArgs !== undefined) config.serializeArgs = serializeArgs;
337
318
  }
338
319
  let INSTANCE = 0;
339
320
  async function createRequest(base, id, instance, options, meta) {
@@ -375,9 +356,20 @@ async function initializeResponse(base, id, instance, options, args, meta) {
375
356
  }, meta);
376
357
  }
377
358
  }
359
+ if (isJSONSafe(args)) {
360
+ return createRequest(base, id, instance, {
361
+ ...options,
362
+ body: JSON.stringify(args),
363
+ headers: {
364
+ ...options.headers,
365
+ "Content-Type": "application/json",
366
+ [BODY_FORMAT_HEADER]: BodyFormat.Json
367
+ }
368
+ }, meta);
369
+ }
378
370
  return createRequest(base, id, instance, {
379
371
  ...options,
380
- body: await serializeString(args, getServerFunctionsCodec()),
372
+ body: await serializeArguments(args),
381
373
  headers: {
382
374
  ...options.headers,
383
375
  "Content-Type": "text/plain",
@@ -387,7 +379,21 @@ async function initializeResponse(base, id, instance, options, args, meta) {
387
379
  }
388
380
  async function fetchServerFunction(base, id, options, args, meta) {
389
381
  const instance = `server-function:${INSTANCE++}`;
382
+ const handler = config.responseHandler;
383
+ const context = handler && handler.capture ? handler.capture({
384
+ id,
385
+ meta
386
+ }) : undefined;
390
387
  const response = await initializeResponse(base, id, instance, options, args, meta);
388
+ if (handler) {
389
+ const handled = handler.handle(response, {
390
+ id,
391
+ meta,
392
+ args,
393
+ context
394
+ });
395
+ if (handled !== undefined) return handled;
396
+ }
391
397
  if (response.headers.has(SINGLE_FLIGHT_HEADER)) {
392
398
  const consumer = getFlightDataConsumer();
393
399
  if (consumer) {
@@ -414,7 +420,18 @@ function createServerReference(id, name, base) {
414
420
  const metadata = name === undefined ? {} : {
415
421
  name
416
422
  };
417
- const fn = (...args) => fetchServerFunction(base || config.endpoint, id, {}, args, metadata);
423
+ const fn = (...args) => {
424
+ const handler = config.responseHandler;
425
+ if (handler && handler.intercept) {
426
+ const hit = handler.intercept({
427
+ id,
428
+ meta: metadata,
429
+ args
430
+ });
431
+ if (hit !== undefined) return hit;
432
+ }
433
+ return fetchServerFunction(base || config.endpoint, id, {}, args, metadata);
434
+ };
418
435
  fn[SERVER_FUNCTION_METADATA] = metadata;
419
436
  return new Proxy(fn, {
420
437
  get(target, prop) {
@@ -435,9 +452,19 @@ function GET(fn) {
435
452
  ...getServerFunctionMetadata(fn)
436
453
  };
437
454
  const wrapped = async (...args) => {
455
+ const handler = config.responseHandler;
456
+ if (handler && handler.intercept) {
457
+ const hit = handler.intercept({
458
+ id,
459
+ meta: metadata,
460
+ args
461
+ });
462
+ if (hit !== undefined) return hit;
463
+ }
438
464
  let base = `${config.endpoint}?id=${encodeURIComponent(id)}`;
439
465
  if (args.length) {
440
- base += `&args=${encodeURIComponent(await serializeString(args, getServerFunctionsCodec()))}`;
466
+ const encoded = isJSONSafe(args) ? JSON.stringify(args) : await serializeArguments(args);
467
+ base += `&args=${encodeURIComponent(encoded)}`;
441
468
  }
442
469
  return fetchServerFunction(base, id, {
443
470
  method: "GET"
@@ -125,7 +125,8 @@ const BodyFormat = {
125
125
  Blob: "4",
126
126
  File: "5",
127
127
  ArrayBuffer: "6",
128
- Uint8Array: "7"
128
+ Uint8Array: "7",
129
+ Json: "8"
129
130
  };
130
131
  function getHeadersAndBody(body) {
131
132
  switch (true) {
@@ -195,6 +196,8 @@ async function extractBody(source, codecOptions) {
195
196
  switch (true) {
196
197
  case format === BodyFormat.Serialized:
197
198
  return await deserializeStream(clone, codecOptions);
199
+ case format === BodyFormat.Json:
200
+ return JSON.parse(await clone.text());
198
201
  case format === BodyFormat.String:
199
202
  return await clone.text();
200
203
  case format === BodyFormat.File:
@@ -329,16 +332,19 @@ async function decodeResponse(response, codecOptions) {
329
332
  const config = {
330
333
  provideEvent: undefined,
331
334
  collectFlightData: undefined,
335
+ transformDirectResult: undefined,
332
336
  endpoint: "/_server"
333
337
  };
334
338
  function configureServerFunctionsServer({
335
339
  provideEvent,
336
340
  collectFlightData,
341
+ transformDirectResult,
337
342
  endpoint,
338
343
  codec
339
344
  } = {}) {
340
345
  if (provideEvent !== undefined) config.provideEvent = provideEvent;
341
346
  if (collectFlightData !== undefined) config.collectFlightData = collectFlightData;
347
+ if (transformDirectResult !== undefined) config.transformDirectResult = transformDirectResult;
342
348
  if (endpoint !== undefined) config.endpoint = endpoint;
343
349
  if (codec !== undefined) configureServerFunctionsCodec(codec);
344
350
  }
@@ -397,9 +403,20 @@ function createServerReference({
397
403
  id
398
404
  };
399
405
  evt.serverOnly = true;
400
- return provideEvent(evt, () => {
406
+ const result = provideEvent(evt, () => {
401
407
  return fn.apply(thisArg, args);
402
408
  });
409
+ const transform = config.transformDirectResult;
410
+ if (transform && result && typeof result.then === "function") {
411
+ return result.then(value => transform(value, {
412
+ id,
413
+ event: evt
414
+ }));
415
+ }
416
+ return transform ? transform(result, {
417
+ id,
418
+ event: evt
419
+ }) : result;
403
420
  }
404
421
  });
405
422
  }
@@ -437,7 +454,7 @@ async function parseArguments(request, url, instance, codec) {
437
454
  }
438
455
  if (request.method === "POST" && request.body !== null) {
439
456
  const decoded = await extractBody(request.clone(), codec);
440
- if (bodyFormat === BodyFormat.Serialized) {
457
+ if (bodyFormat === BodyFormat.Serialized || bodyFormat === BodyFormat.Json) {
441
458
  return decoded;
442
459
  }
443
460
  parsed.push(decoded);
@@ -123,7 +123,8 @@ const BodyFormat = {
123
123
  Blob: "4",
124
124
  File: "5",
125
125
  ArrayBuffer: "6",
126
- Uint8Array: "7"
126
+ Uint8Array: "7",
127
+ Json: "8"
127
128
  };
128
129
  function getHeadersAndBody(body) {
129
130
  switch (true) {
@@ -193,6 +194,8 @@ async function extractBody(source, codecOptions) {
193
194
  switch (true) {
194
195
  case format === BodyFormat.Serialized:
195
196
  return await deserializeStream(clone, codecOptions);
197
+ case format === BodyFormat.Json:
198
+ return JSON.parse(await clone.text());
196
199
  case format === BodyFormat.String:
197
200
  return await clone.text();
198
201
  case format === BodyFormat.File:
@@ -327,16 +330,19 @@ async function decodeResponse(response, codecOptions) {
327
330
  const config = {
328
331
  provideEvent: undefined,
329
332
  collectFlightData: undefined,
333
+ transformDirectResult: undefined,
330
334
  endpoint: "/_server"
331
335
  };
332
336
  function configureServerFunctionsServer({
333
337
  provideEvent,
334
338
  collectFlightData,
339
+ transformDirectResult,
335
340
  endpoint,
336
341
  codec
337
342
  } = {}) {
338
343
  if (provideEvent !== undefined) config.provideEvent = provideEvent;
339
344
  if (collectFlightData !== undefined) config.collectFlightData = collectFlightData;
345
+ if (transformDirectResult !== undefined) config.transformDirectResult = transformDirectResult;
340
346
  if (endpoint !== undefined) config.endpoint = endpoint;
341
347
  if (codec !== undefined) configureServerFunctionsCodec(codec);
342
348
  }
@@ -395,9 +401,20 @@ function createServerReference({
395
401
  id
396
402
  };
397
403
  evt.serverOnly = true;
398
- return provideEvent(evt, () => {
404
+ const result = provideEvent(evt, () => {
399
405
  return fn.apply(thisArg, args);
400
406
  });
407
+ const transform = config.transformDirectResult;
408
+ if (transform && result && typeof result.then === "function") {
409
+ return result.then(value => transform(value, {
410
+ id,
411
+ event: evt
412
+ }));
413
+ }
414
+ return transform ? transform(result, {
415
+ id,
416
+ event: evt
417
+ }) : result;
401
418
  }
402
419
  });
403
420
  }
@@ -435,7 +452,7 @@ async function parseArguments(request, url, instance, codec) {
435
452
  }
436
453
  if (request.method === "POST" && request.body !== null) {
437
454
  const decoded = await extractBody(request.clone(), codec);
438
- if (bodyFormat === BodyFormat.Serialized) {
455
+ if (bodyFormat === BodyFormat.Serialized || bodyFormat === BodyFormat.Json) {
439
456
  return decoded;
440
457
  }
441
458
  parsed.push(decoded);
package/types/client.d.ts CHANGED
@@ -84,6 +84,14 @@ export function registerElementClaim(handler: (element: Element) => void): () =>
84
84
  * Emitted by the compiler at element creation; idempotent by contract.
85
85
  */
86
86
  export function claimElement<T extends Element>(node: T): T;
87
+ /**
88
+ * Sweep-claim every navigation-relevant element (`a[href]`, `form[action]`)
89
+ * in `root` — the subtree equivalent of the per-element `claimElement`
90
+ * compiled output emits, for content that becomes live DOM without compiled
91
+ * creation code (frame streams, adopted SSR ranges). Dormant without a
92
+ * registered consumer.
93
+ */
94
+ export function claimElementTree<T extends Node>(root: T): T;
87
95
  export function className(node: Element, value: JSX.ClassValue, prev?: JSX.ClassValue): void;
88
96
  export function setProperty(node: Element, name: string, value: any): void;
89
97
  export function setStyleProperty(node: Element, name: string, value: any): void;
package/types/core.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { getOwner, runWithOwner, createComponent, createRoot as root, sharedConfig, untrack, merge as mergeProps, flatten, ssrHandleError, ssrScope } from "solid-js";
1
+ export { getOwner, runWithOwner, createComponent, createRoot as root, sharedConfig, untrack, merge as mergeProps, flatten, ssrHandleError, ssrScope, NoHydration, Hydration } from "solid-js";
2
2
  export declare const effect: (fn: any, effectFn: any, options: any) => void;
3
3
  export declare const memo: (fn: any) => import("solid-js").SourceAccessor<any>;
4
+ export declare const runWithHydrationScope: (id: any, fn: any) => unknown;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @solidjs/web/frames — client half. Consume frame streams into live DOM
3
+ * boundaries. There is deliberately no server-component API here beyond
4
+ * `installServerComponents()`: `dynamic` + server functions IS the client
5
+ * surface — a server-function call whose response is a frame stream
6
+ * resolves with a stable per-call-site component.
7
+ *
8
+ * Copied next to the runtime's frame d.ts files at publish (see
9
+ * types:copy-frames), so the relative imports below resolve in-place.
10
+ */
11
+ export {
12
+ createFrame,
13
+ createFrameHost,
14
+ createFrameInsertable,
15
+ FRAME_APPLIED_EVENT
16
+ } from "./frame-client.js";
17
+ export type {
18
+ Frame,
19
+ FrameChunk,
20
+ FrameHost,
21
+ FrameHostOptions,
22
+ FrameOptions,
23
+ FrameWrite,
24
+ Slot,
25
+ SlotContext
26
+ } from "./frame-client.js";
27
+ export {
28
+ FRAME_STREAM_HEADER,
29
+ applyFrameResponse,
30
+ isFrameStreamResponse,
31
+ createServerComponentHandler
32
+ } from "./frame-transport.js";
33
+ export { createJSONDataTable } from "./serializer.js";
34
+ export type { JSONDataTable } from "./serializer.js";
35
+
36
+ import type { FrameHost } from "./frame-client.js";
37
+
38
+ /**
39
+ * The app-wide frame host (created on first use): one chunk router, with
40
+ * codec data tables rotated per response — deserializer cross-reference
41
+ * space is stream-scoped by contract.
42
+ */
43
+ export function getFrameHost(): FrameHost;
44
+
45
+ /**
46
+ * Installs the server-component transport policy on the server-function
47
+ * client: boundary identity derives from the reactive owner captured at the
48
+ * call site, frame-stream responses resolve to stable per-call-site
49
+ * components, and document-SSR boundaries adopt their server-rendered
50
+ * ranges. Call once in the client entry (the package is sideEffects:false —
51
+ * a bare import would be tree-shaken); call again to rebind a custom host.
52
+ */
53
+ export function installServerComponents(host?: FrameHost): void;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Client frame runtime — the consumer side of a frame stream. A frame
3
+ * renders server-owned content into a DOM boundary from a resident keyed
4
+ * record store: chunks are writes, not events, so application is
5
+ * prerequisite-driven and order-independent. Client-owned slot ranges
6
+ * inside the boundary are preserved across server updates — the
7
+ * version is a stale-guard only ("policy A"): newer content morphs in
8
+ * place, and teardown is `dispose()`, never a version bump.
9
+ */
10
+
11
+ /** One transport chunk of a frame stream, addressed by frame `id`. */
12
+ export type FrameChunk =
13
+ | { type: "start"; id: string; version: number }
14
+ | { type: "html"; id: string; version: number; html: string }
15
+ | { type: "fragment"; id: string; version: number; key: string; html: string }
16
+ | {
17
+ type: "reveal";
18
+ id: string;
19
+ version: number;
20
+ keys: string[];
21
+ waitForStyles?: boolean;
22
+ fallback?: boolean;
23
+ }
24
+ | {
25
+ type: "data";
26
+ id: string;
27
+ version: number;
28
+ key?: string;
29
+ node?: unknown;
30
+ initial?: boolean;
31
+ /** Eval-style hydration script — only when produced with the hydration serializer. */
32
+ payload?: string;
33
+ }
34
+ | {
35
+ type: "assets";
36
+ id: string;
37
+ version: number;
38
+ key: string;
39
+ modules?: string[];
40
+ styles?: string[];
41
+ inlineStyles?: { id: string; content?: string; attrs?: Record<string, string> }[];
42
+ }
43
+ | { type: "slot"; id: string; version: number; key: string; args: Record<string, unknown> }
44
+ | { type: "template"; id: string; version: number; key: string; html: string; fields: string[] }
45
+ | {
46
+ type: "block";
47
+ id: string;
48
+ version: number;
49
+ key: string;
50
+ template: string;
51
+ values: unknown[];
52
+ }
53
+ | { type: "complete"; id: string; version: number }
54
+ | { type: "error"; id: string; version: number; key?: string; error: unknown };
55
+
56
+ /**
57
+ * Maps a wire chunk onto resident-store record writes. `data` chunks map to
58
+ * no records — they are response-scoped and the host applies them through
59
+ * its data hook.
60
+ */
61
+ export function chunkToRecords(chunk: FrameChunk): Record<string, unknown>;
62
+
63
+ /**
64
+ * One store write applied to a frame: `r` maps record keys to values
65
+ * (`chunkToRecords` produces these from wire chunks) and `version` is the
66
+ * stream stamp — an older version than the frame's current one is ignored.
67
+ */
68
+ export interface FrameWrite {
69
+ version: number;
70
+ r: Record<string, unknown>;
71
+ }
72
+
73
+ /** Context passed to a slot callback. */
74
+ export interface SlotContext {
75
+ /**
76
+ * Register cleanup for when this occurrence's range is removed from the
77
+ * server content, or the owning frame is disposed.
78
+ */
79
+ onCleanup(fn: () => void): void;
80
+ /**
81
+ * The range's current interior — server-rendered client content on an
82
+ * adopted document-SSR boot, or the previous output on a re-call. A
83
+ * framework binding hydrates onto it and returns `undefined` to claim it
84
+ * in place (zero DOM mutation).
85
+ */
86
+ existing: ChildNode[];
87
+ }
88
+
89
+ /**
90
+ * Client content for a server-declared slot. Direct-insert occurrences
91
+ * call it with empty props; render-prop occurrences pass the occurrence's
92
+ * resolved args (primitives literal, `{$ref}` data resolved through the
93
+ * host, `{$frame}` regions as marker-range fragments). Return nodes to fill
94
+ * the range, or `undefined` to claim `ctx.existing` untouched.
95
+ */
96
+ export type Slot = (props: Record<string, unknown>, ctx: SlotContext) => Node | Node[] | undefined;
97
+
98
+ export interface Frame {
99
+ /** Merge a write into the store and flush (morph/reveal/slot sync). */
100
+ apply(write: FrameWrite): void;
101
+ /** The active version, or undefined before the first apply. */
102
+ readonly version: number | undefined;
103
+ /** Read-only view of the resident record store. */
104
+ readonly store: Readonly<Record<string, unknown>>;
105
+ /** The stream's error record, if an `error` chunk arrived. */
106
+ readonly error: unknown;
107
+ /** Whether the named fragment has been revealed into the boundary. */
108
+ isRevealed(segment: string): boolean;
109
+ /** Tear down: slot cleanups cascade, later chunks are ignored. Idempotent. */
110
+ dispose(): void;
111
+ }
112
+
113
+ /**
114
+ * Routes a flat stream of addressed chunks to frames by id, buffering chunks
115
+ * for frames that have not registered yet (only the newest version's chunks
116
+ * are kept). `data` chunks are response-scoped and go to `applyData`.
117
+ *
118
+ * An id may have several frames (the same server component mounted more
119
+ * than once): chunks fan out to all of them, and a frame registering after
120
+ * delivery is seeded from a sibling's store.
121
+ */
122
+ export interface FrameHost {
123
+ register(id: string, frame: Frame): void;
124
+ /** Remove one frame (or all frames of the id when `frame` is omitted). */
125
+ unregister(id: string, frame?: Frame): void;
126
+ apply(chunk: FrameChunk): void;
127
+ /** The first registered frame under the id, if any. */
128
+ get(id: string): Frame | undefined;
129
+ serialize(value: unknown): { $ref: string };
130
+ /** `frameId` is the resolving frame's id — route to its stream's table. */
131
+ resolve(ref: { $ref: string }, frameId?: string): unknown;
132
+ }
133
+
134
+ /**
135
+ * The bubbling DOM event (`"frame:applied"`) a frame dispatches from its
136
+ * parent element whenever server content lands in the document — root
137
+ * materialize/morph, segment reveal, fallback materialization — with
138
+ * `detail: { id, version, reason }`. One document-level listener sees every
139
+ * boundary (nested region frames dispatch too); use it to re-apply
140
+ * client-owned decorations on server-owned markup (router affordance
141
+ * reflection, e.g. `aria-current`) without a MutationObserver.
142
+ */
143
+ export const FRAME_APPLIED_EVENT: "frame:applied";
144
+
145
+ /** Options for `createFrameHost`. */
146
+ export interface FrameHostOptions {
147
+ /**
148
+ * Backs `{$ref}` slot args (typically a codec data table's `resolve`).
149
+ * `frameId` identifies the resolving frame — data tables are
150
+ * response-scoped, so multi-stream hosts route by it (nested region ids
151
+ * prefix-match their root).
152
+ */
153
+ resolve?(ref: { $ref: string }, frameId?: string): unknown;
154
+ /** Test/host-side counterpart of `resolve`. */
155
+ serialize?(value: unknown): { $ref: string };
156
+ /**
157
+ * Receives each `data` chunk whole. Wire a codec table:
158
+ * `applyData: c => table.apply(c)` (see `createJSONDataTable`).
159
+ */
160
+ applyData?(chunk: Extract<FrameChunk, { type: "data" }>): void;
161
+ }
162
+
163
+ export function createFrameHost(options?: FrameHostOptions): FrameHost;
164
+
165
+ /** Options for `createFrame` / `createFrameInsertable`. */
166
+ export interface FrameOptions {
167
+ /** Register with this host under `id`, receiving routed/buffered chunks. */
168
+ host?: FrameHost;
169
+ id?: string;
170
+ /** Client content keyed by prop name (occurrences resolve by prop). */
171
+ slots?: Record<string, Slot>;
172
+ /**
173
+ * Adopt existing server-rendered DOM: the first apply morphs against it,
174
+ * and slots sync immediately (hydration attach) — a document-SSR boot
175
+ * needs no chunk.
176
+ */
177
+ adopt?: boolean;
178
+ /** Called after each apply flush (tests/telemetry). */
179
+ onApply?(info: { version: number; reason: "materialize" | "morph" | "reveal" }): void;
180
+ /**
181
+ * Wraps element-claim sweeps (`a[href]`/`form[action]` in materialized
182
+ * server content — and only those) so claim consumers register their
183
+ * per-element cleanup against the boundary's reactive owner, e.g.
184
+ * `fn => runWithOwner(owner, fn)`. Nested region frames inherit it.
185
+ * Without it, sweeps run under whatever owner is current (none, for
186
+ * streamed chunks).
187
+ */
188
+ ownerScope?<T>(fn: () => T): T;
189
+ }
190
+
191
+ /** A frame rendering into an element boundary. */
192
+ export function createFrame(boundary: Element, options?: FrameOptions): Frame;
193
+
194
+ /**
195
+ * A branded frame-insertable value: the client runtime's `insert` recognizes
196
+ * it (registered `$$FRAME` symbol) and calls the mount handler the value
197
+ * carries — a comment range is established at the insertion point and a
198
+ * host-registered frame binds to it. One static mount per value; lifecycle
199
+ * belongs to the creator via `dispose()` (register it with your owner's
200
+ * cleanup).
201
+ */
202
+ export function createFrameInsertable(options: FrameOptions): {
203
+ readonly frame: Frame | null;
204
+ dispose(): void;
205
+ };