@modern-js/runtime 3.9.2 → 3.9.3

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.
@@ -101,12 +101,14 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
101
101
  status: -1
102
102
  };
103
103
  const activeDeferreds = new Map();
104
+ const deferredScriptKeys = new Map();
104
105
  return storage.run({
105
106
  headers: headersData,
106
107
  request,
107
108
  monitors: options.monitors,
108
109
  responseProxy,
109
110
  activeDeferreds,
111
+ deferredScriptKeys,
110
112
  serverPayload: void 0
111
113
  }, async ()=>{
112
114
  const Root = createRoot();
@@ -1,4 +1,5 @@
1
1
  import { PassThrough, Readable, Transform } from "stream";
2
+ import { StringDecoder } from "string_decoder";
2
3
  import { storage } from "@modern-js/runtime-utils/node";
3
4
  import { SSR_HYDRATION_ID_PREFIX } from "@modern-js/utils/universal/constants";
4
5
  import { ESCAPED_SHELL_STREAM_END_MARK } from "../../../common.mjs";
@@ -6,6 +7,7 @@ import { RenderLevel } from "../../constants.mjs";
6
7
  import { getGlobalInternalRuntimeContext } from "../../context/index.mjs";
7
8
  import { getMonitors } from "../../context/monitors/index.mjs";
8
9
  import { enqueueFromEntries } from "./deferredScript.mjs";
10
+ import { DeferredScriptOutputCoordinator } from "./deferredScriptOutputCoordinator.mjs";
9
11
  import { ShellChunkStatus, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
10
12
  import { getTemplates } from "./template.mjs";
11
13
  const defaultExtender = {
@@ -54,7 +56,14 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
54
56
  entryName,
55
57
  styledComponentsStyleTags
56
58
  }).then(({ shellAfter, shellBefore })=>{
59
+ const decoder = new StringDecoder('utf8');
57
60
  const pendingScripts = [];
61
+ const outputState = {};
62
+ let deferredResolversComplete = Promise.resolve();
63
+ const writeReact = (content)=>{
64
+ if (!outputState.coordinator) throw new Error("Deferred script coordinator is not ready");
65
+ outputState.coordinator.writeReact(content);
66
+ };
58
67
  const body = new Transform({
59
68
  transform (chunk, _encoding, callback) {
60
69
  try {
@@ -66,17 +75,49 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
66
75
  const beforeMark = concatedChunk.slice(0, markerIndex);
67
76
  const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
68
77
  shellChunkStatus = ShellChunkStatus.FINISH;
69
- this.push(`${shellBefore}${beforeMark}${shellAfter}`);
70
- if (afterMark) this.push(afterMark);
71
- if (pendingScripts.length > 0) for (const s of pendingScripts)this.push(s);
78
+ writeReact(`${shellBefore}${beforeMark}${shellAfter}`);
79
+ if (afterMark) writeReact(afterMark);
80
+ if (outputState.coordinator) {
81
+ outputState.coordinator.markShellFinished();
82
+ for (const script of pendingScripts)outputState.coordinator.enqueueResolver(script);
83
+ pendingScripts.length = 0;
84
+ }
72
85
  }
73
- } else this.push(chunk);
86
+ } else {
87
+ const decodedChunk = decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
88
+ writeReact(decodedChunk);
89
+ }
74
90
  callback();
75
91
  } catch (e) {
76
92
  e instanceof Error ? callback(e) : callback(new Error('Received unknown error when streaming'));
77
93
  }
94
+ },
95
+ flush (callback) {
96
+ try {
97
+ writeReact(decoder.end());
98
+ } catch (error) {
99
+ callback(error instanceof Error ? error : new Error('Received unknown error when streaming'));
100
+ return;
101
+ }
102
+ deferredResolversComplete.then(()=>{
103
+ try {
104
+ outputState.coordinator?.finish();
105
+ callback();
106
+ } catch (error) {
107
+ callback(error instanceof Error ? error : new Error('Received unknown error when streaming'));
108
+ }
109
+ }, (error)=>{
110
+ callback(error instanceof Error ? error : new Error('Received unknown error when streaming'));
111
+ });
78
112
  }
79
113
  });
114
+ const coordinator = new DeferredScriptOutputCoordinator((content)=>{
115
+ body.push(content);
116
+ });
117
+ outputState.coordinator = coordinator;
118
+ body.once('close', ()=>{
119
+ coordinator.abort();
120
+ });
80
121
  const passThrough = new PassThrough();
81
122
  const stream = Readable.toWeb(body);
82
123
  resolve(stream);
@@ -84,23 +125,25 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
84
125
  extenders.forEach((extender)=>{
85
126
  if (extender.processStream) processedStream = extender.processStream(processedStream);
86
127
  });
87
- reactStreamingPipe(passThrough);
88
- processedStream.pipe(body);
89
128
  try {
90
129
  const storageContext = storage.useContext?.();
91
130
  const activeDeferreds = storageContext?.activeDeferreds;
131
+ const deferredScriptKeys = storageContext?.deferredScriptKeys;
92
132
  const entries = activeDeferreds instanceof Map ? Array.from(activeDeferreds.entries()) : [];
93
133
  if (entries.length > 0) {
94
134
  const enqueueScript = (s)=>{
95
- if (shellChunkStatus === ShellChunkStatus.FINISH) body.write(s);
135
+ if (!outputState.coordinator) throw new Error("Deferred script coordinator is not ready");
136
+ if (shellChunkStatus === ShellChunkStatus.FINISH) outputState.coordinator.enqueueResolver(s);
96
137
  else pendingScripts.push(s);
97
138
  };
98
- enqueueFromEntries(entries, config.nonce, (s)=>enqueueScript(s));
139
+ deferredResolversComplete = enqueueFromEntries(entries, deferredScriptKeys, config.nonce, enqueueScript);
99
140
  }
100
141
  } catch (err) {
101
142
  const monitors = getMonitors();
102
143
  monitors.error("cannot inject router data script", err);
103
144
  }
145
+ reactStreamingPipe(passThrough);
146
+ processedStream.pipe(body);
104
147
  });
105
148
  },
106
149
  onShellError (error) {
@@ -3,6 +3,7 @@ import { storage } from "@modern-js/runtime-utils/node";
3
3
  import { ESCAPED_SHELL_STREAM_END_MARK } from "../../../common.mjs";
4
4
  import { RenderLevel } from "../../constants.mjs";
5
5
  import { enqueueFromEntries } from "./deferredScript.mjs";
6
+ import { DeferredScriptOutputCoordinator } from "./deferredScriptOutputCoordinator.mjs";
6
7
  import { ShellChunkStatus, encodeForWebStream, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
7
8
  import { getTemplates } from "./template.mjs";
8
9
  const createReadableStreamFromElement = async (request, rootElement, options)=>{
@@ -35,10 +36,13 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
35
36
  const { waitForAllReady } = resolveStreamingMode(request, forceStreamToString);
36
37
  if (waitForAllReady) await readableOriginal.allReady;
37
38
  const reader = readableOriginal.getReader();
39
+ let coordinator;
38
40
  const stream = new ReadableStream({
39
41
  start (controller) {
42
+ const decoder = new TextDecoder();
40
43
  const pendingScripts = [];
41
44
  let isClosed = false;
45
+ let deferredResolversComplete = Promise.resolve();
42
46
  const safeEnqueue = (chunk)=>{
43
47
  if (isClosed) return;
44
48
  try {
@@ -55,38 +59,55 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
55
59
  } catch {}
56
60
  }
57
61
  };
58
- const flushPendingScripts = ()=>{
59
- for (const s of pendingScripts)safeEnqueue(encodeForWebStream(s));
60
- pendingScripts.length = 0;
61
- };
62
- const enqueueScript = (script)=>{
63
- if (shellChunkStatus === ShellChunkStatus.FINISH) safeEnqueue(encodeForWebStream(script));
64
- else pendingScripts.push(script);
65
- };
62
+ coordinator = new DeferredScriptOutputCoordinator((content)=>{
63
+ safeEnqueue(encodeForWebStream(content));
64
+ });
66
65
  const storageContext = storage.useContext?.();
67
66
  const activeDeferreds = storageContext?.activeDeferreds;
67
+ const deferredScriptKeys = storageContext?.deferredScriptKeys;
68
68
  const entries = activeDeferreds instanceof Map ? Array.from(activeDeferreds.entries()) : [];
69
- if (entries.length > 0) enqueueFromEntries(entries, config.nonce, enqueueScript);
69
+ if (entries.length > 0) deferredResolversComplete = enqueueFromEntries(entries, deferredScriptKeys, config.nonce, (script)=>{
70
+ if (!coordinator) throw new Error("Deferred script coordinator is not ready");
71
+ if (shellChunkStatus === ShellChunkStatus.FINISH) coordinator.enqueueResolver(script);
72
+ else pendingScripts.push(script);
73
+ });
70
74
  async function push() {
71
75
  try {
72
76
  const { done, value } = await reader.read();
73
- if (done) return void closeController();
77
+ if (done) {
78
+ const trailingText = decoder.decode();
79
+ if (trailingText) coordinator?.writeReact(trailingText);
80
+ await deferredResolversComplete;
81
+ coordinator?.finish();
82
+ closeController();
83
+ return;
84
+ }
74
85
  if (isClosed) return;
75
86
  if (shellChunkStatus !== ShellChunkStatus.FINISH) {
76
- chunkVec.push(new TextDecoder().decode(value));
87
+ chunkVec.push(decoder.decode(value, {
88
+ stream: true
89
+ }));
77
90
  const concatedChunk = chunkVec.join('');
78
91
  const markerIndex = concatedChunk.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
79
92
  if (-1 !== markerIndex) {
80
93
  const beforeMark = concatedChunk.slice(0, markerIndex);
81
94
  const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
82
95
  shellChunkStatus = ShellChunkStatus.FINISH;
83
- safeEnqueue(encodeForWebStream(`${shellBefore}${beforeMark}${shellAfter}`));
84
- if (afterMark) safeEnqueue(encodeForWebStream(afterMark));
85
- flushPendingScripts();
96
+ coordinator?.writeReact(`${shellBefore}${beforeMark}${shellAfter}`);
97
+ if (afterMark) coordinator?.writeReact(afterMark);
98
+ coordinator?.markShellFinished();
99
+ for (const script of pendingScripts)coordinator?.enqueueResolver(script);
100
+ pendingScripts.length = 0;
86
101
  }
87
- } else safeEnqueue(value);
102
+ } else {
103
+ const decodedChunk = decoder.decode(value, {
104
+ stream: true
105
+ });
106
+ if (decodedChunk) coordinator?.writeReact(decodedChunk);
107
+ }
88
108
  if (!isClosed) push();
89
109
  } catch (error) {
110
+ coordinator?.abort();
90
111
  if (!isClosed) {
91
112
  isClosed = true;
92
113
  try {
@@ -98,6 +119,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
98
119
  push();
99
120
  },
100
121
  cancel (reason) {
122
+ coordinator?.abort();
101
123
  reader.cancel(reason).catch(()=>{});
102
124
  }
103
125
  });
@@ -40,23 +40,29 @@ function buildDeferredDataScript(nonce, args) {
40
40
  const nonceAttr = nonce ? ` nonce="${nonce}"` : '';
41
41
  return `<script async${nonceAttr} data-fn-name="r" data-script-src="modern-run-router-data-fn" data-fn-args='${escaped}' suppressHydrationWarning>${runRouterDataFnStr}</script>`;
42
42
  }
43
- function enqueueFromEntries(entries, nonce, emit) {
43
+ function enqueueFromEntries(entries, deferredScriptKeys, nonce, emit) {
44
+ const pendingResolvers = [];
44
45
  entries.forEach(([routeId, value])=>{
45
46
  if (!isDeferredDataLike(value)) return;
46
- const pendingKeys = new Set(value.pendingKeys ?? []);
47
- pendingKeys.forEach((key)=>{
47
+ const serializedKeys = deferredScriptKeys ? deferredScriptKeys.get(routeId) ?? [] : value.pendingKeys ?? [];
48
+ serializedKeys.forEach((key)=>{
48
49
  const tracked = value.data?.[key];
49
- if (isPromiseLike(tracked)) tracked.then((val)=>emit(buildDeferredDataScript(nonce, [
50
+ if (isPromiseLike(tracked)) pendingResolvers.push(Promise.resolve(tracked).then((val)=>{
51
+ emit(buildDeferredDataScript(nonce, [
50
52
  routeId,
51
53
  key,
52
54
  val
53
- ])), (err)=>emit(buildDeferredDataScript(nonce, [
55
+ ]));
56
+ }, (err)=>{
57
+ emit(buildDeferredDataScript(nonce, [
54
58
  routeId,
55
59
  key,
56
60
  void 0,
57
61
  toErrorInfo(err)
58
- ])));
62
+ ]));
63
+ }));
59
64
  });
60
65
  });
66
+ return Promise.all(pendingResolvers).then(()=>void 0);
61
67
  }
62
68
  export { buildDeferredDataScript, enqueueFromEntries, escapeAttr, isDeferredDataLike, isPromiseLike, isRecord, toErrorInfo };
@@ -0,0 +1,332 @@
1
+ import { SSR_HYDRATION_ID_PREFIX } from "@modern-js/utils/universal/constants";
2
+ const VOID_ELEMENTS = new Set([
3
+ 'area',
4
+ 'base',
5
+ 'br',
6
+ 'col',
7
+ 'embed',
8
+ 'hr',
9
+ 'img',
10
+ 'input',
11
+ 'link',
12
+ 'meta',
13
+ 'param',
14
+ 'source',
15
+ 'track',
16
+ 'wbr'
17
+ ]);
18
+ const RAW_TEXT_ELEMENTS = new Set([
19
+ 'iframe',
20
+ 'noembed',
21
+ 'noframes',
22
+ "script",
23
+ 'style',
24
+ 'textarea',
25
+ 'title',
26
+ 'xmp'
27
+ ]);
28
+ const REACT_SEGMENT_ID_PATTERN = new RegExp(`^${SSR_HYDRATION_ID_PREFIX}S:[0-9a-f]+$`);
29
+ const TABLE_SEGMENT_CHILDREN = new Set([
30
+ 'tbody',
31
+ 'tr',
32
+ 'colgroup'
33
+ ]);
34
+ function isAsciiAlpha(char) {
35
+ return /[A-Za-z]/.test(char);
36
+ }
37
+ function isWhitespace(char) {
38
+ return /[\t\n\f\r ]/.test(char);
39
+ }
40
+ function getSuffix(value, pattern) {
41
+ const start = Math.max(0, value.length - pattern.length);
42
+ for(let index = start; index < value.length; index++){
43
+ const suffix = value.slice(index);
44
+ if (pattern.startsWith(suffix)) return suffix;
45
+ }
46
+ return '';
47
+ }
48
+ class DeferredScriptOutputCoordinator {
49
+ enqueueResolver(script) {
50
+ if (this.closed) return;
51
+ this.pendingResolvers.push(script);
52
+ this.flushResolvers();
53
+ }
54
+ markShellFinished() {
55
+ if (this.closed) return;
56
+ this.shellFinished = true;
57
+ this.flushResolvers();
58
+ }
59
+ writeReact(chunk) {
60
+ if (this.closed || !chunk) return;
61
+ this.flushResolvers();
62
+ let emittedUntil = 0;
63
+ for(let index = 0; index < chunk.length; index++){
64
+ const wasSafe = this.isSafeBoundary();
65
+ this.consume(chunk[index]);
66
+ if (!wasSafe && this.isSafeBoundary() && this.pendingResolvers.length) {
67
+ this.emit(chunk.slice(emittedUntil, index + 1));
68
+ emittedUntil = index + 1;
69
+ this.flushResolvers();
70
+ }
71
+ }
72
+ if (emittedUntil < chunk.length) this.emit(chunk.slice(emittedUntil));
73
+ }
74
+ finish() {
75
+ if (this.closed) return;
76
+ this.flushResolvers();
77
+ this.closed = true;
78
+ if (this.pendingResolvers.length > 0) throw new Error("Cannot emit deferred resolver scripts because the React stream ended outside a safe HTML boundary.");
79
+ }
80
+ abort() {
81
+ this.closed = true;
82
+ this.pendingResolvers.length = 0;
83
+ }
84
+ isSafeBoundary() {
85
+ return this.shellFinished && 'data' === this.state && 0 === this.openElements.length && !this.awaitingContinuationProtocol;
86
+ }
87
+ flushResolvers() {
88
+ if (!this.isSafeBoundary() || 0 === this.pendingResolvers.length) return;
89
+ while(this.pendingResolvers.length > 0){
90
+ const resolver = this.pendingResolvers.shift();
91
+ if (resolver) this.emit(resolver);
92
+ }
93
+ }
94
+ consume(char) {
95
+ switch(this.state){
96
+ case 'data':
97
+ if ('<' === char) this.state = 'tag-open';
98
+ return;
99
+ case 'tag-open':
100
+ if ('/' === char) {
101
+ this.currentTagName = '';
102
+ this.state = 'end-tag-name';
103
+ } else if ('!' === char) {
104
+ this.declarationPrefix = '';
105
+ this.state = 'markup-declaration';
106
+ } else if ('?' === char) this.state = 'bogus-comment';
107
+ else if (isAsciiAlpha(char)) {
108
+ this.startTag(char);
109
+ this.state = 'start-tag-name';
110
+ } else this.state = 'data';
111
+ return;
112
+ case 'start-tag-name':
113
+ if (isWhitespace(char)) this.state = 'before-attribute-name';
114
+ else if ('/' === char) this.state = 'self-closing-start-tag';
115
+ else if ('>' === char) this.completeStartTag(false);
116
+ else this.currentTagName += char.toLowerCase();
117
+ return;
118
+ case 'before-attribute-name':
119
+ if (isWhitespace(char)) return;
120
+ if ('/' === char) this.state = 'self-closing-start-tag';
121
+ else if ('>' === char) this.completeStartTag(false);
122
+ else {
123
+ this.startAttribute(char);
124
+ this.state = 'attribute-name';
125
+ }
126
+ return;
127
+ case 'attribute-name':
128
+ if (isWhitespace(char)) this.state = 'after-attribute-name';
129
+ else if ('=' === char) {
130
+ this.currentAttributeHasValue = true;
131
+ this.state = 'before-attribute-value';
132
+ } else if ('/' === char) {
133
+ this.completeAttribute();
134
+ this.state = 'self-closing-start-tag';
135
+ } else if ('>' === char) this.completeStartTag(false);
136
+ else this.currentAttributeName += char.toLowerCase();
137
+ return;
138
+ case 'after-attribute-name':
139
+ if (isWhitespace(char)) return;
140
+ if ('=' === char) {
141
+ this.currentAttributeHasValue = true;
142
+ this.state = 'before-attribute-value';
143
+ } else if ('/' === char) {
144
+ this.completeAttribute();
145
+ this.state = 'self-closing-start-tag';
146
+ } else if ('>' === char) this.completeStartTag(false);
147
+ else {
148
+ this.completeAttribute();
149
+ this.startAttribute(char);
150
+ this.state = 'attribute-name';
151
+ }
152
+ return;
153
+ case 'before-attribute-value':
154
+ if (isWhitespace(char)) return;
155
+ if ('"' === char) this.state = 'attribute-value-double-quoted';
156
+ else if ("'" === char) this.state = 'attribute-value-single-quoted';
157
+ else if ('>' === char) this.completeStartTag(false);
158
+ else {
159
+ this.currentAttributeValue += char;
160
+ this.state = 'attribute-value-unquoted';
161
+ }
162
+ return;
163
+ case 'attribute-value-double-quoted':
164
+ if ('"' === char) {
165
+ this.completeAttribute();
166
+ this.state = 'before-attribute-name';
167
+ } else this.currentAttributeValue += char;
168
+ return;
169
+ case 'attribute-value-single-quoted':
170
+ if ("'" === char) {
171
+ this.completeAttribute();
172
+ this.state = 'before-attribute-name';
173
+ } else this.currentAttributeValue += char;
174
+ return;
175
+ case 'attribute-value-unquoted':
176
+ if (isWhitespace(char)) {
177
+ this.completeAttribute();
178
+ this.state = 'before-attribute-name';
179
+ } else if ('>' === char) this.completeStartTag(false);
180
+ else this.currentAttributeValue += char;
181
+ return;
182
+ case 'self-closing-start-tag':
183
+ if ('>' === char) this.completeStartTag(true);
184
+ else if (!isWhitespace(char)) {
185
+ this.startAttribute(char);
186
+ this.state = 'attribute-name';
187
+ }
188
+ return;
189
+ case 'end-tag-name':
190
+ if (isWhitespace(char)) this.state = 'after-end-tag-name';
191
+ else if ('>' === char) this.completeEndTag();
192
+ else this.currentTagName += char.toLowerCase();
193
+ return;
194
+ case 'after-end-tag-name':
195
+ if ('>' === char) this.completeEndTag();
196
+ return;
197
+ case 'markup-declaration':
198
+ if (this.declarationPrefix.length < 2 && '-' === char) {
199
+ this.declarationPrefix += char;
200
+ if ('--' === this.declarationPrefix) {
201
+ this.commentTail = '';
202
+ this.state = 'comment';
203
+ }
204
+ } else this.state = '>' === char ? 'data' : 'declaration';
205
+ return;
206
+ case 'declaration':
207
+ if ('>' === char) this.state = 'data';
208
+ return;
209
+ case 'comment':
210
+ this.commentTail = `${this.commentTail}${char}`.slice(-3);
211
+ if ('-->' === this.commentTail) this.state = 'data';
212
+ return;
213
+ case 'bogus-comment':
214
+ if ('>' === char) this.state = 'data';
215
+ return;
216
+ case 'raw-text':
217
+ this.consumeRawText(char);
218
+ return;
219
+ case 'raw-end-tag':
220
+ if ('>' === char) this.completeRawTextEndTag();
221
+ else if (!isWhitespace(char)) {
222
+ this.state = 'raw-text';
223
+ this.rawTextSearch = getSuffix(`${this.rawTextSearch}${char.toLowerCase()}`, `</${this.rawTextElement}`);
224
+ }
225
+ }
226
+ }
227
+ startTag(char) {
228
+ this.currentTagName = char.toLowerCase();
229
+ this.currentAttributes = new Map();
230
+ this.currentAttributeName = '';
231
+ this.currentAttributeValue = '';
232
+ this.currentAttributeHasValue = false;
233
+ }
234
+ startAttribute(char) {
235
+ this.currentAttributeName = char.toLowerCase();
236
+ this.currentAttributeValue = '';
237
+ this.currentAttributeHasValue = false;
238
+ }
239
+ completeAttribute() {
240
+ if (!this.currentAttributeName) return;
241
+ this.currentAttributes.set(this.currentAttributeName, this.currentAttributeHasValue ? this.currentAttributeValue : true);
242
+ this.currentAttributeName = '';
243
+ this.currentAttributeValue = '';
244
+ this.currentAttributeHasValue = false;
245
+ }
246
+ completeStartTag(selfClosing) {
247
+ this.completeAttribute();
248
+ const tagName = this.currentTagName;
249
+ const id = this.currentAttributes.get('id');
250
+ const hasReactSegmentId = 'string' == typeof id && REACT_SEGMENT_ID_PATTERN.test(id);
251
+ const hasHiddenAttribute = this.currentAttributes.has('hidden');
252
+ const isHiddenSegmentTable = 'table' === tagName && hasHiddenAttribute && !hasReactSegmentId;
253
+ const isTableSegmentChild = hasReactSegmentId && TABLE_SEGMENT_CHILDREN.has(tagName) && this.openElements[this.openElements.length - 1]?.isSegmentTableWrapper === true;
254
+ const isContinuationContainer = hasReactSegmentId && ('div' === tagName && hasHiddenAttribute || ('svg' === tagName || 'math' === tagName) && void 0 !== id && 'true' === this.currentAttributes.get('aria-hidden') && 'display:none' === this.currentAttributes.get('style') || 'table' === tagName && hasHiddenAttribute || isTableSegmentChild);
255
+ this.continuationProtocolScript = "script" === tagName && this.awaitingContinuationProtocol;
256
+ this.continuationProtocolTail = '';
257
+ this.hasContinuationProtocolCall = false;
258
+ if (!selfClosing && !VOID_ELEMENTS.has(tagName)) this.openElements.push({
259
+ tagName,
260
+ isContinuationContainer,
261
+ isSegmentTableWrapper: isHiddenSegmentTable
262
+ });
263
+ this.currentTagName = '';
264
+ this.currentAttributes = new Map();
265
+ this.currentAttributeName = '';
266
+ this.currentAttributeValue = '';
267
+ this.currentAttributeHasValue = false;
268
+ if (!selfClosing && RAW_TEXT_ELEMENTS.has(tagName)) {
269
+ this.rawTextElement = tagName;
270
+ this.rawTextSearch = '';
271
+ this.state = 'raw-text';
272
+ } else this.state = 'data';
273
+ }
274
+ completeEndTag() {
275
+ this.closeOpenElement(this.currentTagName);
276
+ this.currentTagName = '';
277
+ this.state = 'data';
278
+ }
279
+ consumeRawText(char) {
280
+ const rawTextElement = this.rawTextElement;
281
+ if (!rawTextElement) {
282
+ this.state = 'data';
283
+ return;
284
+ }
285
+ const endTagPrefix = `</${rawTextElement}`;
286
+ if (this.continuationProtocolScript) {
287
+ this.continuationProtocolTail = `${this.continuationProtocolTail}${char}`.slice(-4);
288
+ if (/\$(?:RC|RS|RX|RB|RR)\b/.test(this.continuationProtocolTail)) this.hasContinuationProtocolCall = true;
289
+ }
290
+ this.rawTextSearch = getSuffix(`${this.rawTextSearch}${char.toLowerCase()}`, endTagPrefix);
291
+ if (this.rawTextSearch === endTagPrefix) this.state = 'raw-end-tag';
292
+ }
293
+ completeRawTextEndTag() {
294
+ const rawTextElement = this.rawTextElement;
295
+ if (rawTextElement) this.closeOpenElement(rawTextElement);
296
+ if ("script" === rawTextElement && this.continuationProtocolScript && this.hasContinuationProtocolCall) this.awaitingContinuationProtocol = false;
297
+ this.rawTextElement = void 0;
298
+ this.rawTextSearch = '';
299
+ this.continuationProtocolScript = false;
300
+ this.continuationProtocolTail = '';
301
+ this.hasContinuationProtocolCall = false;
302
+ this.state = 'data';
303
+ }
304
+ closeOpenElement(tagName) {
305
+ const matchingIndex = this.openElements.map((element)=>element.tagName).lastIndexOf(tagName);
306
+ if (-1 === matchingIndex) return;
307
+ const closedElements = this.openElements.splice(matchingIndex);
308
+ const closedContinuation = closedElements.some((element)=>element.isContinuationContainer);
309
+ if (closedContinuation) this.awaitingContinuationProtocol = true;
310
+ }
311
+ constructor(emit){
312
+ this.emit = emit;
313
+ this.pendingResolvers = [];
314
+ this.openElements = [];
315
+ this.shellFinished = false;
316
+ this.closed = false;
317
+ this.state = 'data';
318
+ this.currentTagName = '';
319
+ this.currentAttributes = new Map();
320
+ this.currentAttributeName = '';
321
+ this.currentAttributeValue = '';
322
+ this.currentAttributeHasValue = false;
323
+ this.declarationPrefix = '';
324
+ this.commentTail = '';
325
+ this.rawTextSearch = '';
326
+ this.continuationProtocolScript = false;
327
+ this.continuationProtocolTail = '';
328
+ this.hasContinuationProtocolCall = false;
329
+ this.awaitingContinuationProtocol = false;
330
+ }
331
+ }
332
+ export { DeferredScriptOutputCoordinator };
@@ -13,7 +13,8 @@ const DeferredDataScripts = (props)=>{
13
13
  }, []);
14
14
  const deferredScripts = useMemo(()=>{
15
15
  if (!staticContext) return null;
16
- const activeDeferreds = storage.useContext().activeDeferreds;
16
+ const requestStorage = storage.useContext();
17
+ const activeDeferreds = requestStorage.activeDeferreds;
17
18
  const _ROUTER_DATA = {
18
19
  loaderData: staticContext.loaderData,
19
20
  errors: serializeErrors(staticContext.errors)
@@ -26,6 +27,7 @@ const DeferredDataScripts = (props)=>{
26
27
  const deferredDataScripts = [];
27
28
  const initialScripts = Array.from(activeDeferreds.entries()).map(([routeId, deferredData])=>{
28
29
  const pendingKeys = new Set(deferredData.pendingKeys);
30
+ requestStorage.deferredScriptKeys?.set(routeId, Array.from(pendingKeys));
29
31
  const { deferredKeys } = deferredData;
30
32
  const deferredKeyPromiseManifests = deferredKeys.map((key)=>{
31
33
  if (pendingKeys.has(key)) return {
@@ -102,12 +102,14 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
102
102
  status: -1
103
103
  };
104
104
  const activeDeferreds = new Map();
105
+ const deferredScriptKeys = new Map();
105
106
  return storage.run({
106
107
  headers: headersData,
107
108
  request,
108
109
  monitors: options.monitors,
109
110
  responseProxy,
110
111
  activeDeferreds,
112
+ deferredScriptKeys,
111
113
  serverPayload: void 0
112
114
  }, async ()=>{
113
115
  const Root = createRoot();