@fictjs/ssr 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -3
- package/dist/chunk-DI4PFT6R.js +11 -0
- package/dist/fict-stream-runtime.js +1 -0
- package/dist/index.cjs +406 -80
- package/dist/index.d.cts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +396 -71
- package/dist/stream-runtime.cjs +36 -0
- package/dist/stream-runtime.d.cts +16 -0
- package/dist/stream-runtime.d.ts +16 -0
- package/dist/stream-runtime.js +8 -0
- package/package.json +14 -6
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,11 @@ __export(index_exports, {
|
|
|
30
30
|
});
|
|
31
31
|
module.exports = __toCommonJS(index_exports);
|
|
32
32
|
var import_runtime = require("@fictjs/runtime");
|
|
33
|
-
var
|
|
33
|
+
var import_internal2 = require("@fictjs/runtime/internal");
|
|
34
34
|
var import_linkedom = require("linkedom");
|
|
35
35
|
|
|
36
36
|
// src/globals.ts
|
|
37
|
+
var import_internal = require("@fictjs/runtime/internal");
|
|
37
38
|
function installGlobals(window, document) {
|
|
38
39
|
const win = window;
|
|
39
40
|
const required = {
|
|
@@ -83,6 +84,14 @@ function installManifest(manifest) {
|
|
|
83
84
|
} else {
|
|
84
85
|
resolved = manifest;
|
|
85
86
|
}
|
|
87
|
+
const session = (0, import_internal.__fictGetCurrentSSRSession)();
|
|
88
|
+
if (session) {
|
|
89
|
+
const previous = session.manifest;
|
|
90
|
+
session.manifest = resolved;
|
|
91
|
+
return () => {
|
|
92
|
+
session.manifest = previous;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
86
95
|
const key = "__FICT_MANIFEST__";
|
|
87
96
|
const snapshot = {
|
|
88
97
|
exists: Object.prototype.hasOwnProperty.call(globalThis, key),
|
|
@@ -146,12 +155,33 @@ function getNodeRequire() {
|
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
// src/stream-bridge.ts
|
|
149
|
-
function createQueuedTextStream() {
|
|
158
|
+
function createQueuedTextStream(options = {}) {
|
|
150
159
|
const encoder = new TextEncoder();
|
|
151
160
|
const queue = [];
|
|
152
161
|
let controller = null;
|
|
153
162
|
let closed = false;
|
|
154
163
|
let aborted;
|
|
164
|
+
const readyResolvers = [];
|
|
165
|
+
const resolveReady = () => {
|
|
166
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
167
|
+
while (readyResolvers.length > 0) {
|
|
168
|
+
readyResolvers.shift()?.();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const drainReady = () => {
|
|
172
|
+
while (readyResolvers.length > 0) {
|
|
173
|
+
readyResolvers.shift()?.();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const abortQueue = (reason, notifyController = true) => {
|
|
177
|
+
if (closed || aborted !== void 0) return;
|
|
178
|
+
aborted = reason ?? new Error("Stream aborted");
|
|
179
|
+
queue.length = 0;
|
|
180
|
+
drainReady();
|
|
181
|
+
if (notifyController) {
|
|
182
|
+
controller?.error(aborted);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
155
185
|
const stream = new ReadableStream({
|
|
156
186
|
start(ctrl) {
|
|
157
187
|
controller = ctrl;
|
|
@@ -166,6 +196,13 @@ function createQueuedTextStream() {
|
|
|
166
196
|
if (closed) {
|
|
167
197
|
ctrl.close();
|
|
168
198
|
}
|
|
199
|
+
},
|
|
200
|
+
pull() {
|
|
201
|
+
resolveReady();
|
|
202
|
+
},
|
|
203
|
+
cancel(reason) {
|
|
204
|
+
abortQueue(reason, false);
|
|
205
|
+
options.onCancel?.(reason);
|
|
169
206
|
}
|
|
170
207
|
});
|
|
171
208
|
const writer = {
|
|
@@ -174,19 +211,24 @@ function createQueuedTextStream() {
|
|
|
174
211
|
const data = encoder.encode(chunk);
|
|
175
212
|
if (controller) {
|
|
176
213
|
controller.enqueue(data);
|
|
214
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
readyResolvers.push(resolve);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
177
219
|
} else {
|
|
178
220
|
queue.push(data);
|
|
179
221
|
}
|
|
222
|
+
return void 0;
|
|
180
223
|
},
|
|
181
224
|
close() {
|
|
182
225
|
if (closed || aborted !== void 0) return;
|
|
183
226
|
closed = true;
|
|
227
|
+
drainReady();
|
|
184
228
|
controller?.close();
|
|
185
229
|
},
|
|
186
230
|
abort(reason) {
|
|
187
|
-
|
|
188
|
-
aborted = reason ?? new Error("Stream aborted");
|
|
189
|
-
controller?.error(aborted);
|
|
231
|
+
abortQueue(reason);
|
|
190
232
|
}
|
|
191
233
|
};
|
|
192
234
|
return { stream, writer };
|
|
@@ -200,9 +242,20 @@ function createPipeBridge() {
|
|
|
200
242
|
let abortReason = null;
|
|
201
243
|
const safeWrite = (target, chunk) => {
|
|
202
244
|
try {
|
|
203
|
-
target.write(chunk);
|
|
245
|
+
const ready = target.write(chunk);
|
|
246
|
+
if (ready === false) {
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
const withOnce = target;
|
|
249
|
+
if (typeof withOnce.once === "function") {
|
|
250
|
+
withOnce.once("drain", resolve);
|
|
251
|
+
} else {
|
|
252
|
+
resolve();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
204
256
|
} catch {
|
|
205
257
|
}
|
|
258
|
+
return void 0;
|
|
206
259
|
};
|
|
207
260
|
const safeEnd = (target) => {
|
|
208
261
|
try {
|
|
@@ -242,9 +295,12 @@ function createPipeBridge() {
|
|
|
242
295
|
buffer.push(chunk);
|
|
243
296
|
return;
|
|
244
297
|
}
|
|
298
|
+
const pending = [];
|
|
245
299
|
for (const target of targets) {
|
|
246
|
-
safeWrite(target, chunk);
|
|
300
|
+
const result = safeWrite(target, chunk);
|
|
301
|
+
if (result) pending.push(result);
|
|
247
302
|
}
|
|
303
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
248
304
|
},
|
|
249
305
|
close() {
|
|
250
306
|
if (state !== "open") return;
|
|
@@ -274,22 +330,78 @@ function createNodePipeBridge() {
|
|
|
274
330
|
const streamModule = nodeRequire("node:stream");
|
|
275
331
|
if (!streamModule.PassThrough) return null;
|
|
276
332
|
const passThrough = new streamModule.PassThrough();
|
|
333
|
+
const buffer = [];
|
|
334
|
+
let piped = false;
|
|
335
|
+
let state = "open";
|
|
336
|
+
let abortReason = null;
|
|
337
|
+
const writeToPassThrough = (chunk) => {
|
|
338
|
+
if (passThrough.write(chunk) === false) {
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
const withOnce = passThrough;
|
|
341
|
+
if (typeof withOnce.once === "function") {
|
|
342
|
+
withOnce.once("drain", resolve);
|
|
343
|
+
} else {
|
|
344
|
+
resolve();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return void 0;
|
|
349
|
+
};
|
|
350
|
+
const flushBuffer = () => {
|
|
351
|
+
if (buffer.length === 0) return void 0;
|
|
352
|
+
const pending = [];
|
|
353
|
+
for (const chunk of buffer) {
|
|
354
|
+
const result = writeToPassThrough(chunk);
|
|
355
|
+
if (result) pending.push(result);
|
|
356
|
+
}
|
|
357
|
+
buffer.length = 0;
|
|
358
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
359
|
+
};
|
|
360
|
+
const destroyPassThrough = (error) => {
|
|
361
|
+
if (typeof passThrough.destroy === "function") {
|
|
362
|
+
passThrough.destroy(error);
|
|
363
|
+
} else {
|
|
364
|
+
passThrough.end();
|
|
365
|
+
}
|
|
366
|
+
};
|
|
277
367
|
return {
|
|
278
368
|
pipe(writable) {
|
|
369
|
+
piped = true;
|
|
279
370
|
passThrough.pipe(writable);
|
|
371
|
+
if (state === "aborted") {
|
|
372
|
+
destroyPassThrough(abortReason ?? new Error("Stream aborted"));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const flushed = flushBuffer();
|
|
376
|
+
if (state === "closed") {
|
|
377
|
+
if (flushed) {
|
|
378
|
+
void flushed.then(() => passThrough.end());
|
|
379
|
+
} else {
|
|
380
|
+
passThrough.end();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
280
383
|
},
|
|
281
384
|
write(chunk) {
|
|
282
|
-
|
|
385
|
+
if (state !== "open") return;
|
|
386
|
+
if (!piped) {
|
|
387
|
+
buffer.push(chunk);
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
return writeToPassThrough(chunk);
|
|
283
391
|
},
|
|
284
392
|
close() {
|
|
393
|
+
if (state !== "open") return;
|
|
394
|
+
state = "closed";
|
|
395
|
+
if (!piped) return;
|
|
285
396
|
passThrough.end();
|
|
286
397
|
},
|
|
287
398
|
abort(reason) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
399
|
+
if (state !== "open") return;
|
|
400
|
+
state = "aborted";
|
|
401
|
+
abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
|
|
402
|
+
buffer.length = 0;
|
|
403
|
+
if (piped) {
|
|
404
|
+
destroyPassThrough(abortReason);
|
|
293
405
|
}
|
|
294
406
|
}
|
|
295
407
|
};
|
|
@@ -310,6 +422,13 @@ function getNodeRequire2() {
|
|
|
310
422
|
}
|
|
311
423
|
}
|
|
312
424
|
|
|
425
|
+
// src/stream-runtime.ts
|
|
426
|
+
function createStreamRuntimeCode(options = {}) {
|
|
427
|
+
const observerMode = options.observerMode ?? true;
|
|
428
|
+
return `(function(){if(window.__FICT_STREAM)return;var cache=new Map();function find(id){var hit=cache.get(id);if(hit)return hit;var start=null,end=null;var w=document.createTreeWalker(document,NodeFilter.SHOW_COMMENT);while(w.nextNode()){var n=w.currentNode;var d=n.data;if(d==="fict:suspense-start:"+id)start=n;else if(d==="fict:suspense-end:"+id)end=n;if(start&&end)break;}if(start&&end){hit={start:start,end:end};cache.set(id,hit);}return hit;}function apply(id){var tpl=document.querySelector('template[data-fict-suspense="' + id + '"]');if(!tpl)return;var b=find(id);if(!b)return;var node=b.start.nextSibling;while(node&&node!==b.end){var next=node.nextSibling;node.parentNode&&node.parentNode.removeChild(node);node=next;}b.end.parentNode&&b.end.parentNode.insertBefore(tpl.content,b.end);tpl.parentNode&&tpl.parentNode.removeChild(tpl);}window.__FICT_STREAM={apply:apply};` + (observerMode ? 'function scan(root){var list=(root&&root.querySelectorAll?root:document).querySelectorAll("template[data-fict-suspense]");for(var i=0;i<list.length;i++){apply(list[i].getAttribute("data-fict-suspense"));}}if(typeof MutationObserver==="function"){new MutationObserver(function(muts){for(var i=0;i<muts.length;i++){for(var j=0;j<muts[i].addedNodes.length;j++){var n=muts[i].addedNodes[j];if(n.nodeType===1){if(n.matches&&n.matches("template[data-fict-suspense]"))apply(n.getAttribute("data-fict-suspense"));scan(n);}}}}).observe(document.documentElement||document,{childList:true,subtree:true});}if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",function(){scan(document);},{once:true});}else{scan(document);}' : "") + "})();";
|
|
429
|
+
}
|
|
430
|
+
var FICT_STREAM_RUNTIME_CODE = createStreamRuntimeCode({ observerMode: true });
|
|
431
|
+
|
|
313
432
|
// src/index.ts
|
|
314
433
|
var DEFAULT_HTML = "<!doctype html><html><head></head><body></body></html>";
|
|
315
434
|
function createSSRDocument(html = DEFAULT_HTML) {
|
|
@@ -321,8 +440,12 @@ function createSSRDocument(html = DEFAULT_HTML) {
|
|
|
321
440
|
return { window, document };
|
|
322
441
|
}
|
|
323
442
|
function renderToDocument(view, options = {}) {
|
|
443
|
+
const session = (0, import_internal2.__fictCreateSSRSession)();
|
|
444
|
+
return (0, import_internal2.__fictRunWithSSRSession)(session, () => renderToDocumentInSession(view, options));
|
|
445
|
+
}
|
|
446
|
+
function renderToDocumentInSession(view, options) {
|
|
324
447
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
325
|
-
(0,
|
|
448
|
+
(0, import_internal2.__fictEnableSSR)();
|
|
326
449
|
let dom;
|
|
327
450
|
let restoreGlobals2 = () => {
|
|
328
451
|
};
|
|
@@ -334,23 +457,23 @@ function renderToDocument(view, options = {}) {
|
|
|
334
457
|
try {
|
|
335
458
|
dom = resolveDom(options);
|
|
336
459
|
const { document, window } = dom;
|
|
337
|
-
const shouldExpose = options.exposeGlobals
|
|
460
|
+
const shouldExpose = options.exposeGlobals === true;
|
|
338
461
|
restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
|
|
339
462
|
};
|
|
340
463
|
restoreManifest = installManifest(options.manifest);
|
|
341
464
|
container = resolveContainer(document, options);
|
|
342
465
|
teardown = (0, import_runtime.render)(view, container);
|
|
343
466
|
if (includeSnapshot) {
|
|
344
|
-
const state = (0,
|
|
467
|
+
const state = (0, import_internal2.__fictSerializeSSRState)();
|
|
345
468
|
injectSnapshot(document, container, state, options);
|
|
346
469
|
}
|
|
347
470
|
} catch (error) {
|
|
348
|
-
(0,
|
|
471
|
+
(0, import_internal2.__fictDisableSSR)();
|
|
349
472
|
restoreGlobals2();
|
|
350
473
|
restoreManifest();
|
|
351
474
|
throw error;
|
|
352
475
|
}
|
|
353
|
-
(0,
|
|
476
|
+
(0, import_internal2.__fictDisableSSR)();
|
|
354
477
|
const html = serializeOutput(dom.document, container, options);
|
|
355
478
|
const dispose = () => {
|
|
356
479
|
try {
|
|
@@ -374,6 +497,37 @@ async function renderToStringAsync(view, options = {}) {
|
|
|
374
497
|
function renderToStream(view, options = {}) {
|
|
375
498
|
const encoder = new TextEncoder();
|
|
376
499
|
let controller = null;
|
|
500
|
+
let abortRender = null;
|
|
501
|
+
const readyResolvers = [];
|
|
502
|
+
const drainBackpressure = () => {
|
|
503
|
+
while (readyResolvers.length > 0) {
|
|
504
|
+
readyResolvers.shift()?.();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const resolveBackpressure = () => {
|
|
508
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
509
|
+
drainBackpressure();
|
|
510
|
+
};
|
|
511
|
+
const closeController = () => {
|
|
512
|
+
abortRender = null;
|
|
513
|
+
drainBackpressure();
|
|
514
|
+
if (!controller) return;
|
|
515
|
+
try {
|
|
516
|
+
controller.close();
|
|
517
|
+
} finally {
|
|
518
|
+
controller = null;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const errorController = (reason) => {
|
|
522
|
+
abortRender = null;
|
|
523
|
+
drainBackpressure();
|
|
524
|
+
if (!controller) return;
|
|
525
|
+
try {
|
|
526
|
+
controller.error(reason);
|
|
527
|
+
} finally {
|
|
528
|
+
controller = null;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
377
531
|
const stream = new ReadableStream({
|
|
378
532
|
start(ctrl) {
|
|
379
533
|
controller = ctrl;
|
|
@@ -381,15 +535,32 @@ function renderToStream(view, options = {}) {
|
|
|
381
535
|
write(chunk) {
|
|
382
536
|
if (!controller) return;
|
|
383
537
|
controller.enqueue(encoder.encode(chunk));
|
|
538
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
539
|
+
return new Promise((resolve) => {
|
|
540
|
+
readyResolvers.push(resolve);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return void 0;
|
|
384
544
|
},
|
|
385
545
|
close() {
|
|
386
|
-
|
|
546
|
+
closeController();
|
|
387
547
|
},
|
|
388
548
|
abort(reason) {
|
|
389
|
-
|
|
549
|
+
errorController(reason);
|
|
390
550
|
}
|
|
391
551
|
});
|
|
552
|
+
abortRender = started.abort;
|
|
553
|
+
started.shellReady.catch(() => void 0);
|
|
392
554
|
started.allReady.catch(() => void 0);
|
|
555
|
+
},
|
|
556
|
+
pull() {
|
|
557
|
+
resolveBackpressure();
|
|
558
|
+
},
|
|
559
|
+
cancel(reason) {
|
|
560
|
+
const abort = abortRender;
|
|
561
|
+
controller = null;
|
|
562
|
+
drainBackpressure();
|
|
563
|
+
abort?.(reason ?? new Error("Stream canceled"));
|
|
393
564
|
}
|
|
394
565
|
});
|
|
395
566
|
return stream;
|
|
@@ -398,7 +569,7 @@ function renderToPipeableStream(view, options = {}) {
|
|
|
398
569
|
const bridge = createPipeBridge();
|
|
399
570
|
const { shellReady, allReady, abort } = startStreamingRender(view, options, {
|
|
400
571
|
write(chunk) {
|
|
401
|
-
bridge.write(chunk);
|
|
572
|
+
return bridge.write(chunk);
|
|
402
573
|
},
|
|
403
574
|
close() {
|
|
404
575
|
bridge.close();
|
|
@@ -424,7 +595,12 @@ function renderToPartial(view, options = {}) {
|
|
|
424
595
|
};
|
|
425
596
|
let shell = "";
|
|
426
597
|
let shellPhase = true;
|
|
427
|
-
|
|
598
|
+
let abortPartial = null;
|
|
599
|
+
const queued = createQueuedTextStream({
|
|
600
|
+
onCancel(reason) {
|
|
601
|
+
abortPartial?.(reason ?? new Error("Stream canceled"));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
428
604
|
const { shellReady, allReady, abort } = startStreamingRender(
|
|
429
605
|
view,
|
|
430
606
|
partialOptions,
|
|
@@ -434,7 +610,7 @@ function renderToPartial(view, options = {}) {
|
|
|
434
610
|
shell += chunk;
|
|
435
611
|
return;
|
|
436
612
|
}
|
|
437
|
-
queued.writer.write(chunk);
|
|
613
|
+
return queued.writer.write(chunk);
|
|
438
614
|
},
|
|
439
615
|
close() {
|
|
440
616
|
queued.writer.close();
|
|
@@ -450,6 +626,7 @@ function renderToPartial(view, options = {}) {
|
|
|
450
626
|
}
|
|
451
627
|
}
|
|
452
628
|
);
|
|
629
|
+
abortPartial = abort;
|
|
453
630
|
return {
|
|
454
631
|
shell,
|
|
455
632
|
stream: queued.stream,
|
|
@@ -479,17 +656,39 @@ function resolveDom(options) {
|
|
|
479
656
|
}
|
|
480
657
|
return createSSRDocument(options.html);
|
|
481
658
|
}
|
|
659
|
+
function isPromiseLike(value) {
|
|
660
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
661
|
+
}
|
|
482
662
|
function startStreamingRender(view, options, writer, control = {}) {
|
|
663
|
+
const session = (0, import_internal2.__fictCreateSSRSession)();
|
|
664
|
+
return (0, import_internal2.__fictRunWithSSRSession)(
|
|
665
|
+
session,
|
|
666
|
+
() => startStreamingRenderInSession(session, view, options, writer, control)
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
function startStreamingRenderInSession(session, view, options, writer, control = {}) {
|
|
670
|
+
const runInSession = (fn) => (0, import_internal2.__fictRunWithSSRSession)(session, fn);
|
|
483
671
|
const resolvedOptions = {
|
|
484
672
|
...options,
|
|
485
673
|
// Streaming requires a real document; default to fullDocument when unspecified.
|
|
486
674
|
fullDocument: options.fullDocument ?? true
|
|
487
675
|
};
|
|
488
676
|
let resolveShell;
|
|
677
|
+
let rejectShell;
|
|
489
678
|
let resolveAll;
|
|
490
679
|
let rejectAll;
|
|
491
|
-
|
|
492
|
-
|
|
680
|
+
let shellSettled = false;
|
|
681
|
+
const shellReady = new Promise((res, rej) => {
|
|
682
|
+
resolveShell = () => {
|
|
683
|
+
if (shellSettled) return;
|
|
684
|
+
shellSettled = true;
|
|
685
|
+
res();
|
|
686
|
+
};
|
|
687
|
+
rejectShell = (err) => {
|
|
688
|
+
if (shellSettled) return;
|
|
689
|
+
shellSettled = true;
|
|
690
|
+
rej(err);
|
|
691
|
+
};
|
|
493
692
|
});
|
|
494
693
|
const allReady = new Promise((res, rej) => {
|
|
495
694
|
resolveAll = res;
|
|
@@ -507,44 +706,129 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
507
706
|
let tailHtml = "";
|
|
508
707
|
let wroteShell = false;
|
|
509
708
|
let shellCarriesTail = false;
|
|
709
|
+
let writeChain = null;
|
|
710
|
+
let writeFailed = false;
|
|
711
|
+
let removeAbortListener = () => {
|
|
712
|
+
};
|
|
510
713
|
const mode = options.mode ?? "shell";
|
|
511
714
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
512
715
|
const sentScopes = /* @__PURE__ */ new Set();
|
|
513
716
|
const boundaryMap = /* @__PURE__ */ new Map();
|
|
514
717
|
let boundaryId = 0;
|
|
515
718
|
let pendingCount = 0;
|
|
516
|
-
const
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
719
|
+
const handleWriteError = (error) => {
|
|
720
|
+
if (writeFailed) return;
|
|
721
|
+
writeFailed = true;
|
|
722
|
+
options.onError?.(error);
|
|
723
|
+
writer.abort(error);
|
|
724
|
+
cleanup();
|
|
725
|
+
rejectShell(error);
|
|
726
|
+
rejectAll(error);
|
|
727
|
+
};
|
|
728
|
+
const enqueueWrite = (chunk) => {
|
|
729
|
+
if (writeFailed) return;
|
|
730
|
+
const trackWrite = (promise) => {
|
|
731
|
+
const tracked = promise.then(
|
|
732
|
+
() => {
|
|
733
|
+
if (writeChain === tracked) {
|
|
734
|
+
writeChain = null;
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
(error) => {
|
|
738
|
+
if (writeChain === tracked) {
|
|
739
|
+
writeChain = null;
|
|
740
|
+
}
|
|
741
|
+
handleWriteError(error);
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
writeChain = tracked;
|
|
745
|
+
};
|
|
746
|
+
const writeAsync = () => Promise.resolve(writer.write(chunk)).then(() => void 0);
|
|
747
|
+
if (writeChain) {
|
|
748
|
+
trackWrite(writeChain.then(writeAsync));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
const result = writer.write(chunk);
|
|
753
|
+
if (isPromiseLike(result)) {
|
|
754
|
+
trackWrite(
|
|
755
|
+
result.then(
|
|
756
|
+
() => void 0,
|
|
757
|
+
(error) => {
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
)
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
} catch (error) {
|
|
764
|
+
handleWriteError(error);
|
|
527
765
|
}
|
|
528
|
-
|
|
529
|
-
|
|
766
|
+
};
|
|
767
|
+
const afterWrites = (fn) => {
|
|
768
|
+
const pending = writeChain;
|
|
769
|
+
if (!pending) {
|
|
770
|
+
if (!writeFailed) {
|
|
771
|
+
fn();
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
530
774
|
}
|
|
775
|
+
void pending.then(() => {
|
|
776
|
+
if (!writeFailed) {
|
|
777
|
+
fn();
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
};
|
|
781
|
+
const markShellReady = () => {
|
|
782
|
+
afterWrites(() => {
|
|
783
|
+
control.onShellFlushed?.();
|
|
784
|
+
resolveShell();
|
|
785
|
+
options.onShellReady?.();
|
|
786
|
+
});
|
|
787
|
+
};
|
|
788
|
+
const writeSnapshotForScopes = (scopeIds) => {
|
|
789
|
+
runInSession(() => {
|
|
790
|
+
if (!includeSnapshot || scopeIds.length === 0) return;
|
|
791
|
+
const registry = (0, import_internal2.__fictGetScopeRegistry)();
|
|
792
|
+
const pending = scopeIds.filter((id) => registry.has(id) && !sentScopes.has(id));
|
|
793
|
+
if (pending.length === 0) return;
|
|
794
|
+
const snapshot = (0, import_internal2.__fictSerializeSSRStateForScopes)(pending);
|
|
795
|
+
const ids = Object.keys(snapshot.scopes);
|
|
796
|
+
if (ids.length === 0) return;
|
|
797
|
+
const chunk = buildIncrementalSnapshotChunk(snapshot, resolvedOptions);
|
|
798
|
+
if (chunk) {
|
|
799
|
+
enqueueWrite(chunk);
|
|
800
|
+
}
|
|
801
|
+
for (const id of ids) {
|
|
802
|
+
sentScopes.add(id);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
531
805
|
};
|
|
532
806
|
const writeSnapshotForBoundary = (boundary) => {
|
|
533
|
-
|
|
534
|
-
|
|
807
|
+
runInSession(() => {
|
|
808
|
+
const scopes = (0, import_internal2.__fictGetScopesForBoundary)(boundary);
|
|
809
|
+
writeSnapshotForScopes(scopes);
|
|
810
|
+
});
|
|
535
811
|
};
|
|
536
812
|
const writeRemainingSnapshots = () => {
|
|
537
|
-
|
|
538
|
-
|
|
813
|
+
runInSession(() => {
|
|
814
|
+
const scopes = Array.from((0, import_internal2.__fictGetScopeRegistry)().keys());
|
|
815
|
+
writeSnapshotForScopes(scopes);
|
|
816
|
+
});
|
|
539
817
|
};
|
|
540
818
|
const cleanup = () => {
|
|
541
|
-
(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
819
|
+
removeAbortListener();
|
|
820
|
+
removeAbortListener = () => {
|
|
821
|
+
};
|
|
822
|
+
runInSession(() => {
|
|
823
|
+
(0, import_internal2.__fictSetSSRStreamHooks)(null);
|
|
824
|
+
(0, import_internal2.__fictDisableSSR)();
|
|
825
|
+
});
|
|
545
826
|
try {
|
|
546
827
|
teardown();
|
|
547
828
|
} catch {
|
|
829
|
+
} finally {
|
|
830
|
+
restoreGlobals2();
|
|
831
|
+
restoreManifest();
|
|
548
832
|
}
|
|
549
833
|
};
|
|
550
834
|
const finalize = () => {
|
|
@@ -552,27 +836,31 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
552
836
|
closed = true;
|
|
553
837
|
if (mode === "all" && dom && container && !wroteShell) {
|
|
554
838
|
if (includeSnapshot) {
|
|
555
|
-
const snapshot = (0,
|
|
839
|
+
const snapshot = (0, import_internal2.__fictSerializeSSRState)();
|
|
556
840
|
injectSnapshot(dom.document, container, snapshot, resolvedOptions);
|
|
557
841
|
}
|
|
558
842
|
const fullHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
843
|
+
enqueueWrite(fullHtml);
|
|
844
|
+
afterWrites(() => {
|
|
845
|
+
writer.close();
|
|
846
|
+
cleanup();
|
|
847
|
+
resolveShell();
|
|
848
|
+
resolveAll();
|
|
849
|
+
options.onShellReady?.();
|
|
850
|
+
options.onAllReady?.();
|
|
851
|
+
});
|
|
566
852
|
return;
|
|
567
853
|
}
|
|
568
854
|
writeRemainingSnapshots();
|
|
569
855
|
if (tailHtml) {
|
|
570
|
-
|
|
856
|
+
enqueueWrite(tailHtml);
|
|
571
857
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
858
|
+
afterWrites(() => {
|
|
859
|
+
writer.close();
|
|
860
|
+
cleanup();
|
|
861
|
+
resolveAll();
|
|
862
|
+
options.onAllReady?.();
|
|
863
|
+
});
|
|
576
864
|
};
|
|
577
865
|
const maybeFinalize = () => {
|
|
578
866
|
if (pendingCount === 0) {
|
|
@@ -602,7 +890,7 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
602
890
|
writeSnapshotForBoundary(id);
|
|
603
891
|
if (dom) {
|
|
604
892
|
const html = serializeBetween(dom.document, entry.start, entry.end);
|
|
605
|
-
|
|
893
|
+
enqueueWrite(buildPatchChunk(id, html, resolvedOptions));
|
|
606
894
|
}
|
|
607
895
|
}
|
|
608
896
|
maybeFinalize();
|
|
@@ -615,22 +903,28 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
615
903
|
const abort = (reason) => {
|
|
616
904
|
if (closed) return;
|
|
617
905
|
closed = true;
|
|
906
|
+
writeFailed = true;
|
|
618
907
|
writer.abort(reason);
|
|
619
908
|
cleanup();
|
|
620
|
-
|
|
909
|
+
const abortReason = reason ?? new Error("Stream aborted");
|
|
910
|
+
rejectShell(abortReason);
|
|
911
|
+
rejectAll(abortReason);
|
|
621
912
|
};
|
|
622
913
|
if (options.signal) {
|
|
623
914
|
if (options.signal.aborted) {
|
|
624
915
|
abort(options.signal.reason);
|
|
916
|
+
return { shellReady, allReady, abort };
|
|
625
917
|
} else {
|
|
626
|
-
|
|
918
|
+
const onAbort = () => abort(options.signal?.reason);
|
|
919
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
920
|
+
removeAbortListener = () => options.signal?.removeEventListener("abort", onAbort);
|
|
627
921
|
}
|
|
628
922
|
}
|
|
629
923
|
try {
|
|
630
|
-
(0,
|
|
631
|
-
(0,
|
|
924
|
+
(0, import_internal2.__fictEnableSSR)();
|
|
925
|
+
(0, import_internal2.__fictSetSSRStreamHooks)(hooks);
|
|
632
926
|
dom = resolveDom(resolvedOptions);
|
|
633
|
-
restoreGlobals2 = resolvedOptions.exposeGlobals
|
|
927
|
+
restoreGlobals2 = resolvedOptions.exposeGlobals === true ? installGlobals(dom.window, dom.document) : () => {
|
|
634
928
|
};
|
|
635
929
|
restoreManifest = installManifest(resolvedOptions.manifest);
|
|
636
930
|
container = resolveContainer(dom.document, resolvedOptions);
|
|
@@ -642,33 +936,31 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
642
936
|
return { shellReady, allReady, abort };
|
|
643
937
|
}
|
|
644
938
|
const shellHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
645
|
-
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript() : "";
|
|
939
|
+
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript(resolvedOptions) : "";
|
|
646
940
|
if (resolvedOptions.fullDocument) {
|
|
647
941
|
const split = splitDocumentHtml(shellHtml);
|
|
648
942
|
if (!split) {
|
|
649
943
|
throw new Error("[fict/ssr] Failed to locate </body> for streaming output.");
|
|
650
944
|
}
|
|
651
945
|
if (control.includeTailInShell) {
|
|
652
|
-
|
|
946
|
+
enqueueWrite(split.head + streamRuntime);
|
|
653
947
|
tailHtml = split.tail;
|
|
654
948
|
shellCarriesTail = true;
|
|
655
949
|
} else {
|
|
656
|
-
|
|
950
|
+
enqueueWrite(split.head + streamRuntime);
|
|
657
951
|
tailHtml = split.tail;
|
|
658
952
|
}
|
|
659
953
|
} else {
|
|
660
|
-
|
|
954
|
+
enqueueWrite(shellHtml + streamRuntime);
|
|
661
955
|
}
|
|
662
956
|
wroteShell = true;
|
|
663
|
-
writeSnapshotForScopes(Array.from((0,
|
|
957
|
+
writeSnapshotForScopes(Array.from((0, import_internal2.__fictGetScopeRegistry)().keys()));
|
|
664
958
|
if (shellCarriesTail && tailHtml) {
|
|
665
|
-
|
|
959
|
+
enqueueWrite(tailHtml);
|
|
666
960
|
tailHtml = "";
|
|
667
961
|
shellCarriesTail = false;
|
|
668
962
|
}
|
|
669
|
-
|
|
670
|
-
resolveShell();
|
|
671
|
-
options.onShellReady?.();
|
|
963
|
+
markShellReady();
|
|
672
964
|
maybeFinalize();
|
|
673
965
|
} catch (err) {
|
|
674
966
|
options.onError?.(err);
|
|
@@ -699,11 +991,28 @@ function resolveContainer(document, options) {
|
|
|
699
991
|
}
|
|
700
992
|
return container;
|
|
701
993
|
}
|
|
702
|
-
function buildStreamRuntimeScript() {
|
|
703
|
-
|
|
994
|
+
function buildStreamRuntimeScript(options) {
|
|
995
|
+
const nonce = renderNonceAttribute(options);
|
|
996
|
+
if (options.streamRuntime === "external") {
|
|
997
|
+
if (!options.streamRuntimeSrc) {
|
|
998
|
+
throw new Error('[fict/ssr] streamRuntimeSrc is required when streamRuntime is "external".');
|
|
999
|
+
}
|
|
1000
|
+
return `<script${nonce} src="${escapeAttribute(options.streamRuntimeSrc)}" data-fict-stream-runtime data-fict-stream-observer></script>`;
|
|
1001
|
+
}
|
|
1002
|
+
return `<script${nonce}>${createStreamRuntimeCode({
|
|
1003
|
+
observerMode: resolveStreamPatchMode(options) === "observer"
|
|
1004
|
+
})}</script>`;
|
|
704
1005
|
}
|
|
705
|
-
function buildPatchChunk(id, html) {
|
|
706
|
-
|
|
1006
|
+
function buildPatchChunk(id, html, options) {
|
|
1007
|
+
const template = `<template data-fict-suspense="${escapeAttribute(id)}">${html}</template>`;
|
|
1008
|
+
if (resolveStreamPatchMode(options) === "observer") {
|
|
1009
|
+
return template;
|
|
1010
|
+
}
|
|
1011
|
+
return `${template}<script${renderNonceAttribute(options)}>__FICT_STREAM.apply("${escapeScriptString(id)}")</script>`;
|
|
1012
|
+
}
|
|
1013
|
+
function resolveStreamPatchMode(options) {
|
|
1014
|
+
if (options.streamPatchMode) return options.streamPatchMode;
|
|
1015
|
+
return options.streamRuntime === "external" ? "observer" : "inline";
|
|
707
1016
|
}
|
|
708
1017
|
function serializeBetween(document, start, end) {
|
|
709
1018
|
const wrapper = document.createElement("div");
|
|
@@ -722,11 +1031,13 @@ function splitDocumentHtml(html) {
|
|
|
722
1031
|
}
|
|
723
1032
|
function buildIncrementalSnapshotChunk(state, options) {
|
|
724
1033
|
const json = serializeSnapshotForScript(state);
|
|
1034
|
+
const nonce = renderNonceAttribute(options);
|
|
725
1035
|
if (options.snapshotTarget === "head") {
|
|
726
1036
|
const jsonLiteral = JSON.stringify(json);
|
|
727
|
-
|
|
1037
|
+
const setNonce = options.scriptNonce !== void 0 ? `s.setAttribute('nonce',${serializeScriptStringLiteral(options.scriptNonce)});` : "";
|
|
1038
|
+
return `<script${nonce}>(function(){var s=document.createElement('script');s.type='application/json';s.setAttribute('data-fict-snapshot','');${setNonce}s.textContent=${jsonLiteral};(document.head||document.documentElement).appendChild(s);}())</script>`;
|
|
728
1039
|
}
|
|
729
|
-
return `<script type="application/json" data-fict-snapshot>${json}</script>`;
|
|
1040
|
+
return `<script${nonce} type="application/json" data-fict-snapshot>${json}</script>`;
|
|
730
1041
|
}
|
|
731
1042
|
function serializeOutput(document, container, options) {
|
|
732
1043
|
if (options.fullDocument) {
|
|
@@ -743,6 +1054,9 @@ function injectSnapshot(document, container, state, options) {
|
|
|
743
1054
|
const script = document.createElement("script");
|
|
744
1055
|
script.type = "application/json";
|
|
745
1056
|
script.id = options.snapshotScriptId ?? "__FICT_SNAPSHOT__";
|
|
1057
|
+
if (options.scriptNonce !== void 0) {
|
|
1058
|
+
script.setAttribute("nonce", options.scriptNonce);
|
|
1059
|
+
}
|
|
746
1060
|
script.textContent = serializeSnapshotForScript(state);
|
|
747
1061
|
if (options.fullDocument) {
|
|
748
1062
|
if (options.snapshotTarget === "head" && document.head) {
|
|
@@ -768,6 +1082,18 @@ function injectSnapshot(document, container, state, options) {
|
|
|
768
1082
|
function serializeSnapshotForScript(state) {
|
|
769
1083
|
return JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
770
1084
|
}
|
|
1085
|
+
function serializeScriptStringLiteral(value) {
|
|
1086
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1087
|
+
}
|
|
1088
|
+
function renderNonceAttribute(options) {
|
|
1089
|
+
return options.scriptNonce === void 0 ? "" : ` nonce="${escapeAttribute(options.scriptNonce)}"`;
|
|
1090
|
+
}
|
|
1091
|
+
function escapeAttribute(value) {
|
|
1092
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1093
|
+
}
|
|
1094
|
+
function escapeScriptString(value) {
|
|
1095
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1096
|
+
}
|
|
771
1097
|
function serializeDoctype(document, override) {
|
|
772
1098
|
if (override === null) return "";
|
|
773
1099
|
if (override !== void 0) return override;
|