@fictjs/ssr 0.18.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.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStreamRuntimeCode
|
|
3
|
+
} from "./chunk-DI4PFT6R.js";
|
|
4
|
+
|
|
1
5
|
// src/index.ts
|
|
2
6
|
import { render } from "@fictjs/runtime";
|
|
3
7
|
import {
|
|
4
8
|
__fictDisableSSR,
|
|
5
9
|
__fictEnableSSR,
|
|
10
|
+
__fictCreateSSRSession,
|
|
6
11
|
__fictGetScopeRegistry,
|
|
7
12
|
__fictGetScopesForBoundary,
|
|
13
|
+
__fictRunWithSSRSession,
|
|
8
14
|
__fictSerializeSSRState,
|
|
9
15
|
__fictSerializeSSRStateForScopes,
|
|
10
16
|
__fictSetSSRStreamHooks
|
|
@@ -124,12 +130,33 @@ function getNodeRequire() {
|
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
// src/stream-bridge.ts
|
|
127
|
-
function createQueuedTextStream() {
|
|
133
|
+
function createQueuedTextStream(options = {}) {
|
|
128
134
|
const encoder = new TextEncoder();
|
|
129
135
|
const queue = [];
|
|
130
136
|
let controller = null;
|
|
131
137
|
let closed = false;
|
|
132
138
|
let aborted;
|
|
139
|
+
const readyResolvers = [];
|
|
140
|
+
const resolveReady = () => {
|
|
141
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
142
|
+
while (readyResolvers.length > 0) {
|
|
143
|
+
readyResolvers.shift()?.();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const drainReady = () => {
|
|
147
|
+
while (readyResolvers.length > 0) {
|
|
148
|
+
readyResolvers.shift()?.();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const abortQueue = (reason, notifyController = true) => {
|
|
152
|
+
if (closed || aborted !== void 0) return;
|
|
153
|
+
aborted = reason ?? new Error("Stream aborted");
|
|
154
|
+
queue.length = 0;
|
|
155
|
+
drainReady();
|
|
156
|
+
if (notifyController) {
|
|
157
|
+
controller?.error(aborted);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
133
160
|
const stream = new ReadableStream({
|
|
134
161
|
start(ctrl) {
|
|
135
162
|
controller = ctrl;
|
|
@@ -144,6 +171,13 @@ function createQueuedTextStream() {
|
|
|
144
171
|
if (closed) {
|
|
145
172
|
ctrl.close();
|
|
146
173
|
}
|
|
174
|
+
},
|
|
175
|
+
pull() {
|
|
176
|
+
resolveReady();
|
|
177
|
+
},
|
|
178
|
+
cancel(reason) {
|
|
179
|
+
abortQueue(reason, false);
|
|
180
|
+
options.onCancel?.(reason);
|
|
147
181
|
}
|
|
148
182
|
});
|
|
149
183
|
const writer = {
|
|
@@ -152,19 +186,24 @@ function createQueuedTextStream() {
|
|
|
152
186
|
const data = encoder.encode(chunk);
|
|
153
187
|
if (controller) {
|
|
154
188
|
controller.enqueue(data);
|
|
189
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
readyResolvers.push(resolve);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
155
194
|
} else {
|
|
156
195
|
queue.push(data);
|
|
157
196
|
}
|
|
197
|
+
return void 0;
|
|
158
198
|
},
|
|
159
199
|
close() {
|
|
160
200
|
if (closed || aborted !== void 0) return;
|
|
161
201
|
closed = true;
|
|
202
|
+
drainReady();
|
|
162
203
|
controller?.close();
|
|
163
204
|
},
|
|
164
205
|
abort(reason) {
|
|
165
|
-
|
|
166
|
-
aborted = reason ?? new Error("Stream aborted");
|
|
167
|
-
controller?.error(aborted);
|
|
206
|
+
abortQueue(reason);
|
|
168
207
|
}
|
|
169
208
|
};
|
|
170
209
|
return { stream, writer };
|
|
@@ -178,9 +217,20 @@ function createPipeBridge() {
|
|
|
178
217
|
let abortReason = null;
|
|
179
218
|
const safeWrite = (target, chunk) => {
|
|
180
219
|
try {
|
|
181
|
-
target.write(chunk);
|
|
220
|
+
const ready = target.write(chunk);
|
|
221
|
+
if (ready === false) {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
const withOnce = target;
|
|
224
|
+
if (typeof withOnce.once === "function") {
|
|
225
|
+
withOnce.once("drain", resolve);
|
|
226
|
+
} else {
|
|
227
|
+
resolve();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
182
231
|
} catch {
|
|
183
232
|
}
|
|
233
|
+
return void 0;
|
|
184
234
|
};
|
|
185
235
|
const safeEnd = (target) => {
|
|
186
236
|
try {
|
|
@@ -220,9 +270,12 @@ function createPipeBridge() {
|
|
|
220
270
|
buffer.push(chunk);
|
|
221
271
|
return;
|
|
222
272
|
}
|
|
273
|
+
const pending = [];
|
|
223
274
|
for (const target of targets) {
|
|
224
|
-
safeWrite(target, chunk);
|
|
275
|
+
const result = safeWrite(target, chunk);
|
|
276
|
+
if (result) pending.push(result);
|
|
225
277
|
}
|
|
278
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
226
279
|
},
|
|
227
280
|
close() {
|
|
228
281
|
if (state !== "open") return;
|
|
@@ -252,22 +305,78 @@ function createNodePipeBridge() {
|
|
|
252
305
|
const streamModule = nodeRequire("node:stream");
|
|
253
306
|
if (!streamModule.PassThrough) return null;
|
|
254
307
|
const passThrough = new streamModule.PassThrough();
|
|
308
|
+
const buffer = [];
|
|
309
|
+
let piped = false;
|
|
310
|
+
let state = "open";
|
|
311
|
+
let abortReason = null;
|
|
312
|
+
const writeToPassThrough = (chunk) => {
|
|
313
|
+
if (passThrough.write(chunk) === false) {
|
|
314
|
+
return new Promise((resolve) => {
|
|
315
|
+
const withOnce = passThrough;
|
|
316
|
+
if (typeof withOnce.once === "function") {
|
|
317
|
+
withOnce.once("drain", resolve);
|
|
318
|
+
} else {
|
|
319
|
+
resolve();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return void 0;
|
|
324
|
+
};
|
|
325
|
+
const flushBuffer = () => {
|
|
326
|
+
if (buffer.length === 0) return void 0;
|
|
327
|
+
const pending = [];
|
|
328
|
+
for (const chunk of buffer) {
|
|
329
|
+
const result = writeToPassThrough(chunk);
|
|
330
|
+
if (result) pending.push(result);
|
|
331
|
+
}
|
|
332
|
+
buffer.length = 0;
|
|
333
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
334
|
+
};
|
|
335
|
+
const destroyPassThrough = (error) => {
|
|
336
|
+
if (typeof passThrough.destroy === "function") {
|
|
337
|
+
passThrough.destroy(error);
|
|
338
|
+
} else {
|
|
339
|
+
passThrough.end();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
255
342
|
return {
|
|
256
343
|
pipe(writable) {
|
|
344
|
+
piped = true;
|
|
257
345
|
passThrough.pipe(writable);
|
|
346
|
+
if (state === "aborted") {
|
|
347
|
+
destroyPassThrough(abortReason ?? new Error("Stream aborted"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const flushed = flushBuffer();
|
|
351
|
+
if (state === "closed") {
|
|
352
|
+
if (flushed) {
|
|
353
|
+
void flushed.then(() => passThrough.end());
|
|
354
|
+
} else {
|
|
355
|
+
passThrough.end();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
258
358
|
},
|
|
259
359
|
write(chunk) {
|
|
260
|
-
|
|
360
|
+
if (state !== "open") return;
|
|
361
|
+
if (!piped) {
|
|
362
|
+
buffer.push(chunk);
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
return writeToPassThrough(chunk);
|
|
261
366
|
},
|
|
262
367
|
close() {
|
|
368
|
+
if (state !== "open") return;
|
|
369
|
+
state = "closed";
|
|
370
|
+
if (!piped) return;
|
|
263
371
|
passThrough.end();
|
|
264
372
|
},
|
|
265
373
|
abort(reason) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
374
|
+
if (state !== "open") return;
|
|
375
|
+
state = "aborted";
|
|
376
|
+
abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
|
|
377
|
+
buffer.length = 0;
|
|
378
|
+
if (piped) {
|
|
379
|
+
destroyPassThrough(abortReason);
|
|
271
380
|
}
|
|
272
381
|
}
|
|
273
382
|
};
|
|
@@ -299,6 +408,10 @@ function createSSRDocument(html = DEFAULT_HTML) {
|
|
|
299
408
|
return { window, document };
|
|
300
409
|
}
|
|
301
410
|
function renderToDocument(view, options = {}) {
|
|
411
|
+
const session = __fictCreateSSRSession();
|
|
412
|
+
return __fictRunWithSSRSession(session, () => renderToDocumentInSession(view, options));
|
|
413
|
+
}
|
|
414
|
+
function renderToDocumentInSession(view, options) {
|
|
302
415
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
303
416
|
__fictEnableSSR();
|
|
304
417
|
let dom;
|
|
@@ -352,6 +465,37 @@ async function renderToStringAsync(view, options = {}) {
|
|
|
352
465
|
function renderToStream(view, options = {}) {
|
|
353
466
|
const encoder = new TextEncoder();
|
|
354
467
|
let controller = null;
|
|
468
|
+
let abortRender = null;
|
|
469
|
+
const readyResolvers = [];
|
|
470
|
+
const drainBackpressure = () => {
|
|
471
|
+
while (readyResolvers.length > 0) {
|
|
472
|
+
readyResolvers.shift()?.();
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const resolveBackpressure = () => {
|
|
476
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
477
|
+
drainBackpressure();
|
|
478
|
+
};
|
|
479
|
+
const closeController = () => {
|
|
480
|
+
abortRender = null;
|
|
481
|
+
drainBackpressure();
|
|
482
|
+
if (!controller) return;
|
|
483
|
+
try {
|
|
484
|
+
controller.close();
|
|
485
|
+
} finally {
|
|
486
|
+
controller = null;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const errorController = (reason) => {
|
|
490
|
+
abortRender = null;
|
|
491
|
+
drainBackpressure();
|
|
492
|
+
if (!controller) return;
|
|
493
|
+
try {
|
|
494
|
+
controller.error(reason);
|
|
495
|
+
} finally {
|
|
496
|
+
controller = null;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
355
499
|
const stream = new ReadableStream({
|
|
356
500
|
start(ctrl) {
|
|
357
501
|
controller = ctrl;
|
|
@@ -359,15 +503,32 @@ function renderToStream(view, options = {}) {
|
|
|
359
503
|
write(chunk) {
|
|
360
504
|
if (!controller) return;
|
|
361
505
|
controller.enqueue(encoder.encode(chunk));
|
|
506
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
507
|
+
return new Promise((resolve) => {
|
|
508
|
+
readyResolvers.push(resolve);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return void 0;
|
|
362
512
|
},
|
|
363
513
|
close() {
|
|
364
|
-
|
|
514
|
+
closeController();
|
|
365
515
|
},
|
|
366
516
|
abort(reason) {
|
|
367
|
-
|
|
517
|
+
errorController(reason);
|
|
368
518
|
}
|
|
369
519
|
});
|
|
520
|
+
abortRender = started.abort;
|
|
521
|
+
started.shellReady.catch(() => void 0);
|
|
370
522
|
started.allReady.catch(() => void 0);
|
|
523
|
+
},
|
|
524
|
+
pull() {
|
|
525
|
+
resolveBackpressure();
|
|
526
|
+
},
|
|
527
|
+
cancel(reason) {
|
|
528
|
+
const abort = abortRender;
|
|
529
|
+
controller = null;
|
|
530
|
+
drainBackpressure();
|
|
531
|
+
abort?.(reason ?? new Error("Stream canceled"));
|
|
371
532
|
}
|
|
372
533
|
});
|
|
373
534
|
return stream;
|
|
@@ -376,7 +537,7 @@ function renderToPipeableStream(view, options = {}) {
|
|
|
376
537
|
const bridge = createPipeBridge();
|
|
377
538
|
const { shellReady, allReady, abort } = startStreamingRender(view, options, {
|
|
378
539
|
write(chunk) {
|
|
379
|
-
bridge.write(chunk);
|
|
540
|
+
return bridge.write(chunk);
|
|
380
541
|
},
|
|
381
542
|
close() {
|
|
382
543
|
bridge.close();
|
|
@@ -402,7 +563,12 @@ function renderToPartial(view, options = {}) {
|
|
|
402
563
|
};
|
|
403
564
|
let shell = "";
|
|
404
565
|
let shellPhase = true;
|
|
405
|
-
|
|
566
|
+
let abortPartial = null;
|
|
567
|
+
const queued = createQueuedTextStream({
|
|
568
|
+
onCancel(reason) {
|
|
569
|
+
abortPartial?.(reason ?? new Error("Stream canceled"));
|
|
570
|
+
}
|
|
571
|
+
});
|
|
406
572
|
const { shellReady, allReady, abort } = startStreamingRender(
|
|
407
573
|
view,
|
|
408
574
|
partialOptions,
|
|
@@ -412,7 +578,7 @@ function renderToPartial(view, options = {}) {
|
|
|
412
578
|
shell += chunk;
|
|
413
579
|
return;
|
|
414
580
|
}
|
|
415
|
-
queued.writer.write(chunk);
|
|
581
|
+
return queued.writer.write(chunk);
|
|
416
582
|
},
|
|
417
583
|
close() {
|
|
418
584
|
queued.writer.close();
|
|
@@ -428,6 +594,7 @@ function renderToPartial(view, options = {}) {
|
|
|
428
594
|
}
|
|
429
595
|
}
|
|
430
596
|
);
|
|
597
|
+
abortPartial = abort;
|
|
431
598
|
return {
|
|
432
599
|
shell,
|
|
433
600
|
stream: queued.stream,
|
|
@@ -457,17 +624,39 @@ function resolveDom(options) {
|
|
|
457
624
|
}
|
|
458
625
|
return createSSRDocument(options.html);
|
|
459
626
|
}
|
|
627
|
+
function isPromiseLike(value) {
|
|
628
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
629
|
+
}
|
|
460
630
|
function startStreamingRender(view, options, writer, control = {}) {
|
|
631
|
+
const session = __fictCreateSSRSession();
|
|
632
|
+
return __fictRunWithSSRSession(
|
|
633
|
+
session,
|
|
634
|
+
() => startStreamingRenderInSession(session, view, options, writer, control)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
function startStreamingRenderInSession(session, view, options, writer, control = {}) {
|
|
638
|
+
const runInSession = (fn) => __fictRunWithSSRSession(session, fn);
|
|
461
639
|
const resolvedOptions = {
|
|
462
640
|
...options,
|
|
463
641
|
// Streaming requires a real document; default to fullDocument when unspecified.
|
|
464
642
|
fullDocument: options.fullDocument ?? true
|
|
465
643
|
};
|
|
466
644
|
let resolveShell;
|
|
645
|
+
let rejectShell;
|
|
467
646
|
let resolveAll;
|
|
468
647
|
let rejectAll;
|
|
469
|
-
|
|
470
|
-
|
|
648
|
+
let shellSettled = false;
|
|
649
|
+
const shellReady = new Promise((res, rej) => {
|
|
650
|
+
resolveShell = () => {
|
|
651
|
+
if (shellSettled) return;
|
|
652
|
+
shellSettled = true;
|
|
653
|
+
res();
|
|
654
|
+
};
|
|
655
|
+
rejectShell = (err) => {
|
|
656
|
+
if (shellSettled) return;
|
|
657
|
+
shellSettled = true;
|
|
658
|
+
rej(err);
|
|
659
|
+
};
|
|
471
660
|
});
|
|
472
661
|
const allReady = new Promise((res, rej) => {
|
|
473
662
|
resolveAll = res;
|
|
@@ -485,39 +674,123 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
485
674
|
let tailHtml = "";
|
|
486
675
|
let wroteShell = false;
|
|
487
676
|
let shellCarriesTail = false;
|
|
677
|
+
let writeChain = null;
|
|
678
|
+
let writeFailed = false;
|
|
679
|
+
let removeAbortListener = () => {
|
|
680
|
+
};
|
|
488
681
|
const mode = options.mode ?? "shell";
|
|
489
682
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
490
683
|
const sentScopes = /* @__PURE__ */ new Set();
|
|
491
684
|
const boundaryMap = /* @__PURE__ */ new Map();
|
|
492
685
|
let boundaryId = 0;
|
|
493
686
|
let pendingCount = 0;
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
687
|
+
const handleWriteError = (error) => {
|
|
688
|
+
if (writeFailed) return;
|
|
689
|
+
writeFailed = true;
|
|
690
|
+
options.onError?.(error);
|
|
691
|
+
writer.abort(error);
|
|
692
|
+
cleanup();
|
|
693
|
+
rejectShell(error);
|
|
694
|
+
rejectAll(error);
|
|
695
|
+
};
|
|
696
|
+
const enqueueWrite = (chunk) => {
|
|
697
|
+
if (writeFailed) return;
|
|
698
|
+
const trackWrite = (promise) => {
|
|
699
|
+
const tracked = promise.then(
|
|
700
|
+
() => {
|
|
701
|
+
if (writeChain === tracked) {
|
|
702
|
+
writeChain = null;
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
(error) => {
|
|
706
|
+
if (writeChain === tracked) {
|
|
707
|
+
writeChain = null;
|
|
708
|
+
}
|
|
709
|
+
handleWriteError(error);
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
writeChain = tracked;
|
|
713
|
+
};
|
|
714
|
+
const writeAsync = () => Promise.resolve(writer.write(chunk)).then(() => void 0);
|
|
715
|
+
if (writeChain) {
|
|
716
|
+
trackWrite(writeChain.then(writeAsync));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
const result = writer.write(chunk);
|
|
721
|
+
if (isPromiseLike(result)) {
|
|
722
|
+
trackWrite(
|
|
723
|
+
result.then(
|
|
724
|
+
() => void 0,
|
|
725
|
+
(error) => {
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
handleWriteError(error);
|
|
505
733
|
}
|
|
506
|
-
|
|
507
|
-
|
|
734
|
+
};
|
|
735
|
+
const afterWrites = (fn) => {
|
|
736
|
+
const pending = writeChain;
|
|
737
|
+
if (!pending) {
|
|
738
|
+
if (!writeFailed) {
|
|
739
|
+
fn();
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
508
742
|
}
|
|
743
|
+
void pending.then(() => {
|
|
744
|
+
if (!writeFailed) {
|
|
745
|
+
fn();
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
const markShellReady = () => {
|
|
750
|
+
afterWrites(() => {
|
|
751
|
+
control.onShellFlushed?.();
|
|
752
|
+
resolveShell();
|
|
753
|
+
options.onShellReady?.();
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
const writeSnapshotForScopes = (scopeIds) => {
|
|
757
|
+
runInSession(() => {
|
|
758
|
+
if (!includeSnapshot || scopeIds.length === 0) return;
|
|
759
|
+
const registry = __fictGetScopeRegistry();
|
|
760
|
+
const pending = scopeIds.filter((id) => registry.has(id) && !sentScopes.has(id));
|
|
761
|
+
if (pending.length === 0) return;
|
|
762
|
+
const snapshot = __fictSerializeSSRStateForScopes(pending);
|
|
763
|
+
const ids = Object.keys(snapshot.scopes);
|
|
764
|
+
if (ids.length === 0) return;
|
|
765
|
+
const chunk = buildIncrementalSnapshotChunk(snapshot, resolvedOptions);
|
|
766
|
+
if (chunk) {
|
|
767
|
+
enqueueWrite(chunk);
|
|
768
|
+
}
|
|
769
|
+
for (const id of ids) {
|
|
770
|
+
sentScopes.add(id);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
509
773
|
};
|
|
510
774
|
const writeSnapshotForBoundary = (boundary) => {
|
|
511
|
-
|
|
512
|
-
|
|
775
|
+
runInSession(() => {
|
|
776
|
+
const scopes = __fictGetScopesForBoundary(boundary);
|
|
777
|
+
writeSnapshotForScopes(scopes);
|
|
778
|
+
});
|
|
513
779
|
};
|
|
514
780
|
const writeRemainingSnapshots = () => {
|
|
515
|
-
|
|
516
|
-
|
|
781
|
+
runInSession(() => {
|
|
782
|
+
const scopes = Array.from(__fictGetScopeRegistry().keys());
|
|
783
|
+
writeSnapshotForScopes(scopes);
|
|
784
|
+
});
|
|
517
785
|
};
|
|
518
786
|
const cleanup = () => {
|
|
519
|
-
|
|
520
|
-
|
|
787
|
+
removeAbortListener();
|
|
788
|
+
removeAbortListener = () => {
|
|
789
|
+
};
|
|
790
|
+
runInSession(() => {
|
|
791
|
+
__fictSetSSRStreamHooks(null);
|
|
792
|
+
__fictDisableSSR();
|
|
793
|
+
});
|
|
521
794
|
restoreGlobals2();
|
|
522
795
|
restoreManifest();
|
|
523
796
|
try {
|
|
@@ -534,23 +807,27 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
534
807
|
injectSnapshot(dom.document, container, snapshot, resolvedOptions);
|
|
535
808
|
}
|
|
536
809
|
const fullHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
810
|
+
enqueueWrite(fullHtml);
|
|
811
|
+
afterWrites(() => {
|
|
812
|
+
writer.close();
|
|
813
|
+
cleanup();
|
|
814
|
+
resolveShell();
|
|
815
|
+
resolveAll();
|
|
816
|
+
options.onShellReady?.();
|
|
817
|
+
options.onAllReady?.();
|
|
818
|
+
});
|
|
544
819
|
return;
|
|
545
820
|
}
|
|
546
821
|
writeRemainingSnapshots();
|
|
547
822
|
if (tailHtml) {
|
|
548
|
-
|
|
823
|
+
enqueueWrite(tailHtml);
|
|
549
824
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
825
|
+
afterWrites(() => {
|
|
826
|
+
writer.close();
|
|
827
|
+
cleanup();
|
|
828
|
+
resolveAll();
|
|
829
|
+
options.onAllReady?.();
|
|
830
|
+
});
|
|
554
831
|
};
|
|
555
832
|
const maybeFinalize = () => {
|
|
556
833
|
if (pendingCount === 0) {
|
|
@@ -580,7 +857,7 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
580
857
|
writeSnapshotForBoundary(id);
|
|
581
858
|
if (dom) {
|
|
582
859
|
const html = serializeBetween(dom.document, entry.start, entry.end);
|
|
583
|
-
|
|
860
|
+
enqueueWrite(buildPatchChunk(id, html, resolvedOptions));
|
|
584
861
|
}
|
|
585
862
|
}
|
|
586
863
|
maybeFinalize();
|
|
@@ -593,15 +870,21 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
593
870
|
const abort = (reason) => {
|
|
594
871
|
if (closed) return;
|
|
595
872
|
closed = true;
|
|
873
|
+
writeFailed = true;
|
|
596
874
|
writer.abort(reason);
|
|
597
875
|
cleanup();
|
|
598
|
-
|
|
876
|
+
const abortReason = reason ?? new Error("Stream aborted");
|
|
877
|
+
rejectShell(abortReason);
|
|
878
|
+
rejectAll(abortReason);
|
|
599
879
|
};
|
|
600
880
|
if (options.signal) {
|
|
601
881
|
if (options.signal.aborted) {
|
|
602
882
|
abort(options.signal.reason);
|
|
883
|
+
return { shellReady, allReady, abort };
|
|
603
884
|
} else {
|
|
604
|
-
|
|
885
|
+
const onAbort = () => abort(options.signal?.reason);
|
|
886
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
887
|
+
removeAbortListener = () => options.signal?.removeEventListener("abort", onAbort);
|
|
605
888
|
}
|
|
606
889
|
}
|
|
607
890
|
try {
|
|
@@ -620,33 +903,31 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
620
903
|
return { shellReady, allReady, abort };
|
|
621
904
|
}
|
|
622
905
|
const shellHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
623
|
-
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript() : "";
|
|
906
|
+
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript(resolvedOptions) : "";
|
|
624
907
|
if (resolvedOptions.fullDocument) {
|
|
625
908
|
const split = splitDocumentHtml(shellHtml);
|
|
626
909
|
if (!split) {
|
|
627
910
|
throw new Error("[fict/ssr] Failed to locate </body> for streaming output.");
|
|
628
911
|
}
|
|
629
912
|
if (control.includeTailInShell) {
|
|
630
|
-
|
|
913
|
+
enqueueWrite(split.head + streamRuntime);
|
|
631
914
|
tailHtml = split.tail;
|
|
632
915
|
shellCarriesTail = true;
|
|
633
916
|
} else {
|
|
634
|
-
|
|
917
|
+
enqueueWrite(split.head + streamRuntime);
|
|
635
918
|
tailHtml = split.tail;
|
|
636
919
|
}
|
|
637
920
|
} else {
|
|
638
|
-
|
|
921
|
+
enqueueWrite(shellHtml + streamRuntime);
|
|
639
922
|
}
|
|
640
923
|
wroteShell = true;
|
|
641
924
|
writeSnapshotForScopes(Array.from(__fictGetScopeRegistry().keys()));
|
|
642
925
|
if (shellCarriesTail && tailHtml) {
|
|
643
|
-
|
|
926
|
+
enqueueWrite(tailHtml);
|
|
644
927
|
tailHtml = "";
|
|
645
928
|
shellCarriesTail = false;
|
|
646
929
|
}
|
|
647
|
-
|
|
648
|
-
resolveShell();
|
|
649
|
-
options.onShellReady?.();
|
|
930
|
+
markShellReady();
|
|
650
931
|
maybeFinalize();
|
|
651
932
|
} catch (err) {
|
|
652
933
|
options.onError?.(err);
|
|
@@ -677,11 +958,28 @@ function resolveContainer(document, options) {
|
|
|
677
958
|
}
|
|
678
959
|
return container;
|
|
679
960
|
}
|
|
680
|
-
function buildStreamRuntimeScript() {
|
|
681
|
-
|
|
961
|
+
function buildStreamRuntimeScript(options) {
|
|
962
|
+
const nonce = renderNonceAttribute(options);
|
|
963
|
+
if (options.streamRuntime === "external") {
|
|
964
|
+
if (!options.streamRuntimeSrc) {
|
|
965
|
+
throw new Error('[fict/ssr] streamRuntimeSrc is required when streamRuntime is "external".');
|
|
966
|
+
}
|
|
967
|
+
return `<script${nonce} src="${escapeAttribute(options.streamRuntimeSrc)}" data-fict-stream-runtime data-fict-stream-observer></script>`;
|
|
968
|
+
}
|
|
969
|
+
return `<script${nonce}>${createStreamRuntimeCode({
|
|
970
|
+
observerMode: resolveStreamPatchMode(options) === "observer"
|
|
971
|
+
})}</script>`;
|
|
972
|
+
}
|
|
973
|
+
function buildPatchChunk(id, html, options) {
|
|
974
|
+
const template = `<template data-fict-suspense="${escapeAttribute(id)}">${html}</template>`;
|
|
975
|
+
if (resolveStreamPatchMode(options) === "observer") {
|
|
976
|
+
return template;
|
|
977
|
+
}
|
|
978
|
+
return `${template}<script${renderNonceAttribute(options)}>__FICT_STREAM.apply("${escapeScriptString(id)}")</script>`;
|
|
682
979
|
}
|
|
683
|
-
function
|
|
684
|
-
|
|
980
|
+
function resolveStreamPatchMode(options) {
|
|
981
|
+
if (options.streamPatchMode) return options.streamPatchMode;
|
|
982
|
+
return options.streamRuntime === "external" ? "observer" : "inline";
|
|
685
983
|
}
|
|
686
984
|
function serializeBetween(document, start, end) {
|
|
687
985
|
const wrapper = document.createElement("div");
|
|
@@ -700,11 +998,13 @@ function splitDocumentHtml(html) {
|
|
|
700
998
|
}
|
|
701
999
|
function buildIncrementalSnapshotChunk(state, options) {
|
|
702
1000
|
const json = serializeSnapshotForScript(state);
|
|
1001
|
+
const nonce = renderNonceAttribute(options);
|
|
703
1002
|
if (options.snapshotTarget === "head") {
|
|
704
1003
|
const jsonLiteral = JSON.stringify(json);
|
|
705
|
-
|
|
1004
|
+
const setNonce = options.scriptNonce !== void 0 ? `s.setAttribute('nonce',${serializeScriptStringLiteral(options.scriptNonce)});` : "";
|
|
1005
|
+
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>`;
|
|
706
1006
|
}
|
|
707
|
-
return `<script type="application/json" data-fict-snapshot>${json}</script>`;
|
|
1007
|
+
return `<script${nonce} type="application/json" data-fict-snapshot>${json}</script>`;
|
|
708
1008
|
}
|
|
709
1009
|
function serializeOutput(document, container, options) {
|
|
710
1010
|
if (options.fullDocument) {
|
|
@@ -721,6 +1021,9 @@ function injectSnapshot(document, container, state, options) {
|
|
|
721
1021
|
const script = document.createElement("script");
|
|
722
1022
|
script.type = "application/json";
|
|
723
1023
|
script.id = options.snapshotScriptId ?? "__FICT_SNAPSHOT__";
|
|
1024
|
+
if (options.scriptNonce !== void 0) {
|
|
1025
|
+
script.setAttribute("nonce", options.scriptNonce);
|
|
1026
|
+
}
|
|
724
1027
|
script.textContent = serializeSnapshotForScript(state);
|
|
725
1028
|
if (options.fullDocument) {
|
|
726
1029
|
if (options.snapshotTarget === "head" && document.head) {
|
|
@@ -746,6 +1049,18 @@ function injectSnapshot(document, container, state, options) {
|
|
|
746
1049
|
function serializeSnapshotForScript(state) {
|
|
747
1050
|
return JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
748
1051
|
}
|
|
1052
|
+
function serializeScriptStringLiteral(value) {
|
|
1053
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1054
|
+
}
|
|
1055
|
+
function renderNonceAttribute(options) {
|
|
1056
|
+
return options.scriptNonce === void 0 ? "" : ` nonce="${escapeAttribute(options.scriptNonce)}"`;
|
|
1057
|
+
}
|
|
1058
|
+
function escapeAttribute(value) {
|
|
1059
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1060
|
+
}
|
|
1061
|
+
function escapeScriptString(value) {
|
|
1062
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1063
|
+
}
|
|
749
1064
|
function serializeDoctype(document, override) {
|
|
750
1065
|
if (override === null) return "";
|
|
751
1066
|
if (override !== void 0) return override;
|