@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.
- package/dist/dev.cjs +25 -1
- package/dist/dev.js +25 -2
- package/dist/server.cjs +79 -33
- package/dist/server.js +79 -34
- package/dist/web.cjs +25 -1
- package/dist/web.js +25 -2
- package/frames/dist/client.cjs +1442 -0
- package/frames/dist/client.js +1430 -0
- package/frames/dist/server.cjs +1705 -0
- package/frames/dist/server.js +1694 -0
- package/frames/package.json +30 -0
- package/package.json +78 -5
- package/serialization/dist/serialization.cjs +83 -0
- package/serialization/dist/serialization.js +82 -1
- package/serialization/types/index.d.ts +12 -0
- package/serialization/types-cjs/index.d.cts +12 -0
- package/server-functions/dist/client.cjs +82 -55
- package/server-functions/dist/client.js +83 -56
- package/server-functions/dist/server.cjs +20 -3
- package/server-functions/dist/server.js +20 -3
- package/types/client.d.ts +8 -0
- package/types/core.d.ts +2 -1
- package/types/frames/client.d.ts +53 -0
- package/types/frames/frame-client.d.ts +205 -0
- package/types/frames/frame-sink.d.ts +145 -0
- package/types/frames/frame-transport.d.ts +105 -0
- package/types/frames/serializer.d.ts +151 -0
- package/types/frames/server.d.ts +21 -0
- package/types/serializer.d.ts +12 -0
- package/types/server-functions/client.d.ts +24 -0
- package/types/server.d.ts +2 -0
- package/types-cjs/client.d.cts +8 -0
- package/types-cjs/core.d.cts +2 -1
- package/types-cjs/frames/client.d.cts +53 -0
- package/types-cjs/frames/frame-client.d.cts +205 -0
- package/types-cjs/frames/frame-sink.d.cts +145 -0
- package/types-cjs/frames/frame-transport.d.cts +105 -0
- package/types-cjs/frames/serializer.d.cts +151 -0
- package/types-cjs/frames/server.d.cts +21 -0
- package/types-cjs/serializer.d.cts +12 -0
- package/types-cjs/server-functions/client.d.cts +24 -0
- package/types-cjs/server.d.cts +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fromCrossJSON, Feature
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|