@microsoft/webui-framework 0.0.19 → 0.0.20

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.
@@ -0,0 +1,367 @@
1
+ import { abortStreamingGate, markBoundaryCommitted, markBoundaryPending, } from './lifecycle.js';
2
+ import { activateRootsBetween, } from './streaming-activation.js';
3
+ import { applyBoundaryBootstrap } from './streaming-bootstrap.js';
4
+ import { abandonDeferredDocumentRoots, abandonDeferredRange, } from './streaming-cleanup.js';
5
+ import { abandonPendingWaiters, configureStreamingFailureHandler, elementHasPendingStateForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests, resetDeferredActivationForTests, } from './streaming-deferred.js';
6
+ import { findBoundaryScript, findEndMarkerByPrefix, findStartMarkerByPrefix, removeBoundaryScaffolding, resolveBoundaryRange, streamingErrorMessage, } from './streaming-dom.js';
7
+ import { parseBoundaryEnvelope, RECORD_KIND_STATE_UPDATE, RECORD_KIND_TERMINAL, RECORD_KIND_UPDATABLE_CHECKPOINT, } from './streaming-protocol.js';
8
+ const MAX_QUEUED_BOUNDARIES = 512;
9
+ const MAX_UPDATABLE_BOUNDARIES = 128;
10
+ const MAX_RETAINED_UPDATE_ROOTS = 50_000;
11
+ const BOUNDARY_HYDRATED_EVENT = 'webui:boundary-hydrated';
12
+ const BOUNDARY_MARK_PREFIX = 'webui:boundary:';
13
+ const UPDATE_MARK_SUFFIX = ':update';
14
+ const TERMINAL_MARK = 'webui:streaming:terminal';
15
+ const perf = globalThis.performance;
16
+ const queue = [];
17
+ let queueHead = 0;
18
+ let pumpScheduled = false;
19
+ let slicedDrainActive = false;
20
+ let halted = false;
21
+ let nextExpectedRecordSequence = 0;
22
+ let nextExpectedBoundaryId = 0;
23
+ let terminalCommitted = false;
24
+ let pendingTerminalSequence = null;
25
+ let terminalValidationScheduled = false;
26
+ let coordinatorGeneration = 0;
27
+ let retainedUpdateRoots = 0;
28
+ const updatableBoundaries = new Map();
29
+ configureStreamingFailureHandler(fail);
30
+ export function enqueueStreamingSentinel(sentinel) {
31
+ if (halted) {
32
+ discardRejectedBoundary(sentinel);
33
+ abandonDeferredDocumentRoots();
34
+ return;
35
+ }
36
+ if (queue.length - queueHead >= MAX_QUEUED_BOUNDARIES) {
37
+ failBoundary(sentinel, `queued boundary count exceeds ${MAX_QUEUED_BOUNDARIES}`);
38
+ return;
39
+ }
40
+ queue.push(sentinel);
41
+ if (!pumpScheduled) {
42
+ pumpScheduled = true;
43
+ queueMicrotask(drainQueue);
44
+ }
45
+ }
46
+ function drainQueue() {
47
+ const budget = sliceBudgetMs();
48
+ if (budget > 0) {
49
+ void drainSliced(budget);
50
+ return;
51
+ }
52
+ pumpScheduled = false;
53
+ while (queueHead < queue.length) {
54
+ const sentinel = queue[queueHead];
55
+ queueHead++;
56
+ if (!halted)
57
+ processSentinel(sentinel);
58
+ }
59
+ queue.length = 0;
60
+ queueHead = 0;
61
+ scheduleTerminalValidation();
62
+ }
63
+ function sliceBudgetMs() {
64
+ const raw = window
65
+ .__WEBUI_STREAMING_SLICE_MS__;
66
+ return typeof raw === 'number' && raw > 0 ? raw : 0;
67
+ }
68
+ function nowMs() {
69
+ return perf ? perf.now() : Date.now();
70
+ }
71
+ async function drainSliced(budget) {
72
+ const generation = coordinatorGeneration;
73
+ slicedDrainActive = true;
74
+ let deadline = nowMs() + budget;
75
+ while (queueHead < queue.length) {
76
+ const sentinel = queue[queueHead];
77
+ queueHead++;
78
+ if (!halted)
79
+ processSentinel(sentinel);
80
+ if (nowMs() >= deadline) {
81
+ await yieldToRenderer();
82
+ if (generation !== coordinatorGeneration) {
83
+ slicedDrainActive = false;
84
+ return;
85
+ }
86
+ deadline = nowMs() + budget;
87
+ }
88
+ }
89
+ slicedDrainActive = false;
90
+ pumpScheduled = false;
91
+ queue.length = 0;
92
+ queueHead = 0;
93
+ scheduleTerminalValidation();
94
+ }
95
+ function yieldToRenderer() {
96
+ const scheduler = globalThis.scheduler;
97
+ if (scheduler?.yield)
98
+ return scheduler.yield();
99
+ return new Promise((resolve) => {
100
+ setTimeout(resolve, 0);
101
+ });
102
+ }
103
+ function fail(reason) {
104
+ halted = true;
105
+ console.error(`[WebUI] streaming hydration halted: ${reason}.`);
106
+ abortStreamingGate();
107
+ abandonPendingWaiters();
108
+ clearUpdatableBoundaries();
109
+ settlePendingTerminal(false);
110
+ for (let i = queueHead; i < queue.length; i++) {
111
+ discardRejectedBoundary(queue[i]);
112
+ }
113
+ queue.length = 0;
114
+ queueHead = 0;
115
+ abandonDeferredDocumentRoots();
116
+ }
117
+ function failBoundary(sentinel, reason) {
118
+ discardRejectedBoundary(sentinel);
119
+ fail(reason);
120
+ }
121
+ function discardRejectedBoundary(sentinel) {
122
+ const scriptEl = findBoundaryScript(sentinel);
123
+ let endMarker = null;
124
+ let startMarker = null;
125
+ if (scriptEl) {
126
+ endMarker = findEndMarkerByPrefix(scriptEl);
127
+ if (endMarker)
128
+ startMarker = findStartMarkerByPrefix(endMarker);
129
+ }
130
+ if (startMarker && endMarker) {
131
+ abandonDeferredRange(startMarker, endMarker);
132
+ }
133
+ removeBoundaryScaffolding(sentinel, scriptEl, startMarker, endMarker);
134
+ }
135
+ function processSentinel(sentinel) {
136
+ if (terminalCommitted) {
137
+ failBoundary(sentinel, 'boundary arrived after the terminal streaming record');
138
+ return;
139
+ }
140
+ const scriptEl = findBoundaryScript(sentinel);
141
+ if (!scriptEl) {
142
+ failBoundary(sentinel, 'missing boundary payload script before <webui-hydrate>');
143
+ return;
144
+ }
145
+ const parsed = parseBoundaryEnvelope(scriptEl.textContent ?? '');
146
+ if (!parsed.ok) {
147
+ failBoundary(sentinel, parsed.reason);
148
+ return;
149
+ }
150
+ const [, sequence, kind, target, payload] = parsed.envelope;
151
+ if (sequence !== nextExpectedRecordSequence) {
152
+ failBoundary(sentinel, `expected streaming record sequence ${nextExpectedRecordSequence}, received ${sequence}`);
153
+ return;
154
+ }
155
+ if (kind === RECORD_KIND_STATE_UPDATE) {
156
+ const boundary = updatableBoundaries.get(target);
157
+ if (!boundary) {
158
+ failBoundary(sentinel, `state update targets boundary ${target}, which is not committed as updatable`);
159
+ return;
160
+ }
161
+ nextExpectedRecordSequence++;
162
+ commitStateUpdate(boundary, payload, target, sequence, sentinel, scriptEl);
163
+ return;
164
+ }
165
+ if (kind === RECORD_KIND_TERMINAL) {
166
+ const resolved = resolveBoundaryRange(scriptEl, 0, true);
167
+ if (!resolved.ok) {
168
+ failBoundary(sentinel, resolved.reason);
169
+ return;
170
+ }
171
+ nextExpectedRecordSequence++;
172
+ commitTerminal(sequence, sentinel, scriptEl);
173
+ return;
174
+ }
175
+ if (target !== nextExpectedBoundaryId) {
176
+ failBoundary(sentinel, `expected boundary ID ${nextExpectedBoundaryId}, received ${target}`);
177
+ return;
178
+ }
179
+ const resolved = resolveBoundaryRange(scriptEl, target, false);
180
+ if (!resolved.ok) {
181
+ if (resolved.truncated) {
182
+ if (resolved.start) {
183
+ abandonDeferredRange(resolved.start, scriptEl);
184
+ }
185
+ removeBoundaryScaffolding(sentinel, scriptEl, resolved.start, null);
186
+ fail(resolved.reason);
187
+ }
188
+ else {
189
+ failBoundary(sentinel, resolved.reason);
190
+ }
191
+ return;
192
+ }
193
+ nextExpectedRecordSequence++;
194
+ nextExpectedBoundaryId++;
195
+ commitCheckpoint(payload, resolved.range, sequence, target, kind === RECORD_KIND_UPDATABLE_CHECKPOINT, sentinel, scriptEl);
196
+ }
197
+ function commitCheckpoint(bootstrap, range, sequence, target, updatable, sentinel, scriptEl) {
198
+ markBoundaryPending();
199
+ let committed = false;
200
+ try {
201
+ applyBoundaryBootstrap(bootstrap);
202
+ if (range.start && range.end) {
203
+ const boundary = updatable
204
+ ? { roots: [], pendingRoots: 0 }
205
+ : undefined;
206
+ activateRootsBetween(range.start, range.end, bootstrap.state, boundary);
207
+ if (boundary)
208
+ retainUpdatableBoundary(target, boundary);
209
+ }
210
+ else if (updatable) {
211
+ retainUpdatableBoundary(target, { roots: [], pendingRoots: 0 });
212
+ }
213
+ committed = true;
214
+ }
215
+ catch (error) {
216
+ fail(`error committing boundary ${sequence}: ${streamingErrorMessage(error)}`);
217
+ }
218
+ finally {
219
+ removeBoundaryScaffolding(sentinel, scriptEl, range.start, range.end);
220
+ if (committed) {
221
+ notifyCommit(`${BOUNDARY_MARK_PREFIX}${target}`, sequence, 'checkpoint');
222
+ }
223
+ markBoundaryCommitted(false);
224
+ }
225
+ }
226
+ function retainUpdatableBoundary(target, boundary) {
227
+ if (updatableBoundaries.size >= MAX_UPDATABLE_BOUNDARIES) {
228
+ throw new Error(`updatable boundary count exceeds ${MAX_UPDATABLE_BOUNDARIES}`);
229
+ }
230
+ if (retainedUpdateRoots + boundary.roots.length >
231
+ MAX_RETAINED_UPDATE_ROOTS) {
232
+ throw new Error(`retained update root count exceeds ${MAX_RETAINED_UPDATE_ROOTS}`);
233
+ }
234
+ retainedUpdateRoots += boundary.roots.length;
235
+ updatableBoundaries.set(target, boundary);
236
+ }
237
+ function commitStateUpdate(boundary, patch, target, sequence, sentinel, scriptEl) {
238
+ try {
239
+ if (boundary.pendingRoots > 0) {
240
+ if (boundary.patch) {
241
+ Object.assign(boundary.patch, patch);
242
+ }
243
+ else {
244
+ boundary.patch = Object.assign(Object.create(null), patch);
245
+ }
246
+ }
247
+ for (let i = 0; i < boundary.roots.length; i++) {
248
+ const root = boundary.roots[i];
249
+ if (root.hasAttribute('data-ws'))
250
+ continue;
251
+ try {
252
+ if (typeof root.setState !== 'function') {
253
+ throw new Error('no setState() method');
254
+ }
255
+ root.setState(patch);
256
+ }
257
+ catch (error) {
258
+ console.error(`[WebUI] streaming: state update failed for <${root.tagName.toLowerCase()}>: ${streamingErrorMessage(error)}`);
259
+ }
260
+ }
261
+ notifyCommit(`${BOUNDARY_MARK_PREFIX}${target}${UPDATE_MARK_SUFFIX}`, sequence, 'update');
262
+ }
263
+ catch (error) {
264
+ fail(`error applying state update to boundary ${target}: ${streamingErrorMessage(error)}`);
265
+ }
266
+ finally {
267
+ removeBoundaryScaffolding(sentinel, scriptEl, null, null);
268
+ }
269
+ }
270
+ function commitTerminal(sequence, sentinel, scriptEl) {
271
+ markBoundaryPending();
272
+ removeBoundaryScaffolding(sentinel, scriptEl, null, null);
273
+ terminalCommitted = true;
274
+ pendingTerminalSequence = sequence;
275
+ clearUpdatableBoundaries();
276
+ scheduleTerminalValidation();
277
+ }
278
+ function clearUpdatableBoundaries() {
279
+ updatableBoundaries.clear();
280
+ retainedUpdateRoots = 0;
281
+ }
282
+ function notifyCommit(mark, sequence, kind) {
283
+ perf?.mark(mark);
284
+ if (window
285
+ .__WEBUI_STREAMING_DEBUG__ !== true) {
286
+ return;
287
+ }
288
+ window.dispatchEvent(new CustomEvent(BOUNDARY_HYDRATED_EVENT, {
289
+ detail: { sequence, terminal: kind === 'terminal', kind },
290
+ }));
291
+ }
292
+ function scheduleTerminalValidation() {
293
+ if (pendingTerminalSequence === null ||
294
+ terminalValidationScheduled ||
295
+ slicedDrainActive) {
296
+ return;
297
+ }
298
+ terminalValidationScheduled = true;
299
+ const generation = coordinatorGeneration;
300
+ queueMicrotask(() => validatePendingTerminal(generation));
301
+ }
302
+ function validatePendingTerminal(generation) {
303
+ if (generation !== coordinatorGeneration)
304
+ return;
305
+ terminalValidationScheduled = false;
306
+ if (pendingTerminalSequence === null)
307
+ return;
308
+ if (pumpScheduled || queueHead < queue.length) {
309
+ scheduleTerminalValidation();
310
+ return;
311
+ }
312
+ settlePendingTerminal(!halted);
313
+ }
314
+ function settlePendingTerminal(success) {
315
+ const sequence = pendingTerminalSequence;
316
+ if (sequence === null)
317
+ return;
318
+ pendingTerminalSequence = null;
319
+ if (success)
320
+ notifyCommit(TERMINAL_MARK, sequence, 'terminal');
321
+ markBoundaryCommitted(success);
322
+ }
323
+ export function installStreamingTruncationGuard() {
324
+ if (typeof document === 'undefined')
325
+ return;
326
+ const generation = coordinatorGeneration;
327
+ if (document.readyState === 'loading') {
328
+ document.addEventListener('DOMContentLoaded', () => onDomContentLoaded(generation), { once: true });
329
+ }
330
+ else {
331
+ queueMicrotask(() => onDomContentLoaded(generation));
332
+ }
333
+ }
334
+ function onDomContentLoaded(generation) {
335
+ if (generation !== coordinatorGeneration)
336
+ return;
337
+ if (halted) {
338
+ abandonDeferredDocumentRoots();
339
+ return;
340
+ }
341
+ if (terminalCommitted)
342
+ return;
343
+ fail('response ended at DOMContentLoaded before the terminal streaming boundary committed (truncated stream)');
344
+ }
345
+ export function resetStreamingCoordinatorStateForTests() {
346
+ resetDeferredActivationForTests();
347
+ settlePendingTerminal(false);
348
+ clearUpdatableBoundaries();
349
+ coordinatorGeneration++;
350
+ queue.length = 0;
351
+ queueHead = 0;
352
+ pumpScheduled = false;
353
+ slicedDrainActive = false;
354
+ halted = false;
355
+ nextExpectedRecordSequence = 0;
356
+ nextExpectedBoundaryId = 0;
357
+ terminalCommitted = false;
358
+ pendingTerminalSequence = null;
359
+ terminalValidationScheduled = false;
360
+ }
361
+ export function isStreamingHaltedForTests() {
362
+ return halted;
363
+ }
364
+ export function streamingRetentionStateForTests() {
365
+ return [updatableBoundaries.size, retainedUpdateRoots];
366
+ }
367
+ export { elementHasPendingStateForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests, };
@@ -0,0 +1,22 @@
1
+ export declare const ACTIVATION_MISSING_TEMPLATE = 3;
2
+ export declare const ELEMENT_IGNORED = 0;
3
+ export declare const ELEMENT_DEFERRED = 4;
4
+ export declare const ELEMENT_LIMIT_FAILURE = 5;
5
+ export declare const MAX_PENDING_UNDEFINED_ROOTS = 50000;
6
+ export interface PendingBoundaryUpdates {
7
+ readonly roots: Element[];
8
+ pendingRoots: number;
9
+ patch?: Record<string, unknown>;
10
+ }
11
+ export interface DeferredActivationOptions {
12
+ updates?: PendingBoundaryUpdates;
13
+ retainedRoots?: Element[];
14
+ }
15
+ export declare function configureStreamingFailureHandler(handler: (reason: string) => void): void;
16
+ export declare function activateElement(el: Element, state: Record<string, unknown> | undefined, updates?: PendingBoundaryUpdates): number;
17
+ export declare function activateDeferredTree(first: Node | null, root: Node, end: Node | null, state: Record<string, unknown> | undefined, options?: DeferredActivationOptions): string | null;
18
+ export declare function abandonPendingWaiters(): void;
19
+ export declare function resetDeferredActivationForTests(): void;
20
+ export declare function pendingTagWaiterCountForTests(): number;
21
+ export declare function pendingUndefinedRootCountForTests(): number;
22
+ export declare function elementHasPendingStateForTests(el: Element): boolean;
@@ -0,0 +1,273 @@
1
+ import { markLateActivationPending, settleLateActivation, } from './lifecycle.js';
2
+ import { abandonDeferredDescendants, abandonDeferredElement, } from './streaming-cleanup.js';
3
+ import { firstNodeWithin, MAX_ELEMENTS_PER_BOUNDARY, MAX_MARKER_SCAN_NODES, nextAfterSubtreeWithin, nextWithinRoot, safeRemoveAttribute, streamingErrorMessage, } from './streaming-dom.js';
4
+ import { PENDING_ROOT_CONNECTED, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
5
+ const ACTIVATION_ACTIVATED = 1;
6
+ const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
7
+ export const ACTIVATION_MISSING_TEMPLATE = 3;
8
+ export const ELEMENT_IGNORED = 0;
9
+ export const ELEMENT_DEFERRED = 4;
10
+ export const ELEMENT_LIMIT_FAILURE = 5;
11
+ export const MAX_PENDING_UNDEFINED_ROOTS = 50_000;
12
+ const pendingTagWaiters = new Map();
13
+ let pendingUndefinedRoots = 0;
14
+ let activationGeneration = 0;
15
+ let failureHandler = null;
16
+ const PENDING_BOUNDARY_STATE = Symbol();
17
+ const PENDING_BOUNDARY_UPDATES = Symbol();
18
+ const NO_BOUNDARY_STATE = Symbol();
19
+ export function configureStreamingFailureHandler(handler) {
20
+ failureHandler = handler;
21
+ }
22
+ function fail(reason) {
23
+ if (!failureHandler) {
24
+ throw new Error('streaming failure handler is not installed');
25
+ }
26
+ failureHandler(reason);
27
+ }
28
+ export function activateElement(el, state, updates) {
29
+ if (!el.hasAttribute(STREAMED_HOST_ATTR))
30
+ return ELEMENT_IGNORED;
31
+ const tag = el.tagName.toLowerCase();
32
+ if (tag.indexOf('-') === -1)
33
+ return ELEMENT_IGNORED;
34
+ if (customElements.get(tag))
35
+ return invokeActivationHook(el, state);
36
+ if (!hasPendingState(el) &&
37
+ pendingUndefinedRoots >= MAX_PENDING_UNDEFINED_ROOTS) {
38
+ return ELEMENT_LIMIT_FAILURE;
39
+ }
40
+ stashPendingState(el, state, updates);
41
+ let waiter = pendingTagWaiters.get(tag);
42
+ if (!waiter) {
43
+ waiter = { generation: activationGeneration, roots: new Set() };
44
+ pendingTagWaiters.set(tag, waiter);
45
+ markLateActivationPending();
46
+ const generation = activationGeneration;
47
+ customElements
48
+ .whenDefined(tag)
49
+ .then(() => onTagDefined(tag, generation));
50
+ }
51
+ if (!waiter.roots.has(el)) {
52
+ waiter.roots.add(el);
53
+ pendingUndefinedRoots++;
54
+ }
55
+ el[PENDING_ROOT_CONNECTED] = resumePendingRoot;
56
+ return ELEMENT_DEFERRED;
57
+ }
58
+ function onTagDefined(tag, generation) {
59
+ if (generation !== activationGeneration)
60
+ return;
61
+ const waiter = pendingTagWaiters.get(tag);
62
+ if (!waiter || waiter.generation !== generation)
63
+ return;
64
+ for (const el of waiter.roots) {
65
+ if (!el.isConnected) {
66
+ try {
67
+ customElements.upgrade(el);
68
+ }
69
+ catch (error) {
70
+ fail(`failed to upgrade detached <${tag}>: ${streamingErrorMessage(error)}`);
71
+ return;
72
+ }
73
+ const hook = el[STREAMING_BOUNDARY_ACTIVATE];
74
+ if (el.hasAttribute(STREAMED_HOST_ATTR) &&
75
+ typeof hook !== 'function') {
76
+ fail(`upgrading detached <${tag}> did not install its streaming activation hook`);
77
+ return;
78
+ }
79
+ }
80
+ activatePendingRoot(tag, waiter, el);
81
+ }
82
+ }
83
+ function resumePendingRoot() {
84
+ const tag = this.tagName.toLowerCase();
85
+ const waiter = pendingTagWaiters.get(tag);
86
+ if (!waiter ||
87
+ waiter.generation !== activationGeneration ||
88
+ !waiter.roots.has(this)) {
89
+ delete this[PENDING_ROOT_CONNECTED];
90
+ return;
91
+ }
92
+ activatePendingRoot(tag, waiter, this);
93
+ }
94
+ function activatePendingRoot(tag, waiter, el) {
95
+ if (!waiter.roots.delete(el))
96
+ return;
97
+ pendingUndefinedRoots--;
98
+ delete el[PENDING_ROOT_CONNECTED];
99
+ let updates;
100
+ try {
101
+ updates = takePendingUpdates(el);
102
+ const state = mergePendingUpdates(takePendingState(el), updates);
103
+ const outcome = invokeActivationHook(el, state);
104
+ if (outcome === ACTIVATION_MISSING_TEMPLATE) {
105
+ abandonDeferredDescendants(el);
106
+ abandonDeferredElement(el);
107
+ fail(`template metadata missing while activating <${tag}>`);
108
+ return;
109
+ }
110
+ const failure = activateDeferredTree(firstNodeWithin(el), el, null, state, updates ? { updates } : undefined);
111
+ if (failure)
112
+ fail(failure);
113
+ }
114
+ catch (error) {
115
+ abandonDeferredDescendants(el);
116
+ reportActivationFailure(tag, error);
117
+ }
118
+ finally {
119
+ if (updates?.pendingRoots === 0)
120
+ updates.patch = undefined;
121
+ if (waiter.roots.size === 0 &&
122
+ pendingTagWaiters.get(tag) === waiter) {
123
+ pendingTagWaiters.delete(tag);
124
+ settleLateActivation();
125
+ }
126
+ }
127
+ }
128
+ export function activateDeferredTree(first, root, end, state, options) {
129
+ let node = first;
130
+ let resumeAfterDeferred = null;
131
+ let skippingDeferredDescendants = false;
132
+ let visited = 0;
133
+ let elements = 0;
134
+ while (node && node !== end) {
135
+ if (visited >= MAX_MARKER_SCAN_NODES) {
136
+ return `streaming boundary walk exceeds ${MAX_MARKER_SCAN_NODES} nodes`;
137
+ }
138
+ visited++;
139
+ if (node.nodeType === 1) {
140
+ const el = node;
141
+ if (options?.retainedRoots &&
142
+ el.hasAttribute(STREAMED_HOST_ATTR)) {
143
+ if (options.retainedRoots.length >= MAX_ELEMENTS_PER_BOUNDARY) {
144
+ return `updatable streaming boundary exceeds ${MAX_ELEMENTS_PER_BOUNDARY} roots`;
145
+ }
146
+ options.retainedRoots.push(el);
147
+ }
148
+ }
149
+ if (skippingDeferredDescendants &&
150
+ node !== resumeAfterDeferred) {
151
+ node = nextWithinRoot(node, root);
152
+ continue;
153
+ }
154
+ skippingDeferredDescendants = false;
155
+ if (node.nodeType === 1) {
156
+ if (elements >= MAX_ELEMENTS_PER_BOUNDARY) {
157
+ return `streaming boundary exceeds ${MAX_ELEMENTS_PER_BOUNDARY} elements`;
158
+ }
159
+ elements++;
160
+ try {
161
+ const outcome = activateElement(node, state, options?.updates);
162
+ if (outcome === ACTIVATION_MISSING_TEMPLATE) {
163
+ return `template metadata missing while activating <${node.tagName.toLowerCase()}>`;
164
+ }
165
+ if (outcome === ELEMENT_LIMIT_FAILURE) {
166
+ return `pending undefined root count exceeds ${MAX_PENDING_UNDEFINED_ROOTS}`;
167
+ }
168
+ if (outcome === ELEMENT_DEFERRED) {
169
+ resumeAfterDeferred = nextAfterSubtreeWithin(node, root);
170
+ skippingDeferredDescendants = true;
171
+ }
172
+ }
173
+ catch (error) {
174
+ reportActivationFailure(node.tagName.toLowerCase(), error);
175
+ }
176
+ }
177
+ node = nextWithinRoot(node, root);
178
+ }
179
+ return end && node !== end
180
+ ? 'streaming boundary end marker became unreachable during activation'
181
+ : null;
182
+ }
183
+ function reportActivationFailure(tag, error) {
184
+ console.error(`[WebUI] streaming: late activation failed for <${tag}>: ${streamingErrorMessage(error)}`);
185
+ }
186
+ export function abandonPendingWaiters() {
187
+ if (pendingTagWaiters.size === 0) {
188
+ pendingUndefinedRoots = 0;
189
+ return;
190
+ }
191
+ for (const waiter of pendingTagWaiters.values()) {
192
+ for (const el of waiter.roots)
193
+ clearPendingRoot(el);
194
+ settleLateActivation();
195
+ }
196
+ pendingTagWaiters.clear();
197
+ pendingUndefinedRoots = 0;
198
+ }
199
+ function clearPendingRoot(el) {
200
+ if (hasPendingState(el))
201
+ takePendingState(el);
202
+ takePendingUpdates(el);
203
+ delete el[PENDING_ROOT_CONNECTED];
204
+ abandonDeferredDescendants(el);
205
+ abandonDeferredElement(el);
206
+ }
207
+ function stashPendingState(el, state, updates) {
208
+ const store = el;
209
+ store[PENDING_BOUNDARY_STATE] =
210
+ state === undefined ? NO_BOUNDARY_STATE : state;
211
+ if (updates) {
212
+ store[PENDING_BOUNDARY_UPDATES] = updates;
213
+ updates.pendingRoots++;
214
+ }
215
+ }
216
+ function hasPendingState(el) {
217
+ return Object.prototype.hasOwnProperty.call(el, PENDING_BOUNDARY_STATE);
218
+ }
219
+ function takePendingState(el) {
220
+ const store = el;
221
+ const stored = store[PENDING_BOUNDARY_STATE];
222
+ delete store[PENDING_BOUNDARY_STATE];
223
+ return stored === NO_BOUNDARY_STATE
224
+ ? undefined
225
+ : stored;
226
+ }
227
+ function takePendingUpdates(el) {
228
+ const store = el;
229
+ const updates = store[PENDING_BOUNDARY_UPDATES];
230
+ delete store[PENDING_BOUNDARY_UPDATES];
231
+ if (updates)
232
+ updates.pendingRoots--;
233
+ return updates;
234
+ }
235
+ function mergePendingUpdates(state, updates) {
236
+ if (!updates?.patch)
237
+ return state;
238
+ return state
239
+ ? { ...state, ...updates.patch }
240
+ : { ...updates.patch };
241
+ }
242
+ function invokeActivationHook(el, state) {
243
+ const hook = el[STREAMING_BOUNDARY_ACTIVATE];
244
+ if (typeof hook !== 'function')
245
+ return ACTIVATION_MISSING_TEMPLATE;
246
+ let outcome;
247
+ try {
248
+ outcome = hook.call(el, state);
249
+ }
250
+ catch (error) {
251
+ safeRemoveAttribute(el, STREAMED_HOST_ATTR);
252
+ throw error;
253
+ }
254
+ if (outcome !== ACTIVATION_ACTIVATED &&
255
+ outcome !== ACTIVATION_STATIC_HOST_OPT_OUT) {
256
+ return ACTIVATION_MISSING_TEMPLATE;
257
+ }
258
+ safeRemoveAttribute(el, STREAMED_HOST_ATTR);
259
+ return outcome;
260
+ }
261
+ export function resetDeferredActivationForTests() {
262
+ abandonPendingWaiters();
263
+ activationGeneration++;
264
+ }
265
+ export function pendingTagWaiterCountForTests() {
266
+ return pendingTagWaiters.size;
267
+ }
268
+ export function pendingUndefinedRootCountForTests() {
269
+ return pendingUndefinedRoots;
270
+ }
271
+ export function elementHasPendingStateForTests(el) {
272
+ return hasPendingState(el);
273
+ }
@@ -0,0 +1,32 @@
1
+ export declare const BOUNDARY_SCRIPT_ATTR = "data-webui-boundary";
2
+ export declare const MAX_ELEMENTS_PER_BOUNDARY = 10000;
3
+ export declare const MAX_MARKER_SCAN_NODES = 50000;
4
+ export declare const MAX_BOUNDARY_SCRIPT_SCAN = 8;
5
+ export declare function streamingErrorMessage(error: unknown): string;
6
+ export interface HydrationRange {
7
+ readonly start: Comment | null;
8
+ readonly end: Comment | null;
9
+ }
10
+ export type RangeResolution = {
11
+ readonly ok: true;
12
+ readonly range: HydrationRange;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly reason: string;
16
+ readonly truncated: false;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly reason: string;
20
+ readonly truncated: true;
21
+ readonly start: Comment | null;
22
+ };
23
+ export declare function resolveBoundaryRange(scriptEl: Element, sequence: number, terminal: boolean): RangeResolution;
24
+ export declare function findBoundaryScript(sentinel: Element): Element | null;
25
+ export declare function findEndMarkerByPrefix(scriptEl: Element): Comment | null;
26
+ export declare function findStartMarkerByPrefix(endMarker: Comment): Comment | null;
27
+ export declare function safeRemove(node: Node | null | undefined): void;
28
+ export declare function safeRemoveAttribute(el: Element, name: string): void;
29
+ export declare function removeBoundaryScaffolding(sentinel: Element, scriptEl: Element | null, startMarker: Comment | null, endMarker: Comment | null): void;
30
+ export declare function firstNodeWithin(root: Element): Node | null;
31
+ export declare function nextWithinRoot(node: Node, root: Node): Node | null;
32
+ export declare function nextAfterSubtreeWithin(node: Node, root: Node): Node | null;