@fictjs/ssr 0.27.0 → 0.28.1

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 CHANGED
@@ -1,6 +1,13 @@
1
1
  # @fictjs/ssr
2
2
 
3
- Fict's Server-Side Rendering (SSR) package, providing high-performance server-side rendering and client-side resumability capabilities.
3
+ Fict's Satellite Server-Side Rendering (SSR) package, providing supported
4
+ string and streaming rendering plus opt-in client resumability.
5
+
6
+ > **Preview** — resumability, snapshot schema v2, and partial prerendering have no
7
+ > semver guarantee and are excluded from Core 1.0. They require
8
+ > `includeSnapshot: true`, compiler `resumable: true`, and the
9
+ > `fict/experimental/loader` entrypoint. Basic string and streaming SSR does not
10
+ > enable them by default.
4
11
 
5
12
  ## Table of Contents
6
13
 
@@ -20,7 +27,9 @@ Fict's Server-Side Rendering (SSR) package, providing high-performance server-si
20
27
 
21
28
  ## Overview
22
29
 
23
- Fict SSR adopts a **Resumability** architecture, which is fundamentally different from traditional Hydration:
30
+ Supported SSR produces HTML without a resumability snapshot by default. The
31
+ optional **Preview Resumability** architecture is fundamentally different from
32
+ traditional hydration:
24
33
 
25
34
  | Feature | Traditional Hydration | Fict Resumability |
26
35
  | ------------------------- | --------------------------------- | --------------------------------- |
@@ -29,7 +38,7 @@ Fict SSR adopts a **Resumability** architecture, which is fundamentally differen
29
38
  | Handler Loading | All preloaded | Lazy loaded on demand |
30
39
  | State Restoration | Re-calculated | Restored from serialized snapshot |
31
40
 
32
- ### How it Works
41
+ ### How Preview Resumability Works
33
42
 
34
43
  ```
35
44
  ┌─────────────────────────────────────────────────────────────────┐
@@ -79,7 +88,7 @@ import { renderToString } from '@fictjs/ssr'
79
88
  import { App } from './App'
80
89
 
81
90
  const html = renderToString(() => <App />, {
82
- includeSnapshot: true, // Include state snapshot (default true)
91
+ includeSnapshot: true, // Explicit Preview snapshot opt-in (default false)
83
92
  containerId: 'app',
84
93
  manifest: './dist/client/fict.manifest.json',
85
94
  })
@@ -90,7 +99,7 @@ const html = renderToString(() => <App />, {
90
99
  ```typescript
91
100
  // entry-client.tsx
92
101
  import { render } from 'fict'
93
- import { installResumableLoader } from 'fict/loader'
102
+ import { installResumableLoader } from 'fict/experimental/loader'
94
103
  import { App } from './App'
95
104
 
96
105
  // Load manifest (production)
@@ -285,7 +294,7 @@ interface RenderToStringOptions {
285
294
  doctype?: string | null
286
295
 
287
296
  // Resumability Configuration
288
- includeSnapshot?: boolean // Default: true
297
+ includeSnapshot?: boolean // Preview opt-in; default: false
289
298
  snapshotScriptId?: string // Default: '__FICT_SNAPSHOT__'
290
299
  snapshotTarget?: 'container' | 'body' | 'head'
291
300
 
@@ -299,7 +308,11 @@ By default SSR does not write `window`, `document`, `Node`, or related DOM const
299
308
  `globalThis`. This keeps concurrent renders from racing over process-global DOM state. Components
300
309
  should use Fict's render-provided document/ownerDocument paths; set `exposeGlobals: true` only for
301
310
  legacy code that still reads DOM globals during server render. That compatibility mode is restored
302
- on `dispose()`, but it is not concurrency-safe while overlapping renders are active.
311
+ on `dispose()`. Default renders may overlap each other, but a compatibility render is exclusive
312
+ with every other SSR render so no request can observe another request's process-global DOM. On a
313
+ non-extensible hardened global object, ordinary render-local SSR remains available without a
314
+ process marker; compatibility mode remains unavailable because it cannot acquire its exclusive
315
+ lease or install DOM globals.
303
316
 
304
317
  ### renderToStringAsync
305
318
 
@@ -367,6 +380,11 @@ Trusted Types deployments should use this external observer runtime. The patch
367
380
  runtime moves `<template>` content with DOM APIs (`content` + `insertBefore`) and
368
381
  does not call `innerHTML`, `insertAdjacentHTML`, `eval`, or `Function`.
369
382
 
383
+ When Preview snapshots are enabled on a nonce-free strict-CSP route, keep
384
+ `snapshotTarget` at `container` or `body`. Incremental `head` snapshots require a
385
+ small executable mover; external runtime mode rejects that combination unless
386
+ `scriptNonce` is non-empty.
387
+
370
388
  Each shell stream receives an automatic unique namespace for its Suspense patch
371
389
  identifiers. When independently cached fragments or streams from multiple
372
390
  services can be composed into one document, pass a stable
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_render_core = require("./render-core-D_EheVwh.cjs");
2
+ const require_render_core = require("./render-core-BDbPxAeQ.cjs");
3
3
  exports.renderToPartial = require_render_core.renderToPartial;
@@ -1,2 +1,2 @@
1
- import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-KCiJV_SK.cjs";
1
+ import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.cjs";
2
2
  export { type PartialPrerenderResult, renderToPartial };
@@ -1,2 +1,2 @@
1
- import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-KCiJV_SK.js";
1
+ import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.js";
2
2
  export { type PartialPrerenderResult, renderToPartial };
@@ -1,2 +1,2 @@
1
- import { r as renderToPartial } from "./render-core-D0nOT_Pe.js";
1
+ import { r as renderToPartial } from "./render-core-BgW-UZfD.js";
2
2
  export { renderToPartial };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_render_core = require("./render-core-D_EheVwh.cjs");
2
+ const require_render_core = require("./render-core-BDbPxAeQ.cjs");
3
3
  require("./node-session-carrier-CTo-1Awq.cjs");
4
4
  require("./experimental.cjs");
5
5
  exports.renderToPartial = require_render_core.renderToPartial;
@@ -1,2 +1,2 @@
1
- import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-KCiJV_SK.cjs";
1
+ import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.cjs";
2
2
  export { type PartialPrerenderResult, renderToPartial };
@@ -1,2 +1,2 @@
1
- import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-KCiJV_SK.js";
1
+ import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.js";
2
2
  export { type PartialPrerenderResult, renderToPartial };
@@ -1,4 +1,4 @@
1
- import { r as renderToPartial } from "./render-core-D0nOT_Pe.js";
1
+ import { r as renderToPartial } from "./render-core-BgW-UZfD.js";
2
2
  import "./node-session-carrier-ClqRbdoQ.js";
3
3
  import "./experimental.js";
4
4
  export { renderToPartial };
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_render_core = require("./render-core-D_EheVwh.cjs");
2
+ const require_render_core = require("./render-core-BDbPxAeQ.cjs");
3
3
  exports.createSSRDocument = require_render_core.createSSRDocument;
4
4
  exports.renderToDocument = require_render_core.renderToDocument;
5
5
  exports.renderToPipeableStream = require_render_core.renderToPipeableStream;
package/dist/index.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-KCiJV_SK.cjs";
1
+ import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.cjs";
2
2
  export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-KCiJV_SK.js";
1
+ import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.js";
2
2
  export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-D0nOT_Pe.js";
1
+ import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-BgW-UZfD.js";
2
2
  export { createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_render_core = require("./render-core-D_EheVwh.cjs");
2
+ const require_render_core = require("./render-core-BDbPxAeQ.cjs");
3
3
  require("./index.cjs");
4
4
  require("./node-session-carrier-CTo-1Awq.cjs");
5
5
  exports.createSSRDocument = require_render_core.createSSRDocument;
@@ -1,2 +1,2 @@
1
- import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-KCiJV_SK.cjs";
1
+ import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.cjs";
2
2
  export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
@@ -1,2 +1,2 @@
1
- import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-KCiJV_SK.js";
1
+ import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.js";
2
2
  export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
@@ -1,4 +1,4 @@
1
- import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-D0nOT_Pe.js";
1
+ import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-BgW-UZfD.js";
2
2
  import "./index.js";
3
3
  import "./node-session-carrier-ClqRbdoQ.js";
4
4
  export { createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
@@ -22,7 +22,55 @@ function getNodeRequire() {
22
22
  }
23
23
  //#endregion
24
24
  //#region src/globals.ts
25
- function installGlobals(window, document) {
25
+ const activeGlobalTargets = /* @__PURE__ */ new WeakSet();
26
+ const activeSharedGlobalTargets = /* @__PURE__ */ new WeakMap();
27
+ const GLOBALS_LEASE = Symbol.for("@fictjs/ssr.exposeGlobalsLease");
28
+ const SHARED_RENDER_LEASE = Symbol.for("@fictjs/ssr.sharedRenderLease");
29
+ /**
30
+ * Reserve the process-global target for an SSR render that does not expose DOM
31
+ * globals. Shared reservations may overlap each other, but they prevent a
32
+ * compatibility-global render from starting until every owner has cleaned up.
33
+ */
34
+ function acquireSharedGlobalTarget(target = globalThis) {
35
+ const local = activeSharedGlobalTargets.get(target);
36
+ if (local) {
37
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
38
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== local.state) throw poisonedSharedLeaseError();
39
+ setSharedLeaseCount(local.state, readSharedLeaseCount(local.state) + 1);
40
+ local.count += 1;
41
+ return createSharedLeaseRelease(target, local);
42
+ }
43
+ if (activeGlobalTargets.has(target)) throw renderOverlapError();
44
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
45
+ let state;
46
+ if (descriptor === void 0) {
47
+ if (isDefinitelyNonExtensible(target)) return () => {};
48
+ state = createSharedLeaseState();
49
+ try {
50
+ Object.defineProperty(target, GLOBALS_LEASE, {
51
+ configurable: true,
52
+ enumerable: false,
53
+ value: state,
54
+ writable: false
55
+ });
56
+ } catch (error) {
57
+ const failure = /* @__PURE__ */ new Error(`[fict/ssr] Failed to acquire the shared SSR render lease: ${formatError(error)}`);
58
+ failure.cause = error;
59
+ throw failure;
60
+ }
61
+ } else {
62
+ if (!("value" in descriptor) || !isSharedLeaseState(descriptor.value)) throw renderOverlapError();
63
+ state = descriptor.value;
64
+ setSharedLeaseCount(state, readSharedLeaseCount(state) + 1);
65
+ }
66
+ const localLease = {
67
+ state,
68
+ count: 1
69
+ };
70
+ activeSharedGlobalTargets.set(target, localLease);
71
+ return createSharedLeaseRelease(target, localLease);
72
+ }
73
+ function installGlobals(window, document, target = globalThis) {
26
74
  const win = window;
27
75
  const required = {
28
76
  window: win,
@@ -47,17 +95,44 @@ function installGlobals(window, document) {
47
95
  };
48
96
  const missing = Object.entries(required).filter(([, value]) => value === void 0).map(([key]) => key);
49
97
  if (missing.length) throw new Error(`[fict/ssr] Missing DOM globals: ${missing.join(", ")}`);
50
- const globals = {
98
+ const globals = Object.entries({
51
99
  ...required,
52
100
  ...optional
53
- };
54
- const keys = Object.keys(globals);
55
- const snapshot = captureGlobals(keys);
56
- for (const key of keys) {
57
- const value = globals[key];
58
- if (value !== void 0) globalThis[key] = value;
101
+ }).filter(([, value]) => value !== void 0);
102
+ const lease = acquireGlobalTarget(target);
103
+ let snapshot = [];
104
+ let installingKey = "<capture>";
105
+ try {
106
+ snapshot = captureGlobals(target, globals.map(([key]) => key));
107
+ for (const [key, value] of globals) {
108
+ installingKey = key;
109
+ const previous = snapshot.find((entry) => entry.key === key)?.descriptor;
110
+ Object.defineProperty(target, key, {
111
+ configurable: true,
112
+ enumerable: previous?.enumerable ?? true,
113
+ value,
114
+ writable: true
115
+ });
116
+ }
117
+ } catch (error) {
118
+ let rollbackError;
119
+ try {
120
+ restoreGlobals(target, snapshot);
121
+ releaseGlobalTarget(target, lease);
122
+ } catch (caught) {
123
+ rollbackError = caught;
124
+ }
125
+ const failure = /* @__PURE__ */ new Error(rollbackError === void 0 ? `[fict/ssr] Failed to install DOM global \`${installingKey}\`: ${formatError(error)}` : `[fict/ssr] Failed to install DOM global \`${installingKey}\` and roll back the partial installation: ${formatError(error)}; rollback: ${formatError(rollbackError)}`);
126
+ failure.cause = error;
127
+ throw failure;
59
128
  }
60
- return () => restoreGlobals(snapshot);
129
+ let active = true;
130
+ return () => {
131
+ if (!active) return;
132
+ restoreGlobals(target, snapshot);
133
+ releaseGlobalTarget(target, lease);
134
+ active = false;
135
+ };
61
136
  }
62
137
  function installManifest(manifest) {
63
138
  if (!manifest) return () => {};
@@ -85,22 +160,120 @@ function installManifest(manifest) {
85
160
  else delete globalThis[key];
86
161
  };
87
162
  }
88
- function captureGlobals(keys) {
163
+ function captureGlobals(target, keys) {
89
164
  const snapshot = [];
90
- for (const key of keys) {
91
- const exists = Object.prototype.hasOwnProperty.call(globalThis, key);
92
- const value = globalThis[key];
93
- snapshot.push({
94
- key,
95
- exists,
96
- value
165
+ for (const key of keys) snapshot.push({
166
+ key,
167
+ descriptor: Object.getOwnPropertyDescriptor(target, key)
168
+ });
169
+ return snapshot;
170
+ }
171
+ function restoreGlobals(target, snapshot) {
172
+ let firstError;
173
+ for (let index = snapshot.length - 1; index >= 0; index--) {
174
+ const entry = snapshot[index];
175
+ if (!entry) continue;
176
+ try {
177
+ if (entry.descriptor) Object.defineProperty(target, entry.key, entry.descriptor);
178
+ else if (!Reflect.deleteProperty(target, entry.key)) throw new Error(`Could not delete newly installed global \`${entry.key}\`.`);
179
+ } catch (error) {
180
+ firstError ??= error;
181
+ }
182
+ }
183
+ if (firstError !== void 0) throw firstError;
184
+ }
185
+ function acquireGlobalTarget(target) {
186
+ if (activeGlobalTargets.has(target) || activeSharedGlobalTargets.has(target) || Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE) !== void 0) throw renderOverlapError();
187
+ const lease = {};
188
+ activeGlobalTargets.add(target);
189
+ try {
190
+ Object.defineProperty(target, GLOBALS_LEASE, {
191
+ configurable: true,
192
+ enumerable: false,
193
+ value: lease,
194
+ writable: false
97
195
  });
196
+ } catch (error) {
197
+ activeGlobalTargets.delete(target);
198
+ const failure = /* @__PURE__ */ new Error(`[fict/ssr] Failed to acquire the process DOM-global compatibility lease: ${formatError(error)}`);
199
+ failure.cause = error;
200
+ throw failure;
98
201
  }
99
- return snapshot;
202
+ return lease;
203
+ }
204
+ function releaseGlobalTarget(target, lease) {
205
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
206
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== lease) throw new Error("[fict/ssr] The process DOM-global compatibility lease changed before cleanup.");
207
+ if (!Reflect.deleteProperty(target, GLOBALS_LEASE)) throw new Error("[fict/ssr] Could not release the process DOM-global compatibility lease.");
208
+ activeGlobalTargets.delete(target);
209
+ }
210
+ function formatError(error) {
211
+ return error instanceof Error ? error.message : String(error);
212
+ }
213
+ function isDefinitelyNonExtensible(target) {
214
+ try {
215
+ return !Object.isExtensible(target);
216
+ } catch {
217
+ return false;
218
+ }
219
+ }
220
+ function createSharedLeaseState() {
221
+ const state = {};
222
+ Object.defineProperties(state, {
223
+ [SHARED_RENDER_LEASE]: {
224
+ configurable: false,
225
+ enumerable: false,
226
+ value: true,
227
+ writable: false
228
+ },
229
+ count: {
230
+ configurable: false,
231
+ enumerable: false,
232
+ value: 1,
233
+ writable: true
234
+ }
235
+ });
236
+ return state;
237
+ }
238
+ function isSharedLeaseState(value) {
239
+ if (typeof value !== "object" || value === null) return false;
240
+ const marker = Object.getOwnPropertyDescriptor(value, SHARED_RENDER_LEASE);
241
+ const count = Object.getOwnPropertyDescriptor(value, "count");
242
+ return marker !== void 0 && "value" in marker && marker.value === true && count !== void 0 && "value" in count && Number.isSafeInteger(count.value) && count.value > 0 && count.writable === true;
243
+ }
244
+ function readSharedLeaseCount(state) {
245
+ const descriptor = Object.getOwnPropertyDescriptor(state, "count");
246
+ if (!descriptor || !("value" in descriptor) || !Number.isSafeInteger(descriptor.value) || descriptor.value <= 0 || descriptor.writable !== true) throw poisonedSharedLeaseError();
247
+ return descriptor.value;
100
248
  }
101
- function restoreGlobals(snapshot) {
102
- for (const entry of snapshot) if (entry.exists) globalThis[entry.key] = entry.value;
103
- else delete globalThis[entry.key];
249
+ function setSharedLeaseCount(state, count) {
250
+ try {
251
+ Object.defineProperty(state, "count", { value: count });
252
+ } catch (error) {
253
+ const failure = poisonedSharedLeaseError();
254
+ failure.cause = error;
255
+ throw failure;
256
+ }
257
+ }
258
+ function createSharedLeaseRelease(target, local) {
259
+ let active = true;
260
+ return () => {
261
+ if (!active) return;
262
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
263
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== local.state) throw poisonedSharedLeaseError();
264
+ const count = readSharedLeaseCount(local.state);
265
+ if (count > 1) setSharedLeaseCount(local.state, count - 1);
266
+ else if (!Reflect.deleteProperty(target, GLOBALS_LEASE)) throw new Error("[fict/ssr] Could not release the shared SSR render lease.");
267
+ local.count -= 1;
268
+ if (local.count === 0) activeSharedGlobalTargets.delete(target);
269
+ active = false;
270
+ };
271
+ }
272
+ function renderOverlapError() {
273
+ return /* @__PURE__ */ new Error("[fict/ssr] `exposeGlobals: true` cannot be used by overlapping or nested renders, including renders that do not expose globals. Keep DOM access render-local, or wait for every active render to clean up.");
274
+ }
275
+ function poisonedSharedLeaseError() {
276
+ return /* @__PURE__ */ new Error("[fict/ssr] The shared SSR render lease changed before cleanup.");
104
277
  }
105
278
  function readTextFileFromPath(path) {
106
279
  const deno = globalThis.Deno;
@@ -996,18 +1169,21 @@ function renderToDocument(view, options = {}) {
996
1169
  function renderToDocumentInSession(view, options) {
997
1170
  validateScopeIdentifierPrefix(options.scopeIdentifierPrefix);
998
1171
  const scopeIdentifierPrefix = resolveScopeIdentifierPrefix(options.scopeIdentifierPrefix);
999
- const includeSnapshot = options.includeSnapshot !== false;
1172
+ const includeSnapshot = options.includeSnapshot === true;
1000
1173
  (0, _fictjs_runtime_internal.__fictEnableSSR)();
1001
1174
  (0, _fictjs_runtime_internal.__fictSetSSRScopeIdentifierPrefix)(scopeIdentifierPrefix);
1002
1175
  let dom;
1003
1176
  let restoreGlobals = () => {};
1004
1177
  let restoreManifest = () => {};
1178
+ let releaseRenderLease = () => {};
1005
1179
  let container;
1006
1180
  let teardown = () => {};
1007
1181
  try {
1182
+ const shouldExpose = options.exposeGlobals === true;
1183
+ releaseRenderLease = shouldExpose ? () => {} : acquireSharedGlobalTarget();
1008
1184
  dom = resolveDom(options);
1009
1185
  const { document, window } = dom;
1010
- restoreGlobals = options.exposeGlobals === true ? installGlobals(window, document) : () => {};
1186
+ restoreGlobals = shouldExpose ? installGlobals(window, document) : () => {};
1011
1187
  restoreManifest = installManifest(options.manifest);
1012
1188
  container = resolveContainer(document, options);
1013
1189
  teardown = (0, _fictjs_runtime.render)(view, container);
@@ -1017,7 +1193,7 @@ function renderToDocumentInSession(view, options) {
1017
1193
  }
1018
1194
  } catch (error) {
1019
1195
  (0, _fictjs_runtime_internal.__fictDisableSSR)();
1020
- cleanupRenderResources(teardown, restoreGlobals, restoreManifest, true);
1196
+ cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, true);
1021
1197
  throw error;
1022
1198
  }
1023
1199
  (0, _fictjs_runtime_internal.__fictDisableSSR)();
@@ -1025,10 +1201,10 @@ function renderToDocumentInSession(view, options) {
1025
1201
  try {
1026
1202
  html = serializeOutput(dom.document, container, options);
1027
1203
  } catch (error) {
1028
- cleanupRenderResources(teardown, restoreGlobals, restoreManifest, true);
1204
+ cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, true);
1029
1205
  throw error;
1030
1206
  }
1031
- const dispose = () => cleanupRenderResources(teardown, restoreGlobals, restoreManifest, false);
1207
+ const dispose = () => cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, false);
1032
1208
  return {
1033
1209
  html,
1034
1210
  document: dom.document,
@@ -1216,13 +1392,14 @@ function resolveDom(options) {
1216
1392
  function isPromiseLike(value) {
1217
1393
  return typeof value === "object" && value !== null && typeof value.then === "function";
1218
1394
  }
1219
- function cleanupRenderResources(teardown, restoreGlobals, restoreManifest, suppressErrors) {
1395
+ function cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, suppressErrors) {
1220
1396
  let failed = false;
1221
1397
  let firstError;
1222
1398
  for (const cleanup of [
1223
1399
  teardown,
1224
1400
  restoreGlobals,
1225
- restoreManifest
1401
+ restoreManifest,
1402
+ releaseRenderLease
1226
1403
  ]) try {
1227
1404
  cleanup();
1228
1405
  } catch (error) {
@@ -1273,6 +1450,7 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1273
1450
  let dom = null;
1274
1451
  let restoreGlobals = () => {};
1275
1452
  let restoreManifest = () => {};
1453
+ let releaseRenderLease = () => {};
1276
1454
  let teardown = () => {};
1277
1455
  let container = null;
1278
1456
  let closed = false;
@@ -1290,7 +1468,7 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1290
1468
  const mode = options.mode ?? "shell";
1291
1469
  const scopeIdentifierPrefix = resolveScopeIdentifierPrefix(options.scopeIdentifierPrefix);
1292
1470
  const streamIdentifierPrefix = mode === "shell" ? resolveStreamIdentifierPrefix(options.streamIdentifierPrefix) : null;
1293
- const includeSnapshot = options.includeSnapshot !== false;
1471
+ const includeSnapshot = options.includeSnapshot === true;
1294
1472
  const sentScopeSnapshots = /* @__PURE__ */ new Map();
1295
1473
  const boundaryMap = /* @__PURE__ */ new Map();
1296
1474
  let boundaryId = 0;
@@ -1397,7 +1575,7 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1397
1575
  });
1398
1576
  } catch {}
1399
1577
  try {
1400
- cleanupRenderResources(teardown, restoreGlobals, restoreManifest, true);
1578
+ cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, true);
1401
1579
  } finally {
1402
1580
  releaseSession();
1403
1581
  }
@@ -1553,11 +1731,14 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1553
1731
  removeAbortListener = () => options.signal?.removeEventListener("abort", onAbort);
1554
1732
  }
1555
1733
  try {
1734
+ validateExternalHeadSnapshotCsp(resolvedOptions, includeSnapshot, mode);
1556
1735
  (0, _fictjs_runtime_internal.__fictEnableSSR)();
1557
1736
  (0, _fictjs_runtime_internal.__fictSetSSRScopeIdentifierPrefix)(scopeIdentifierPrefix);
1558
1737
  (0, _fictjs_runtime_internal.__fictSetSSRStreamHooks)(hooks);
1738
+ const shouldExpose = resolvedOptions.exposeGlobals === true;
1739
+ releaseRenderLease = shouldExpose ? () => {} : acquireSharedGlobalTarget();
1559
1740
  dom = resolveDom(resolvedOptions);
1560
- restoreGlobals = resolvedOptions.exposeGlobals === true ? installGlobals(dom.window, dom.document) : () => {};
1741
+ restoreGlobals = shouldExpose ? installGlobals(dom.window, dom.document) : () => {};
1561
1742
  restoreManifest = installManifest(resolvedOptions.manifest);
1562
1743
  container = resolveContainer(dom.document, resolvedOptions);
1563
1744
  teardown = (0, _fictjs_runtime.render)(view, container);
@@ -1632,6 +1813,9 @@ function buildStreamRuntimeScript(options) {
1632
1813
  }
1633
1814
  return `<script${nonce}>${require_stream_runtime.createStreamRuntimeCode({ observerMode: resolveStreamPatchMode(options) === "observer" })}<\/script>`;
1634
1815
  }
1816
+ function validateExternalHeadSnapshotCsp(options, includeSnapshot, mode) {
1817
+ if (includeSnapshot && mode === "shell" && options.snapshotTarget === "head" && options.streamRuntime === "external" && !options.scriptNonce) throw new Error("[fict/ssr] Streaming Preview snapshots with `snapshotTarget: \"head\"` and an external runtime require a non-empty `scriptNonce`, because incremental head insertion uses an inline executable mover. Use `snapshotTarget: \"container\"` or `\"body\"` for a nonce-free strict CSP route.");
1818
+ }
1635
1819
  function buildPatchChunk(id, content, options) {
1636
1820
  const namespaceAttribute = content.namespace ? ` data-fict-patch-namespace="${content.namespace}"` : "";
1637
1821
  const html = content.namespace === "svg" ? `<svg>${content.html}</svg>` : content.namespace === "mathml" ? `<math>${content.html}</math>` : content.html;