@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.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
|
|
@@ -12,6 +18,7 @@ import {
|
|
|
12
18
|
import { parseHTML } from "linkedom";
|
|
13
19
|
|
|
14
20
|
// src/globals.ts
|
|
21
|
+
import { __fictGetCurrentSSRSession } from "@fictjs/runtime/internal";
|
|
15
22
|
function installGlobals(window, document) {
|
|
16
23
|
const win = window;
|
|
17
24
|
const required = {
|
|
@@ -61,6 +68,14 @@ function installManifest(manifest) {
|
|
|
61
68
|
} else {
|
|
62
69
|
resolved = manifest;
|
|
63
70
|
}
|
|
71
|
+
const session = __fictGetCurrentSSRSession();
|
|
72
|
+
if (session) {
|
|
73
|
+
const previous = session.manifest;
|
|
74
|
+
session.manifest = resolved;
|
|
75
|
+
return () => {
|
|
76
|
+
session.manifest = previous;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
64
79
|
const key = "__FICT_MANIFEST__";
|
|
65
80
|
const snapshot = {
|
|
66
81
|
exists: Object.prototype.hasOwnProperty.call(globalThis, key),
|
|
@@ -124,12 +139,33 @@ function getNodeRequire() {
|
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
// src/stream-bridge.ts
|
|
127
|
-
function createQueuedTextStream() {
|
|
142
|
+
function createQueuedTextStream(options = {}) {
|
|
128
143
|
const encoder = new TextEncoder();
|
|
129
144
|
const queue = [];
|
|
130
145
|
let controller = null;
|
|
131
146
|
let closed = false;
|
|
132
147
|
let aborted;
|
|
148
|
+
const readyResolvers = [];
|
|
149
|
+
const resolveReady = () => {
|
|
150
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
151
|
+
while (readyResolvers.length > 0) {
|
|
152
|
+
readyResolvers.shift()?.();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const drainReady = () => {
|
|
156
|
+
while (readyResolvers.length > 0) {
|
|
157
|
+
readyResolvers.shift()?.();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const abortQueue = (reason, notifyController = true) => {
|
|
161
|
+
if (closed || aborted !== void 0) return;
|
|
162
|
+
aborted = reason ?? new Error("Stream aborted");
|
|
163
|
+
queue.length = 0;
|
|
164
|
+
drainReady();
|
|
165
|
+
if (notifyController) {
|
|
166
|
+
controller?.error(aborted);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
133
169
|
const stream = new ReadableStream({
|
|
134
170
|
start(ctrl) {
|
|
135
171
|
controller = ctrl;
|
|
@@ -144,6 +180,13 @@ function createQueuedTextStream() {
|
|
|
144
180
|
if (closed) {
|
|
145
181
|
ctrl.close();
|
|
146
182
|
}
|
|
183
|
+
},
|
|
184
|
+
pull() {
|
|
185
|
+
resolveReady();
|
|
186
|
+
},
|
|
187
|
+
cancel(reason) {
|
|
188
|
+
abortQueue(reason, false);
|
|
189
|
+
options.onCancel?.(reason);
|
|
147
190
|
}
|
|
148
191
|
});
|
|
149
192
|
const writer = {
|
|
@@ -152,19 +195,24 @@ function createQueuedTextStream() {
|
|
|
152
195
|
const data = encoder.encode(chunk);
|
|
153
196
|
if (controller) {
|
|
154
197
|
controller.enqueue(data);
|
|
198
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
199
|
+
return new Promise((resolve) => {
|
|
200
|
+
readyResolvers.push(resolve);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
155
203
|
} else {
|
|
156
204
|
queue.push(data);
|
|
157
205
|
}
|
|
206
|
+
return void 0;
|
|
158
207
|
},
|
|
159
208
|
close() {
|
|
160
209
|
if (closed || aborted !== void 0) return;
|
|
161
210
|
closed = true;
|
|
211
|
+
drainReady();
|
|
162
212
|
controller?.close();
|
|
163
213
|
},
|
|
164
214
|
abort(reason) {
|
|
165
|
-
|
|
166
|
-
aborted = reason ?? new Error("Stream aborted");
|
|
167
|
-
controller?.error(aborted);
|
|
215
|
+
abortQueue(reason);
|
|
168
216
|
}
|
|
169
217
|
};
|
|
170
218
|
return { stream, writer };
|
|
@@ -178,9 +226,20 @@ function createPipeBridge() {
|
|
|
178
226
|
let abortReason = null;
|
|
179
227
|
const safeWrite = (target, chunk) => {
|
|
180
228
|
try {
|
|
181
|
-
target.write(chunk);
|
|
229
|
+
const ready = target.write(chunk);
|
|
230
|
+
if (ready === false) {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
const withOnce = target;
|
|
233
|
+
if (typeof withOnce.once === "function") {
|
|
234
|
+
withOnce.once("drain", resolve);
|
|
235
|
+
} else {
|
|
236
|
+
resolve();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
182
240
|
} catch {
|
|
183
241
|
}
|
|
242
|
+
return void 0;
|
|
184
243
|
};
|
|
185
244
|
const safeEnd = (target) => {
|
|
186
245
|
try {
|
|
@@ -220,9 +279,12 @@ function createPipeBridge() {
|
|
|
220
279
|
buffer.push(chunk);
|
|
221
280
|
return;
|
|
222
281
|
}
|
|
282
|
+
const pending = [];
|
|
223
283
|
for (const target of targets) {
|
|
224
|
-
safeWrite(target, chunk);
|
|
284
|
+
const result = safeWrite(target, chunk);
|
|
285
|
+
if (result) pending.push(result);
|
|
225
286
|
}
|
|
287
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
226
288
|
},
|
|
227
289
|
close() {
|
|
228
290
|
if (state !== "open") return;
|
|
@@ -252,22 +314,78 @@ function createNodePipeBridge() {
|
|
|
252
314
|
const streamModule = nodeRequire("node:stream");
|
|
253
315
|
if (!streamModule.PassThrough) return null;
|
|
254
316
|
const passThrough = new streamModule.PassThrough();
|
|
317
|
+
const buffer = [];
|
|
318
|
+
let piped = false;
|
|
319
|
+
let state = "open";
|
|
320
|
+
let abortReason = null;
|
|
321
|
+
const writeToPassThrough = (chunk) => {
|
|
322
|
+
if (passThrough.write(chunk) === false) {
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
const withOnce = passThrough;
|
|
325
|
+
if (typeof withOnce.once === "function") {
|
|
326
|
+
withOnce.once("drain", resolve);
|
|
327
|
+
} else {
|
|
328
|
+
resolve();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return void 0;
|
|
333
|
+
};
|
|
334
|
+
const flushBuffer = () => {
|
|
335
|
+
if (buffer.length === 0) return void 0;
|
|
336
|
+
const pending = [];
|
|
337
|
+
for (const chunk of buffer) {
|
|
338
|
+
const result = writeToPassThrough(chunk);
|
|
339
|
+
if (result) pending.push(result);
|
|
340
|
+
}
|
|
341
|
+
buffer.length = 0;
|
|
342
|
+
return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
|
|
343
|
+
};
|
|
344
|
+
const destroyPassThrough = (error) => {
|
|
345
|
+
if (typeof passThrough.destroy === "function") {
|
|
346
|
+
passThrough.destroy(error);
|
|
347
|
+
} else {
|
|
348
|
+
passThrough.end();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
255
351
|
return {
|
|
256
352
|
pipe(writable) {
|
|
353
|
+
piped = true;
|
|
257
354
|
passThrough.pipe(writable);
|
|
355
|
+
if (state === "aborted") {
|
|
356
|
+
destroyPassThrough(abortReason ?? new Error("Stream aborted"));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const flushed = flushBuffer();
|
|
360
|
+
if (state === "closed") {
|
|
361
|
+
if (flushed) {
|
|
362
|
+
void flushed.then(() => passThrough.end());
|
|
363
|
+
} else {
|
|
364
|
+
passThrough.end();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
258
367
|
},
|
|
259
368
|
write(chunk) {
|
|
260
|
-
|
|
369
|
+
if (state !== "open") return;
|
|
370
|
+
if (!piped) {
|
|
371
|
+
buffer.push(chunk);
|
|
372
|
+
return void 0;
|
|
373
|
+
}
|
|
374
|
+
return writeToPassThrough(chunk);
|
|
261
375
|
},
|
|
262
376
|
close() {
|
|
377
|
+
if (state !== "open") return;
|
|
378
|
+
state = "closed";
|
|
379
|
+
if (!piped) return;
|
|
263
380
|
passThrough.end();
|
|
264
381
|
},
|
|
265
382
|
abort(reason) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
383
|
+
if (state !== "open") return;
|
|
384
|
+
state = "aborted";
|
|
385
|
+
abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
|
|
386
|
+
buffer.length = 0;
|
|
387
|
+
if (piped) {
|
|
388
|
+
destroyPassThrough(abortReason);
|
|
271
389
|
}
|
|
272
390
|
}
|
|
273
391
|
};
|
|
@@ -299,6 +417,10 @@ function createSSRDocument(html = DEFAULT_HTML) {
|
|
|
299
417
|
return { window, document };
|
|
300
418
|
}
|
|
301
419
|
function renderToDocument(view, options = {}) {
|
|
420
|
+
const session = __fictCreateSSRSession();
|
|
421
|
+
return __fictRunWithSSRSession(session, () => renderToDocumentInSession(view, options));
|
|
422
|
+
}
|
|
423
|
+
function renderToDocumentInSession(view, options) {
|
|
302
424
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
303
425
|
__fictEnableSSR();
|
|
304
426
|
let dom;
|
|
@@ -312,7 +434,7 @@ function renderToDocument(view, options = {}) {
|
|
|
312
434
|
try {
|
|
313
435
|
dom = resolveDom(options);
|
|
314
436
|
const { document, window } = dom;
|
|
315
|
-
const shouldExpose = options.exposeGlobals
|
|
437
|
+
const shouldExpose = options.exposeGlobals === true;
|
|
316
438
|
restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
|
|
317
439
|
};
|
|
318
440
|
restoreManifest = installManifest(options.manifest);
|
|
@@ -352,6 +474,37 @@ async function renderToStringAsync(view, options = {}) {
|
|
|
352
474
|
function renderToStream(view, options = {}) {
|
|
353
475
|
const encoder = new TextEncoder();
|
|
354
476
|
let controller = null;
|
|
477
|
+
let abortRender = null;
|
|
478
|
+
const readyResolvers = [];
|
|
479
|
+
const drainBackpressure = () => {
|
|
480
|
+
while (readyResolvers.length > 0) {
|
|
481
|
+
readyResolvers.shift()?.();
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const resolveBackpressure = () => {
|
|
485
|
+
if (!controller || (controller.desiredSize ?? 1) <= 0) return;
|
|
486
|
+
drainBackpressure();
|
|
487
|
+
};
|
|
488
|
+
const closeController = () => {
|
|
489
|
+
abortRender = null;
|
|
490
|
+
drainBackpressure();
|
|
491
|
+
if (!controller) return;
|
|
492
|
+
try {
|
|
493
|
+
controller.close();
|
|
494
|
+
} finally {
|
|
495
|
+
controller = null;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const errorController = (reason) => {
|
|
499
|
+
abortRender = null;
|
|
500
|
+
drainBackpressure();
|
|
501
|
+
if (!controller) return;
|
|
502
|
+
try {
|
|
503
|
+
controller.error(reason);
|
|
504
|
+
} finally {
|
|
505
|
+
controller = null;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
355
508
|
const stream = new ReadableStream({
|
|
356
509
|
start(ctrl) {
|
|
357
510
|
controller = ctrl;
|
|
@@ -359,15 +512,32 @@ function renderToStream(view, options = {}) {
|
|
|
359
512
|
write(chunk) {
|
|
360
513
|
if (!controller) return;
|
|
361
514
|
controller.enqueue(encoder.encode(chunk));
|
|
515
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
516
|
+
return new Promise((resolve) => {
|
|
517
|
+
readyResolvers.push(resolve);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return void 0;
|
|
362
521
|
},
|
|
363
522
|
close() {
|
|
364
|
-
|
|
523
|
+
closeController();
|
|
365
524
|
},
|
|
366
525
|
abort(reason) {
|
|
367
|
-
|
|
526
|
+
errorController(reason);
|
|
368
527
|
}
|
|
369
528
|
});
|
|
529
|
+
abortRender = started.abort;
|
|
530
|
+
started.shellReady.catch(() => void 0);
|
|
370
531
|
started.allReady.catch(() => void 0);
|
|
532
|
+
},
|
|
533
|
+
pull() {
|
|
534
|
+
resolveBackpressure();
|
|
535
|
+
},
|
|
536
|
+
cancel(reason) {
|
|
537
|
+
const abort = abortRender;
|
|
538
|
+
controller = null;
|
|
539
|
+
drainBackpressure();
|
|
540
|
+
abort?.(reason ?? new Error("Stream canceled"));
|
|
371
541
|
}
|
|
372
542
|
});
|
|
373
543
|
return stream;
|
|
@@ -376,7 +546,7 @@ function renderToPipeableStream(view, options = {}) {
|
|
|
376
546
|
const bridge = createPipeBridge();
|
|
377
547
|
const { shellReady, allReady, abort } = startStreamingRender(view, options, {
|
|
378
548
|
write(chunk) {
|
|
379
|
-
bridge.write(chunk);
|
|
549
|
+
return bridge.write(chunk);
|
|
380
550
|
},
|
|
381
551
|
close() {
|
|
382
552
|
bridge.close();
|
|
@@ -402,7 +572,12 @@ function renderToPartial(view, options = {}) {
|
|
|
402
572
|
};
|
|
403
573
|
let shell = "";
|
|
404
574
|
let shellPhase = true;
|
|
405
|
-
|
|
575
|
+
let abortPartial = null;
|
|
576
|
+
const queued = createQueuedTextStream({
|
|
577
|
+
onCancel(reason) {
|
|
578
|
+
abortPartial?.(reason ?? new Error("Stream canceled"));
|
|
579
|
+
}
|
|
580
|
+
});
|
|
406
581
|
const { shellReady, allReady, abort } = startStreamingRender(
|
|
407
582
|
view,
|
|
408
583
|
partialOptions,
|
|
@@ -412,7 +587,7 @@ function renderToPartial(view, options = {}) {
|
|
|
412
587
|
shell += chunk;
|
|
413
588
|
return;
|
|
414
589
|
}
|
|
415
|
-
queued.writer.write(chunk);
|
|
590
|
+
return queued.writer.write(chunk);
|
|
416
591
|
},
|
|
417
592
|
close() {
|
|
418
593
|
queued.writer.close();
|
|
@@ -428,6 +603,7 @@ function renderToPartial(view, options = {}) {
|
|
|
428
603
|
}
|
|
429
604
|
}
|
|
430
605
|
);
|
|
606
|
+
abortPartial = abort;
|
|
431
607
|
return {
|
|
432
608
|
shell,
|
|
433
609
|
stream: queued.stream,
|
|
@@ -457,17 +633,39 @@ function resolveDom(options) {
|
|
|
457
633
|
}
|
|
458
634
|
return createSSRDocument(options.html);
|
|
459
635
|
}
|
|
636
|
+
function isPromiseLike(value) {
|
|
637
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
638
|
+
}
|
|
460
639
|
function startStreamingRender(view, options, writer, control = {}) {
|
|
640
|
+
const session = __fictCreateSSRSession();
|
|
641
|
+
return __fictRunWithSSRSession(
|
|
642
|
+
session,
|
|
643
|
+
() => startStreamingRenderInSession(session, view, options, writer, control)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
function startStreamingRenderInSession(session, view, options, writer, control = {}) {
|
|
647
|
+
const runInSession = (fn) => __fictRunWithSSRSession(session, fn);
|
|
461
648
|
const resolvedOptions = {
|
|
462
649
|
...options,
|
|
463
650
|
// Streaming requires a real document; default to fullDocument when unspecified.
|
|
464
651
|
fullDocument: options.fullDocument ?? true
|
|
465
652
|
};
|
|
466
653
|
let resolveShell;
|
|
654
|
+
let rejectShell;
|
|
467
655
|
let resolveAll;
|
|
468
656
|
let rejectAll;
|
|
469
|
-
|
|
470
|
-
|
|
657
|
+
let shellSettled = false;
|
|
658
|
+
const shellReady = new Promise((res, rej) => {
|
|
659
|
+
resolveShell = () => {
|
|
660
|
+
if (shellSettled) return;
|
|
661
|
+
shellSettled = true;
|
|
662
|
+
res();
|
|
663
|
+
};
|
|
664
|
+
rejectShell = (err) => {
|
|
665
|
+
if (shellSettled) return;
|
|
666
|
+
shellSettled = true;
|
|
667
|
+
rej(err);
|
|
668
|
+
};
|
|
471
669
|
});
|
|
472
670
|
const allReady = new Promise((res, rej) => {
|
|
473
671
|
resolveAll = res;
|
|
@@ -485,44 +683,129 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
485
683
|
let tailHtml = "";
|
|
486
684
|
let wroteShell = false;
|
|
487
685
|
let shellCarriesTail = false;
|
|
686
|
+
let writeChain = null;
|
|
687
|
+
let writeFailed = false;
|
|
688
|
+
let removeAbortListener = () => {
|
|
689
|
+
};
|
|
488
690
|
const mode = options.mode ?? "shell";
|
|
489
691
|
const includeSnapshot = options.includeSnapshot !== false;
|
|
490
692
|
const sentScopes = /* @__PURE__ */ new Set();
|
|
491
693
|
const boundaryMap = /* @__PURE__ */ new Map();
|
|
492
694
|
let boundaryId = 0;
|
|
493
695
|
let pendingCount = 0;
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
696
|
+
const handleWriteError = (error) => {
|
|
697
|
+
if (writeFailed) return;
|
|
698
|
+
writeFailed = true;
|
|
699
|
+
options.onError?.(error);
|
|
700
|
+
writer.abort(error);
|
|
701
|
+
cleanup();
|
|
702
|
+
rejectShell(error);
|
|
703
|
+
rejectAll(error);
|
|
704
|
+
};
|
|
705
|
+
const enqueueWrite = (chunk) => {
|
|
706
|
+
if (writeFailed) return;
|
|
707
|
+
const trackWrite = (promise) => {
|
|
708
|
+
const tracked = promise.then(
|
|
709
|
+
() => {
|
|
710
|
+
if (writeChain === tracked) {
|
|
711
|
+
writeChain = null;
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
(error) => {
|
|
715
|
+
if (writeChain === tracked) {
|
|
716
|
+
writeChain = null;
|
|
717
|
+
}
|
|
718
|
+
handleWriteError(error);
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
writeChain = tracked;
|
|
722
|
+
};
|
|
723
|
+
const writeAsync = () => Promise.resolve(writer.write(chunk)).then(() => void 0);
|
|
724
|
+
if (writeChain) {
|
|
725
|
+
trackWrite(writeChain.then(writeAsync));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
const result = writer.write(chunk);
|
|
730
|
+
if (isPromiseLike(result)) {
|
|
731
|
+
trackWrite(
|
|
732
|
+
result.then(
|
|
733
|
+
() => void 0,
|
|
734
|
+
(error) => {
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
handleWriteError(error);
|
|
505
742
|
}
|
|
506
|
-
|
|
507
|
-
|
|
743
|
+
};
|
|
744
|
+
const afterWrites = (fn) => {
|
|
745
|
+
const pending = writeChain;
|
|
746
|
+
if (!pending) {
|
|
747
|
+
if (!writeFailed) {
|
|
748
|
+
fn();
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
508
751
|
}
|
|
752
|
+
void pending.then(() => {
|
|
753
|
+
if (!writeFailed) {
|
|
754
|
+
fn();
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
const markShellReady = () => {
|
|
759
|
+
afterWrites(() => {
|
|
760
|
+
control.onShellFlushed?.();
|
|
761
|
+
resolveShell();
|
|
762
|
+
options.onShellReady?.();
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
const writeSnapshotForScopes = (scopeIds) => {
|
|
766
|
+
runInSession(() => {
|
|
767
|
+
if (!includeSnapshot || scopeIds.length === 0) return;
|
|
768
|
+
const registry = __fictGetScopeRegistry();
|
|
769
|
+
const pending = scopeIds.filter((id) => registry.has(id) && !sentScopes.has(id));
|
|
770
|
+
if (pending.length === 0) return;
|
|
771
|
+
const snapshot = __fictSerializeSSRStateForScopes(pending);
|
|
772
|
+
const ids = Object.keys(snapshot.scopes);
|
|
773
|
+
if (ids.length === 0) return;
|
|
774
|
+
const chunk = buildIncrementalSnapshotChunk(snapshot, resolvedOptions);
|
|
775
|
+
if (chunk) {
|
|
776
|
+
enqueueWrite(chunk);
|
|
777
|
+
}
|
|
778
|
+
for (const id of ids) {
|
|
779
|
+
sentScopes.add(id);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
509
782
|
};
|
|
510
783
|
const writeSnapshotForBoundary = (boundary) => {
|
|
511
|
-
|
|
512
|
-
|
|
784
|
+
runInSession(() => {
|
|
785
|
+
const scopes = __fictGetScopesForBoundary(boundary);
|
|
786
|
+
writeSnapshotForScopes(scopes);
|
|
787
|
+
});
|
|
513
788
|
};
|
|
514
789
|
const writeRemainingSnapshots = () => {
|
|
515
|
-
|
|
516
|
-
|
|
790
|
+
runInSession(() => {
|
|
791
|
+
const scopes = Array.from(__fictGetScopeRegistry().keys());
|
|
792
|
+
writeSnapshotForScopes(scopes);
|
|
793
|
+
});
|
|
517
794
|
};
|
|
518
795
|
const cleanup = () => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
796
|
+
removeAbortListener();
|
|
797
|
+
removeAbortListener = () => {
|
|
798
|
+
};
|
|
799
|
+
runInSession(() => {
|
|
800
|
+
__fictSetSSRStreamHooks(null);
|
|
801
|
+
__fictDisableSSR();
|
|
802
|
+
});
|
|
523
803
|
try {
|
|
524
804
|
teardown();
|
|
525
805
|
} catch {
|
|
806
|
+
} finally {
|
|
807
|
+
restoreGlobals2();
|
|
808
|
+
restoreManifest();
|
|
526
809
|
}
|
|
527
810
|
};
|
|
528
811
|
const finalize = () => {
|
|
@@ -534,23 +817,27 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
534
817
|
injectSnapshot(dom.document, container, snapshot, resolvedOptions);
|
|
535
818
|
}
|
|
536
819
|
const fullHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
820
|
+
enqueueWrite(fullHtml);
|
|
821
|
+
afterWrites(() => {
|
|
822
|
+
writer.close();
|
|
823
|
+
cleanup();
|
|
824
|
+
resolveShell();
|
|
825
|
+
resolveAll();
|
|
826
|
+
options.onShellReady?.();
|
|
827
|
+
options.onAllReady?.();
|
|
828
|
+
});
|
|
544
829
|
return;
|
|
545
830
|
}
|
|
546
831
|
writeRemainingSnapshots();
|
|
547
832
|
if (tailHtml) {
|
|
548
|
-
|
|
833
|
+
enqueueWrite(tailHtml);
|
|
549
834
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
835
|
+
afterWrites(() => {
|
|
836
|
+
writer.close();
|
|
837
|
+
cleanup();
|
|
838
|
+
resolveAll();
|
|
839
|
+
options.onAllReady?.();
|
|
840
|
+
});
|
|
554
841
|
};
|
|
555
842
|
const maybeFinalize = () => {
|
|
556
843
|
if (pendingCount === 0) {
|
|
@@ -580,7 +867,7 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
580
867
|
writeSnapshotForBoundary(id);
|
|
581
868
|
if (dom) {
|
|
582
869
|
const html = serializeBetween(dom.document, entry.start, entry.end);
|
|
583
|
-
|
|
870
|
+
enqueueWrite(buildPatchChunk(id, html, resolvedOptions));
|
|
584
871
|
}
|
|
585
872
|
}
|
|
586
873
|
maybeFinalize();
|
|
@@ -593,22 +880,28 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
593
880
|
const abort = (reason) => {
|
|
594
881
|
if (closed) return;
|
|
595
882
|
closed = true;
|
|
883
|
+
writeFailed = true;
|
|
596
884
|
writer.abort(reason);
|
|
597
885
|
cleanup();
|
|
598
|
-
|
|
886
|
+
const abortReason = reason ?? new Error("Stream aborted");
|
|
887
|
+
rejectShell(abortReason);
|
|
888
|
+
rejectAll(abortReason);
|
|
599
889
|
};
|
|
600
890
|
if (options.signal) {
|
|
601
891
|
if (options.signal.aborted) {
|
|
602
892
|
abort(options.signal.reason);
|
|
893
|
+
return { shellReady, allReady, abort };
|
|
603
894
|
} else {
|
|
604
|
-
|
|
895
|
+
const onAbort = () => abort(options.signal?.reason);
|
|
896
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
897
|
+
removeAbortListener = () => options.signal?.removeEventListener("abort", onAbort);
|
|
605
898
|
}
|
|
606
899
|
}
|
|
607
900
|
try {
|
|
608
901
|
__fictEnableSSR();
|
|
609
902
|
__fictSetSSRStreamHooks(hooks);
|
|
610
903
|
dom = resolveDom(resolvedOptions);
|
|
611
|
-
restoreGlobals2 = resolvedOptions.exposeGlobals
|
|
904
|
+
restoreGlobals2 = resolvedOptions.exposeGlobals === true ? installGlobals(dom.window, dom.document) : () => {
|
|
612
905
|
};
|
|
613
906
|
restoreManifest = installManifest(resolvedOptions.manifest);
|
|
614
907
|
container = resolveContainer(dom.document, resolvedOptions);
|
|
@@ -620,33 +913,31 @@ function startStreamingRender(view, options, writer, control = {}) {
|
|
|
620
913
|
return { shellReady, allReady, abort };
|
|
621
914
|
}
|
|
622
915
|
const shellHtml = serializeOutput(dom.document, container, resolvedOptions);
|
|
623
|
-
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript() : "";
|
|
916
|
+
const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript(resolvedOptions) : "";
|
|
624
917
|
if (resolvedOptions.fullDocument) {
|
|
625
918
|
const split = splitDocumentHtml(shellHtml);
|
|
626
919
|
if (!split) {
|
|
627
920
|
throw new Error("[fict/ssr] Failed to locate </body> for streaming output.");
|
|
628
921
|
}
|
|
629
922
|
if (control.includeTailInShell) {
|
|
630
|
-
|
|
923
|
+
enqueueWrite(split.head + streamRuntime);
|
|
631
924
|
tailHtml = split.tail;
|
|
632
925
|
shellCarriesTail = true;
|
|
633
926
|
} else {
|
|
634
|
-
|
|
927
|
+
enqueueWrite(split.head + streamRuntime);
|
|
635
928
|
tailHtml = split.tail;
|
|
636
929
|
}
|
|
637
930
|
} else {
|
|
638
|
-
|
|
931
|
+
enqueueWrite(shellHtml + streamRuntime);
|
|
639
932
|
}
|
|
640
933
|
wroteShell = true;
|
|
641
934
|
writeSnapshotForScopes(Array.from(__fictGetScopeRegistry().keys()));
|
|
642
935
|
if (shellCarriesTail && tailHtml) {
|
|
643
|
-
|
|
936
|
+
enqueueWrite(tailHtml);
|
|
644
937
|
tailHtml = "";
|
|
645
938
|
shellCarriesTail = false;
|
|
646
939
|
}
|
|
647
|
-
|
|
648
|
-
resolveShell();
|
|
649
|
-
options.onShellReady?.();
|
|
940
|
+
markShellReady();
|
|
650
941
|
maybeFinalize();
|
|
651
942
|
} catch (err) {
|
|
652
943
|
options.onError?.(err);
|
|
@@ -677,11 +968,28 @@ function resolveContainer(document, options) {
|
|
|
677
968
|
}
|
|
678
969
|
return container;
|
|
679
970
|
}
|
|
680
|
-
function buildStreamRuntimeScript() {
|
|
681
|
-
|
|
971
|
+
function buildStreamRuntimeScript(options) {
|
|
972
|
+
const nonce = renderNonceAttribute(options);
|
|
973
|
+
if (options.streamRuntime === "external") {
|
|
974
|
+
if (!options.streamRuntimeSrc) {
|
|
975
|
+
throw new Error('[fict/ssr] streamRuntimeSrc is required when streamRuntime is "external".');
|
|
976
|
+
}
|
|
977
|
+
return `<script${nonce} src="${escapeAttribute(options.streamRuntimeSrc)}" data-fict-stream-runtime data-fict-stream-observer></script>`;
|
|
978
|
+
}
|
|
979
|
+
return `<script${nonce}>${createStreamRuntimeCode({
|
|
980
|
+
observerMode: resolveStreamPatchMode(options) === "observer"
|
|
981
|
+
})}</script>`;
|
|
682
982
|
}
|
|
683
|
-
function buildPatchChunk(id, html) {
|
|
684
|
-
|
|
983
|
+
function buildPatchChunk(id, html, options) {
|
|
984
|
+
const template = `<template data-fict-suspense="${escapeAttribute(id)}">${html}</template>`;
|
|
985
|
+
if (resolveStreamPatchMode(options) === "observer") {
|
|
986
|
+
return template;
|
|
987
|
+
}
|
|
988
|
+
return `${template}<script${renderNonceAttribute(options)}>__FICT_STREAM.apply("${escapeScriptString(id)}")</script>`;
|
|
989
|
+
}
|
|
990
|
+
function resolveStreamPatchMode(options) {
|
|
991
|
+
if (options.streamPatchMode) return options.streamPatchMode;
|
|
992
|
+
return options.streamRuntime === "external" ? "observer" : "inline";
|
|
685
993
|
}
|
|
686
994
|
function serializeBetween(document, start, end) {
|
|
687
995
|
const wrapper = document.createElement("div");
|
|
@@ -700,11 +1008,13 @@ function splitDocumentHtml(html) {
|
|
|
700
1008
|
}
|
|
701
1009
|
function buildIncrementalSnapshotChunk(state, options) {
|
|
702
1010
|
const json = serializeSnapshotForScript(state);
|
|
1011
|
+
const nonce = renderNonceAttribute(options);
|
|
703
1012
|
if (options.snapshotTarget === "head") {
|
|
704
1013
|
const jsonLiteral = JSON.stringify(json);
|
|
705
|
-
|
|
1014
|
+
const setNonce = options.scriptNonce !== void 0 ? `s.setAttribute('nonce',${serializeScriptStringLiteral(options.scriptNonce)});` : "";
|
|
1015
|
+
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
1016
|
}
|
|
707
|
-
return `<script type="application/json" data-fict-snapshot>${json}</script>`;
|
|
1017
|
+
return `<script${nonce} type="application/json" data-fict-snapshot>${json}</script>`;
|
|
708
1018
|
}
|
|
709
1019
|
function serializeOutput(document, container, options) {
|
|
710
1020
|
if (options.fullDocument) {
|
|
@@ -721,6 +1031,9 @@ function injectSnapshot(document, container, state, options) {
|
|
|
721
1031
|
const script = document.createElement("script");
|
|
722
1032
|
script.type = "application/json";
|
|
723
1033
|
script.id = options.snapshotScriptId ?? "__FICT_SNAPSHOT__";
|
|
1034
|
+
if (options.scriptNonce !== void 0) {
|
|
1035
|
+
script.setAttribute("nonce", options.scriptNonce);
|
|
1036
|
+
}
|
|
724
1037
|
script.textContent = serializeSnapshotForScript(state);
|
|
725
1038
|
if (options.fullDocument) {
|
|
726
1039
|
if (options.snapshotTarget === "head" && document.head) {
|
|
@@ -746,6 +1059,18 @@ function injectSnapshot(document, container, state, options) {
|
|
|
746
1059
|
function serializeSnapshotForScript(state) {
|
|
747
1060
|
return JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
748
1061
|
}
|
|
1062
|
+
function serializeScriptStringLiteral(value) {
|
|
1063
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1064
|
+
}
|
|
1065
|
+
function renderNonceAttribute(options) {
|
|
1066
|
+
return options.scriptNonce === void 0 ? "" : ` nonce="${escapeAttribute(options.scriptNonce)}"`;
|
|
1067
|
+
}
|
|
1068
|
+
function escapeAttribute(value) {
|
|
1069
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1070
|
+
}
|
|
1071
|
+
function escapeScriptString(value) {
|
|
1072
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1073
|
+
}
|
|
749
1074
|
function serializeDoctype(document, override) {
|
|
750
1075
|
if (override === null) return "";
|
|
751
1076
|
if (override !== void 0) return override;
|