@fictjs/ssr 0.19.0 → 0.20.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 +19 -0
- package/dist/chunk-DI4PFT6R.js +11 -0
- package/dist/fict-stream-runtime.js +1 -0
- package/dist/index.cjs +383 -67
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +382 -67
- 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
|
@@ -146,12 +146,33 @@ function getNodeRequire() {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// src/stream-bridge.ts
|
|
149
|
-
function createQueuedTextStream() {
|
|
149
|
+
function createQueuedTextStream(options = {}) {
|
|
150
150
|
const encoder = new TextEncoder();
|
|
151
151
|
const queue = [];
|
|
152
152
|
let controller = null;
|
|
153
153
|
let closed = false;
|
|
154
154
|
let aborted;
|
|
155
|
+
const readyResolvers = [];
|
|
156
|
+
const resolveReady = () => {
|
|
157
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
158
|
+
while (readyResolvers.length > 0) {
|
|
159
|
+
readyResolvers.shift()?.();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const drainReady = () => {
|
|
163
|
+
while (readyResolvers.length > 0) {
|
|
164
|
+
readyResolvers.shift()?.();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const abortQueue = (reason, notifyController = true) => {
|
|
168
|
+
if (closed || aborted !== void 0) return;
|
|
169
|
+
aborted = reason ?? new Error("Stream aborted");
|
|
170
|
+
queue.length = 0;
|
|
171
|
+
drainReady();
|
|
172
|
+
if (notifyController) {
|
|
173
|
+
controller?.error(aborted);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
155
176
|
const stream = new ReadableStream({
|
|
156
177
|
start(ctrl) {
|
|
157
178
|
controller = ctrl;
|
|
@@ -166,6 +187,13 @@ function createQueuedTextStream() {
|
|
|
166
187
|
if (closed) {
|
|
167
188
|
ctrl.close();
|
|
168
189
|
}
|
|
190
|
+
},
|
|
191
|
+
pull() {
|
|
192
|
+
resolveReady();
|
|
193
|
+
},
|
|
194
|
+
cancel(reason) {
|
|
195
|
+
abortQueue(reason, false);
|
|
196
|
+
options.onCancel?.(reason);
|
|
169
197
|
}
|
|
170
198
|
});
|
|
171
199
|
const writer = {
|
|
@@ -174,19 +202,24 @@ function createQueuedTextStream() {
|
|
|
174
202
|
const data = encoder.encode(chunk);
|
|
175
203
|
if (controller) {
|
|
176
204
|
controller.enqueue(data);
|
|
205
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
readyResolvers.push(resolve);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
177
210
|
} else {
|
|
178
211
|
queue.push(data);
|
|
179
212
|
}
|
|
213
|
+
return void 0;
|
|
180
214
|
},
|
|
181
215
|
close() {
|
|
182
216
|
if (closed || aborted !== void 0) return;
|
|
183
217
|
closed = true;
|
|
218
|
+
drainReady();
|
|
184
219
|
controller?.close();
|
|
185
220
|
},
|
|
186
221
|
abort(reason) {
|
|
187
|
-
|
|
188
|
-
aborted = reason ?? new Error("Stream aborted");
|
|
189
|
-
controller?.error(aborted);
|
|
222
|
+
abortQueue(reason);
|
|
190
223
|
}
|
|
191
224
|
};
|
|
192
225
|
return { stream, writer };
|
|
@@ -200,9 +233,20 @@ function createPipeBridge() {
|
|
|
200
233
|
let abortReason = null;
|
|
201
234
|
const safeWrite = (target, chunk) => {
|
|
202
235
|
try {
|
|
203
|
-
target.write(chunk);
|
|
236
|
+
const ready = target.write(chunk);
|
|
237
|
+
if (ready === false) {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
const withOnce = target;
|
|
240
|
+
if (typeof withOnce.once === "function") {
|
|
241
|
+
withOnce.once("drain", resolve);
|
|
242
|
+
} else {
|
|
243
|
+
resolve();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
204
247
|
} catch {
|
|
205
248
|
}
|
|
249
|
+
return void 0;
|
|
206
250
|
};
|
|
207
251
|
const safeEnd = (target) => {
|
|
208
252
|
try {
|
|
@@ -242,9 +286,12 @@ function createPipeBridge() {
|
|
|
242
286
|
buffer.push(chunk);
|
|
243
287
|
return;
|
|
244
288
|
}
|
|
289
|
+
const pending = [];
|
|
245
290
|
for (const target of targets) {
|
|
246
|
-
safeWrite(target, chunk);
|
|
291
|
+
const result = safeWrite(target, chunk);
|
|
292
|
+
if (result) pending.push(result);
|
|
247
293
|
}
|
|
294
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
248
295
|
},
|
|
249
296
|
close() {
|
|
250
297
|
if (state !== "open") return;
|
|
@@ -274,22 +321,78 @@ function createNodePipeBridge() {
|
|
|
274
321
|
const streamModule = nodeRequire("node:stream");
|
|
275
322
|
if (!streamModule.PassThrough) return null;
|
|
276
323
|
const passThrough = new streamModule.PassThrough();
|
|
324
|
+
const buffer = [];
|
|
325
|
+
let piped = false;
|
|
326
|
+
let state = "open";
|
|
327
|
+
let abortReason = null;
|
|
328
|
+
const writeToPassThrough = (chunk) => {
|
|
329
|
+
if (passThrough.write(chunk) === false) {
|
|
330
|
+
return new Promise((resolve) => {
|
|
331
|
+
const withOnce = passThrough;
|
|
332
|
+
if (typeof withOnce.once === "function") {
|
|
333
|
+
withOnce.once("drain", resolve);
|
|
334
|
+
} else {
|
|
335
|
+
resolve();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return void 0;
|
|
340
|
+
};
|
|
341
|
+
const flushBuffer = () => {
|
|
342
|
+
if (buffer.length === 0) return void 0;
|
|
343
|
+
const pending = [];
|
|
344
|
+
for (const chunk of buffer) {
|
|
345
|
+
const result = writeToPassThrough(chunk);
|
|
346
|
+
if (result) pending.push(result);
|
|
347
|
+
}
|
|
348
|
+
buffer.length = 0;
|
|
349
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
350
|
+
};
|
|
351
|
+
const destroyPassThrough = (error) => {
|
|
352
|
+
if (typeof passThrough.destroy === "function") {
|
|
353
|
+
passThrough.destroy(error);
|
|
354
|
+
} else {
|
|
355
|
+
passThrough.end();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
277
358
|
return {
|
|
278
359
|
pipe(writable) {
|
|
360
|
+
piped = true;
|
|
279
361
|
passThrough.pipe(writable);
|
|
362
|
+
if (state === "aborted") {
|
|
363
|
+
destroyPassThrough(abortReason ?? new Error("Stream aborted"));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const flushed = flushBuffer();
|
|
367
|
+
if (state === "closed") {
|
|
368
|
+
if (flushed) {
|
|
369
|
+
void flushed.then(() => passThrough.end());
|
|
370
|
+
} else {
|
|
371
|
+
passThrough.end();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
280
374
|
},
|
|
281
375
|
write(chunk) {
|
|
282
|
-
|
|
376
|
+
if (state !== "open") return;
|
|
377
|
+
if (!piped) {
|
|
378
|
+
buffer.push(chunk);
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
return writeToPassThrough(chunk);
|
|
283
382
|
},
|
|
284
383
|
close() {
|
|
384
|
+
if (state !== "open") return;
|
|
385
|
+
state = "closed";
|
|
386
|
+
if (!piped) return;
|
|
285
387
|
passThrough.end();
|
|
286
388
|
},
|
|
287
389
|
abort(reason) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
390
|
+
if (state !== "open") return;
|
|
391
|
+
state = "aborted";
|
|
392
|
+
abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
|
|
393
|
+
buffer.length = 0;
|
|
394
|
+
if (piped) {
|
|
395
|
+
destroyPassThrough(abortReason);
|
|
293
396
|
}
|
|
294
397
|
}
|
|
295
398
|
};
|
|
@@ -310,6 +413,13 @@ function getNodeRequire2() {
|
|
|
310
413
|
}
|
|
311
414
|
}
|
|
312
415
|
|
|
416
|
+
// src/stream-runtime.ts
|
|
417
|
+
function createStreamRuntimeCode(options = {}) {
|
|
418
|
+
const observerMode = options.observerMode ?? true;
|
|
419
|
+
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);}' : "") + "})();";
|
|
420
|
+
}
|
|
421
|
+
var FICT_STREAM_RUNTIME_CODE = createStreamRuntimeCode({ observerMode: true });
|
|
422
|
+
|
|
313
423
|
// src/index.ts
|
|
314
424
|
var DEFAULT_HTML = "<!doctype html><html><head></head><body></body></html>";
|
|
315
425
|
function createSSRDocument(html = DEFAULT_HTML) {
|
|
@@ -321,6 +431,10 @@ function createSSRDocument(html = DEFAULT_HTML) {
|
|
|
321
431
|
return { window, document };
|
|
322
432
|
}
|
|
323
433
|
function renderToDocument(view, options = {}) {
|
|
434
|
+
const session = (0, import_internal.__fictCreateSSRSession)();
|
|
435
|
+
return (0, import_internal.__fictRunWithSSRSession)(session, () => renderToDocumentInSession(view, options));
|
|
436
|
+
}
|
|
437
|
+
function renderToDocumentInSession(view, options) {
|
|
324
438
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
325
439
|
(0, import_internal.__fictEnableSSR)();
|
|
326
440
|
let dom;
|
|
@@ -374,6 +488,37 @@ async function renderToStringAsync(view, options = {}) {
|
|
|
374
488
|
function renderToStream(view, options = {}) {
|
|
375
489
|
const encoder = new TextEncoder();
|
|
376
490
|
let controller = null;
|
|
491
|
+
let abortRender = null;
|
|
492
|
+
const readyResolvers = [];
|
|
493
|
+
const drainBackpressure = () => {
|
|
494
|
+
while (readyResolvers.length > 0) {
|
|
495
|
+
readyResolvers.shift()?.();
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const resolveBackpressure = () => {
|
|
499
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
500
|
+
drainBackpressure();
|
|
501
|
+
};
|
|
502
|
+
const closeController = () => {
|
|
503
|
+
abortRender = null;
|
|
504
|
+
drainBackpressure();
|
|
505
|
+
if (!controller) return;
|
|
506
|
+
try {
|
|
507
|
+
controller.close();
|
|
508
|
+
} finally {
|
|
509
|
+
controller = null;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
const errorController = (reason) => {
|
|
513
|
+
abortRender = null;
|
|
514
|
+
drainBackpressure();
|
|
515
|
+
if (!controller) return;
|
|
516
|
+
try {
|
|
517
|
+
controller.error(reason);
|
|
518
|
+
} finally {
|
|
519
|
+
controller = null;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
377
522
|
const stream = new ReadableStream({
|
|
378
523
|
start(ctrl) {
|
|
379
524
|
controller = ctrl;
|
|
@@ -381,15 +526,32 @@ function renderToStream(view, options = {}) {
|
|
|
381
526
|
write(chunk) {
|
|
382
527
|
if (!controller) return;
|
|
383
528
|
controller.enqueue(encoder.encode(chunk));
|
|
529
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
530
|
+
return new Promise((resolve) => {
|
|
531
|
+
readyResolvers.push(resolve);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return void 0;
|
|
384
535
|
},
|
|
385
536
|
close() {
|
|
386
|
-
|
|
537
|
+
closeController();
|
|
387
538
|
},
|
|
388
539
|
abort(reason) {
|
|
389
|
-
|
|
540
|
+
errorController(reason);
|
|
390
541
|
}
|
|
391
542
|
});
|
|
543
|
+
abortRender = started.abort;
|
|
544
|
+
started.shellReady.catch(() => void 0);
|
|
392
545
|
started.allReady.catch(() => void 0);
|
|
546
|
+
},
|
|
547
|
+
pull() {
|
|
548
|
+
resolveBackpressure();
|
|
549
|
+
},
|
|
550
|
+
cancel(reason) {
|
|
551
|
+
const abort = abortRender;
|
|
552
|
+
controller = null;
|
|
553
|
+
drainBackpressure();
|
|
554
|
+
abort?.(reason ?? new Error("Stream canceled"));
|
|
393
555
|
}
|
|
394
556
|
});
|
|
395
557
|
return stream;
|
|
@@ -398,7 +560,7 @@ function renderToPipeableStream(view, options = {}) {
|
|
|
398
560
|
const bridge = createPipeBridge();
|
|
399
561
|
const { shellReady, allReady, abort } = startStreamingRender(view, options, {
|
|
400
562
|
write(chunk) {
|
|
401
|
-
bridge.write(chunk);
|
|
563
|
+
return bridge.write(chunk);
|
|
402
564
|
},
|
|
403
565
|
close() {
|
|
404
566
|
bridge.close();
|
|
@@ -424,7 +586,12 @@ function renderToPartial(view, options = {}) {
|
|
|
424
586
|
};
|
|
425
587
|
let shell = "";
|
|
426
588
|
let shellPhase = true;
|
|
427
|
-
|
|
589
|
+
let abortPartial = null;
|
|
590
|
+
const queued = createQueuedTextStream({
|
|
591
|
+
onCancel(reason) {
|
|
592
|
+
abortPartial?.(reason ?? new Error("Stream canceled"));
|
|
593
|
+
}
|
|
594
|
+
});
|
|
428
595
|
const { shellReady, allReady, abort } = startStreamingRender(
|
|
429
596
|
view,
|
|
430
597
|
partialOptions,
|
|
@@ -434,7 +601,7 @@ function renderToPartial(view, options = {}) {
|
|
|
434
601
|
shell += chunk;
|
|
435
602
|
return;
|
|
436
603
|
}
|
|
437
|
-
queued.writer.write(chunk);
|
|
604
|
+
return queued.writer.write(chunk);
|
|
438
605
|
},
|
|
439
606
|
close() {
|
|
440
607
|
queued.writer.close();
|
|
@@ -450,6 +617,7 @@ function renderToPartial(view, options = {}) {
|
|
|
450
617
|
}
|
|
451
618
|
}
|
|
452
619
|
);
|
|
620
|
+
abortPartial = abort;
|
|
453
621
|
return {
|
|
454
622
|
shell,
|
|
455
623
|
stream: queued.stream,
|
|
@@ -479,17 +647,39 @@ function resolveDom(options) {
|
|
|
479
647
|
}
|
|
480
648
|
return createSSRDocument(options.html);
|
|
481
649
|
}
|
|
650
|
+
function isPromiseLike(value) {
|
|
651
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
652
|
+
}
|
|
482
653
|
function startStreamingRender(view, options, writer, control = {}) {
|
|
654
|
+
const session = (0, import_internal.__fictCreateSSRSession)();
|
|
655
|
+
return (0, import_internal.__fictRunWithSSRSession)(
|
|
656
|
+
session,
|
|
657
|
+
() => startStreamingRenderInSession(session, view, options, writer, control)
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
function startStreamingRenderInSession(session, view, options, writer, control = {}) {
|
|
661
|
+
const runInSession = (fn) => (0, import_internal.__fictRunWithSSRSession)(session, fn);
|
|
483
662
|
const resolvedOptions = {
|
|
484
663
|
...options,
|
|
485
664
|
// Streaming requires a real document; default to fullDocument when unspecified.
|
|
486
665
|
fullDocument: options.fullDocument ?? true
|
|
487
666
|
};
|
|
488
667
|
let resolveShell;
|
|
668
|
+
let rejectShell;
|
|
489
669
|
let resolveAll;
|
|
490
670
|
let rejectAll;
|
|
491
|
-
|
|
492
|
-
|
|
671
|
+
let shellSettled = false;
|
|
672
|
+
const shellReady = new Promise((res, rej) => {
|
|
673
|
+
resolveShell = () => {
|
|
674
|
+
if (shellSettled) return;
|
|
675
|
+
shellSettled = true;
|
|
676
|
+
res();
|
|
677
|
+
};
|
|
678
|
+
rejectShell = (err) => {
|
|
679
|
+
if (shellSettled) return;
|
|
680
|
+
shellSettled = true;
|
|
681
|
+
rej(err);
|
|
682
|
+
};
|
|
493
683
|
});
|
|
494
684
|
const allReady = new Promise((res, rej) => {
|
|
495
685
|
resolveAll = res;
|
|
@@ -507,39 +697,123 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
507
697
|
let tailHtml = "";
|
|
508
698
|
let wroteShell = false;
|
|
509
699
|
let shellCarriesTail = false;
|
|
700
|
+
let writeChain = null;
|
|
701
|
+
let writeFailed = false;
|
|
702
|
+
let removeAbortListener = () => {
|
|
703
|
+
};
|
|
510
704
|
const mode = options.mode ?? "shell";
|
|
511
705
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
512
706
|
const sentScopes = /* @__PURE__ */ new Set();
|
|
513
707
|
const boundaryMap = /* @__PURE__ */ new Map();
|
|
514
708
|
let boundaryId = 0;
|
|
515
709
|
let pendingCount = 0;
|
|
516
|
-
const
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
710
|
+
const handleWriteError = (error) => {
|
|
711
|
+
if (writeFailed) return;
|
|
712
|
+
writeFailed = true;
|
|
713
|
+
options.onError?.(error);
|
|
714
|
+
writer.abort(error);
|
|
715
|
+
cleanup();
|
|
716
|
+
rejectShell(error);
|
|
717
|
+
rejectAll(error);
|
|
718
|
+
};
|
|
719
|
+
const enqueueWrite = (chunk) => {
|
|
720
|
+
if (writeFailed) return;
|
|
721
|
+
const trackWrite = (promise) => {
|
|
722
|
+
const tracked = promise.then(
|
|
723
|
+
() => {
|
|
724
|
+
if (writeChain === tracked) {
|
|
725
|
+
writeChain = null;
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
(error) => {
|
|
729
|
+
if (writeChain === tracked) {
|
|
730
|
+
writeChain = null;
|
|
731
|
+
}
|
|
732
|
+
handleWriteError(error);
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
writeChain = tracked;
|
|
736
|
+
};
|
|
737
|
+
const writeAsync = () => Promise.resolve(writer.write(chunk)).then(() => void 0);
|
|
738
|
+
if (writeChain) {
|
|
739
|
+
trackWrite(writeChain.then(writeAsync));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const result = writer.write(chunk);
|
|
744
|
+
if (isPromiseLike(result)) {
|
|
745
|
+
trackWrite(
|
|
746
|
+
result.then(
|
|
747
|
+
() => void 0,
|
|
748
|
+
(error) => {
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
handleWriteError(error);
|
|
527
756
|
}
|
|
528
|
-
|
|
529
|
-
|
|
757
|
+
};
|
|
758
|
+
const afterWrites = (fn) => {
|
|
759
|
+
const pending = writeChain;
|
|
760
|
+
if (!pending) {
|
|
761
|
+
if (!writeFailed) {
|
|
762
|
+
fn();
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
530
765
|
}
|
|
766
|
+
void pending.then(() => {
|
|
767
|
+
if (!writeFailed) {
|
|
768
|
+
fn();
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
};
|
|
772
|
+
const markShellReady = () => {
|
|
773
|
+
afterWrites(() => {
|
|
774
|
+
control.onShellFlushed?.();
|
|
775
|
+
resolveShell();
|
|
776
|
+
options.onShellReady?.();
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
const writeSnapshotForScopes = (scopeIds) => {
|
|
780
|
+
runInSession(() => {
|
|
781
|
+
if (!includeSnapshot || scopeIds.length === 0) return;
|
|
782
|
+
const registry = (0, import_internal.__fictGetScopeRegistry)();
|
|
783
|
+
const pending = scopeIds.filter((id) => registry.has(id) && !sentScopes.has(id));
|
|
784
|
+
if (pending.length === 0) return;
|
|
785
|
+
const snapshot = (0, import_internal.__fictSerializeSSRStateForScopes)(pending);
|
|
786
|
+
const ids = Object.keys(snapshot.scopes);
|
|
787
|
+
if (ids.length === 0) return;
|
|
788
|
+
const chunk = buildIncrementalSnapshotChunk(snapshot, resolvedOptions);
|
|
789
|
+
if (chunk) {
|
|
790
|
+
enqueueWrite(chunk);
|
|
791
|
+
}
|
|
792
|
+
for (const id of ids) {
|
|
793
|
+
sentScopes.add(id);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
531
796
|
};
|
|
532
797
|
const writeSnapshotForBoundary = (boundary) => {
|
|
533
|
-
|
|
534
|
-
|
|
798
|
+
runInSession(() => {
|
|
799
|
+
const scopes = (0, import_internal.__fictGetScopesForBoundary)(boundary);
|
|
800
|
+
writeSnapshotForScopes(scopes);
|
|
801
|
+
});
|
|
535
802
|
};
|
|
536
803
|
const writeRemainingSnapshots = () => {
|
|
537
|
-
|
|
538
|
-
|
|
804
|
+
runInSession(() => {
|
|
805
|
+
const scopes = Array.from((0, import_internal.__fictGetScopeRegistry)().keys());
|
|
806
|
+
writeSnapshotForScopes(scopes);
|
|
807
|
+
});
|
|
539
808
|
};
|
|
540
809
|
const cleanup = () => {
|
|
541
|
-
(
|
|
542
|
-
|
|
810
|
+
removeAbortListener();
|
|
811
|
+
removeAbortListener = () => {
|
|
812
|
+
};
|
|
813
|
+
runInSession(() => {
|
|
814
|
+
(0, import_internal.__fictSetSSRStreamHooks)(null);
|
|
815
|
+
(0, import_internal.__fictDisableSSR)();
|
|
816
|
+
});
|
|
543
817
|
restoreGlobals2();
|
|
544
818
|
restoreManifest();
|
|
545
819
|
try {
|
|
@@ -556,23 +830,27 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
556
830
|
injectSnapshot(dom.document, container, snapshot, resolvedOptions);
|
|
557
831
|
}
|
|
558
832
|
const fullHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
833
|
+
enqueueWrite(fullHtml);
|
|
834
|
+
afterWrites(() => {
|
|
835
|
+
writer.close();
|
|
836
|
+
cleanup();
|
|
837
|
+
resolveShell();
|
|
838
|
+
resolveAll();
|
|
839
|
+
options.onShellReady?.();
|
|
840
|
+
options.onAllReady?.();
|
|
841
|
+
});
|
|
566
842
|
return;
|
|
567
843
|
}
|
|
568
844
|
writeRemainingSnapshots();
|
|
569
845
|
if (tailHtml) {
|
|
570
|
-
|
|
846
|
+
enqueueWrite(tailHtml);
|
|
571
847
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
848
|
+
afterWrites(() => {
|
|
849
|
+
writer.close();
|
|
850
|
+
cleanup();
|
|
851
|
+
resolveAll();
|
|
852
|
+
options.onAllReady?.();
|
|
853
|
+
});
|
|
576
854
|
};
|
|
577
855
|
const maybeFinalize = () => {
|
|
578
856
|
if (pendingCount === 0) {
|
|
@@ -602,7 +880,7 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
602
880
|
writeSnapshotForBoundary(id);
|
|
603
881
|
if (dom) {
|
|
604
882
|
const html = serializeBetween(dom.document, entry.start, entry.end);
|
|
605
|
-
|
|
883
|
+
enqueueWrite(buildPatchChunk(id, html, resolvedOptions));
|
|
606
884
|
}
|
|
607
885
|
}
|
|
608
886
|
maybeFinalize();
|
|
@@ -615,15 +893,21 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
615
893
|
const abort = (reason) => {
|
|
616
894
|
if (closed) return;
|
|
617
895
|
closed = true;
|
|
896
|
+
writeFailed = true;
|
|
618
897
|
writer.abort(reason);
|
|
619
898
|
cleanup();
|
|
620
|
-
|
|
899
|
+
const abortReason = reason ?? new Error("Stream aborted");
|
|
900
|
+
rejectShell(abortReason);
|
|
901
|
+
rejectAll(abortReason);
|
|
621
902
|
};
|
|
622
903
|
if (options.signal) {
|
|
623
904
|
if (options.signal.aborted) {
|
|
624
905
|
abort(options.signal.reason);
|
|
906
|
+
return { shellReady, allReady, abort };
|
|
625
907
|
} else {
|
|
626
|
-
|
|
908
|
+
const onAbort = () => abort(options.signal?.reason);
|
|
909
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
910
|
+
removeAbortListener = () => options.signal?.removeEventListener("abort", onAbort);
|
|
627
911
|
}
|
|
628
912
|
}
|
|
629
913
|
try {
|
|
@@ -642,33 +926,31 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
642
926
|
return { shellReady, allReady, abort };
|
|
643
927
|
}
|
|
644
928
|
const shellHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
645
|
-
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript() : "";
|
|
929
|
+
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript(resolvedOptions) : "";
|
|
646
930
|
if (resolvedOptions.fullDocument) {
|
|
647
931
|
const split = splitDocumentHtml(shellHtml);
|
|
648
932
|
if (!split) {
|
|
649
933
|
throw new Error("[fict/ssr] Failed to locate </body> for streaming output.");
|
|
650
934
|
}
|
|
651
935
|
if (control.includeTailInShell) {
|
|
652
|
-
|
|
936
|
+
enqueueWrite(split.head + streamRuntime);
|
|
653
937
|
tailHtml = split.tail;
|
|
654
938
|
shellCarriesTail = true;
|
|
655
939
|
} else {
|
|
656
|
-
|
|
940
|
+
enqueueWrite(split.head + streamRuntime);
|
|
657
941
|
tailHtml = split.tail;
|
|
658
942
|
}
|
|
659
943
|
} else {
|
|
660
|
-
|
|
944
|
+
enqueueWrite(shellHtml + streamRuntime);
|
|
661
945
|
}
|
|
662
946
|
wroteShell = true;
|
|
663
947
|
writeSnapshotForScopes(Array.from((0, import_internal.__fictGetScopeRegistry)().keys()));
|
|
664
948
|
if (shellCarriesTail && tailHtml) {
|
|
665
|
-
|
|
949
|
+
enqueueWrite(tailHtml);
|
|
666
950
|
tailHtml = "";
|
|
667
951
|
shellCarriesTail = false;
|
|
668
952
|
}
|
|
669
|
-
|
|
670
|
-
resolveShell();
|
|
671
|
-
options.onShellReady?.();
|
|
953
|
+
markShellReady();
|
|
672
954
|
maybeFinalize();
|
|
673
955
|
} catch (err) {
|
|
674
956
|
options.onError?.(err);
|
|
@@ -699,11 +981,28 @@ function resolveContainer(document, options) {
|
|
|
699
981
|
}
|
|
700
982
|
return container;
|
|
701
983
|
}
|
|
702
|
-
function buildStreamRuntimeScript() {
|
|
703
|
-
|
|
984
|
+
function buildStreamRuntimeScript(options) {
|
|
985
|
+
const nonce = renderNonceAttribute(options);
|
|
986
|
+
if (options.streamRuntime === "external") {
|
|
987
|
+
if (!options.streamRuntimeSrc) {
|
|
988
|
+
throw new Error('[fict/ssr] streamRuntimeSrc is required when streamRuntime is "external".');
|
|
989
|
+
}
|
|
990
|
+
return `<script${nonce} src="${escapeAttribute(options.streamRuntimeSrc)}" data-fict-stream-runtime data-fict-stream-observer></script>`;
|
|
991
|
+
}
|
|
992
|
+
return `<script${nonce}>${createStreamRuntimeCode({
|
|
993
|
+
observerMode: resolveStreamPatchMode(options) === "observer"
|
|
994
|
+
})}</script>`;
|
|
995
|
+
}
|
|
996
|
+
function buildPatchChunk(id, html, options) {
|
|
997
|
+
const template = `<template data-fict-suspense="${escapeAttribute(id)}">${html}</template>`;
|
|
998
|
+
if (resolveStreamPatchMode(options) === "observer") {
|
|
999
|
+
return template;
|
|
1000
|
+
}
|
|
1001
|
+
return `${template}<script${renderNonceAttribute(options)}>__FICT_STREAM.apply("${escapeScriptString(id)}")</script>`;
|
|
704
1002
|
}
|
|
705
|
-
function
|
|
706
|
-
|
|
1003
|
+
function resolveStreamPatchMode(options) {
|
|
1004
|
+
if (options.streamPatchMode) return options.streamPatchMode;
|
|
1005
|
+
return options.streamRuntime === "external" ? "observer" : "inline";
|
|
707
1006
|
}
|
|
708
1007
|
function serializeBetween(document, start, end) {
|
|
709
1008
|
const wrapper = document.createElement("div");
|
|
@@ -722,11 +1021,13 @@ function splitDocumentHtml(html) {
|
|
|
722
1021
|
}
|
|
723
1022
|
function buildIncrementalSnapshotChunk(state, options) {
|
|
724
1023
|
const json = serializeSnapshotForScript(state);
|
|
1024
|
+
const nonce = renderNonceAttribute(options);
|
|
725
1025
|
if (options.snapshotTarget === "head") {
|
|
726
1026
|
const jsonLiteral = JSON.stringify(json);
|
|
727
|
-
|
|
1027
|
+
const setNonce = options.scriptNonce !== void 0 ? `s.setAttribute('nonce',${serializeScriptStringLiteral(options.scriptNonce)});` : "";
|
|
1028
|
+
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
1029
|
}
|
|
729
|
-
return `<script type="application/json" data-fict-snapshot>${json}</script>`;
|
|
1030
|
+
return `<script${nonce} type="application/json" data-fict-snapshot>${json}</script>`;
|
|
730
1031
|
}
|
|
731
1032
|
function serializeOutput(document, container, options) {
|
|
732
1033
|
if (options.fullDocument) {
|
|
@@ -743,6 +1044,9 @@ function injectSnapshot(document, container, state, options) {
|
|
|
743
1044
|
const script = document.createElement("script");
|
|
744
1045
|
script.type = "application/json";
|
|
745
1046
|
script.id = options.snapshotScriptId ?? "__FICT_SNAPSHOT__";
|
|
1047
|
+
if (options.scriptNonce !== void 0) {
|
|
1048
|
+
script.setAttribute("nonce", options.scriptNonce);
|
|
1049
|
+
}
|
|
746
1050
|
script.textContent = serializeSnapshotForScript(state);
|
|
747
1051
|
if (options.fullDocument) {
|
|
748
1052
|
if (options.snapshotTarget === "head" && document.head) {
|
|
@@ -768,6 +1072,18 @@ function injectSnapshot(document, container, state, options) {
|
|
|
768
1072
|
function serializeSnapshotForScript(state) {
|
|
769
1073
|
return JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
770
1074
|
}
|
|
1075
|
+
function serializeScriptStringLiteral(value) {
|
|
1076
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1077
|
+
}
|
|
1078
|
+
function renderNonceAttribute(options) {
|
|
1079
|
+
return options.scriptNonce === void 0 ? "" : ` nonce="${escapeAttribute(options.scriptNonce)}"`;
|
|
1080
|
+
}
|
|
1081
|
+
function escapeAttribute(value) {
|
|
1082
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1083
|
+
}
|
|
1084
|
+
function escapeScriptString(value) {
|
|
1085
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1086
|
+
}
|
|
771
1087
|
function serializeDoctype(document, override) {
|
|
772
1088
|
if (override === null) return "";
|
|
773
1089
|
if (override !== void 0) return override;
|