@microsoft/webui-framework 0.0.18 → 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.
- package/README.md +130 -70
- package/dist/decorators.d.ts +1 -0
- package/dist/decorators.js +3 -0
- package/dist/element/diff.d.ts +3 -1
- package/dist/element/diff.js +197 -90
- package/dist/element/markers.js +13 -5
- package/dist/element/types.d.ts +18 -10
- package/dist/element/types.js +3 -0
- package/dist/element.d.ts +0 -4
- package/dist/element.js +11 -69
- package/dist/hydration-mismatch.js +3 -2
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +17 -0
- package/dist/lifecycle.js +66 -3
- package/dist/static-host.js +20 -10
- package/dist/streaming-activation.d.ts +2 -0
- package/dist/streaming-activation.js +13 -0
- package/dist/streaming-bootstrap.d.ts +2 -0
- package/dist/streaming-bootstrap.js +48 -0
- package/dist/streaming-cleanup.d.ts +4 -0
- package/dist/streaming-cleanup.js +66 -0
- package/dist/streaming-coordinator.d.ts +7 -0
- package/dist/streaming-coordinator.js +367 -0
- package/dist/streaming-deferred.d.ts +22 -0
- package/dist/streaming-deferred.js +273 -0
- package/dist/streaming-dom.d.ts +32 -0
- package/dist/streaming-dom.js +148 -0
- package/dist/streaming-entry.d.ts +1 -0
- package/dist/streaming-entry.js +2 -0
- package/dist/streaming-install.d.ts +2 -0
- package/dist/streaming-install.js +25 -0
- package/dist/streaming-mode.d.ts +5 -0
- package/dist/streaming-mode.js +14 -0
- package/dist/streaming-protocol.d.ts +32 -0
- package/dist/streaming-protocol.js +27 -0
- package/dist/streaming.d.ts +5 -0
- package/dist/streaming.js +9 -0
- package/dist/template-element.d.ts +28 -9
- package/dist/template-element.js +448 -238
- package/dist/template-events.js +1 -1
- package/dist/template-roots.d.ts +0 -1
- package/dist/template-roots.js +0 -11
- package/dist/template-types.d.ts +7 -1
- package/dist/template.d.ts +2 -0
- package/dist/template.js +46 -19
- package/package.json +9 -3
|
@@ -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;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const BOUNDARY_SCRIPT_ATTR = 'data-webui-boundary';
|
|
2
|
+
export const MAX_ELEMENTS_PER_BOUNDARY = 10_000;
|
|
3
|
+
export const MAX_MARKER_SCAN_NODES = 50_000;
|
|
4
|
+
export const MAX_BOUNDARY_SCRIPT_SCAN = 8;
|
|
5
|
+
export function streamingErrorMessage(error) {
|
|
6
|
+
return error instanceof Error ? error.message : String(error);
|
|
7
|
+
}
|
|
8
|
+
const BOUNDARY_START_PREFIX = 'wb:';
|
|
9
|
+
const BOUNDARY_END_PREFIX = '/wb:';
|
|
10
|
+
const MARKERLESS_RANGE = { start: null, end: null };
|
|
11
|
+
export function resolveBoundaryRange(scriptEl, sequence, terminal) {
|
|
12
|
+
const end = findEndMarker(scriptEl, sequence);
|
|
13
|
+
if (end) {
|
|
14
|
+
if (terminal) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
reason: `terminal boundary ${sequence} must be markerless`,
|
|
18
|
+
truncated: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const start = findStartMarkerBefore(end, sequence);
|
|
22
|
+
if (!start) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
reason: `missing start marker for boundary ${sequence}`,
|
|
26
|
+
truncated: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, range: { start, end } };
|
|
30
|
+
}
|
|
31
|
+
if (terminal)
|
|
32
|
+
return { ok: true, range: MARKERLESS_RANGE };
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: `missing end marker for boundary ${sequence}`,
|
|
36
|
+
truncated: true,
|
|
37
|
+
start: findStartMarkerBefore(scriptEl, sequence),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function findBoundaryScript(sentinel) {
|
|
41
|
+
let node = sentinel.previousElementSibling;
|
|
42
|
+
for (let hops = 0; node && hops < MAX_BOUNDARY_SCRIPT_SCAN; hops++) {
|
|
43
|
+
if (node.tagName === 'SCRIPT' &&
|
|
44
|
+
node.hasAttribute(BOUNDARY_SCRIPT_ATTR)) {
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
node = node.previousElementSibling;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function findEndMarker(scriptEl, sequence) {
|
|
52
|
+
const marker = previousComment(scriptEl);
|
|
53
|
+
return marker?.data === `${BOUNDARY_END_PREFIX}${sequence}`
|
|
54
|
+
? marker
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
function findStartMarkerBefore(nodeBefore, sequence) {
|
|
58
|
+
return findCommentBefore(nodeBefore, `${BOUNDARY_START_PREFIX}${sequence}`);
|
|
59
|
+
}
|
|
60
|
+
export function findEndMarkerByPrefix(scriptEl) {
|
|
61
|
+
const marker = previousComment(scriptEl);
|
|
62
|
+
return marker?.data.startsWith(BOUNDARY_END_PREFIX) ? marker : null;
|
|
63
|
+
}
|
|
64
|
+
export function findStartMarkerByPrefix(endMarker) {
|
|
65
|
+
return findCommentBefore(endMarker, `${BOUNDARY_START_PREFIX}${endMarker.data.slice(BOUNDARY_END_PREFIX.length)}`);
|
|
66
|
+
}
|
|
67
|
+
function previousComment(node) {
|
|
68
|
+
const previous = node.previousSibling;
|
|
69
|
+
return previous?.nodeType === 8
|
|
70
|
+
? previous
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
function findCommentBefore(nodeBefore, target) {
|
|
74
|
+
let node = nodeBefore.previousSibling;
|
|
75
|
+
for (let hops = 0; node && hops < MAX_MARKER_SCAN_NODES; hops++) {
|
|
76
|
+
if (node.nodeType === 8 &&
|
|
77
|
+
node.data === target) {
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
node = node.previousSibling;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
export function safeRemove(node) {
|
|
85
|
+
try {
|
|
86
|
+
node?.remove?.();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function safeRemoveAttribute(el, name) {
|
|
92
|
+
try {
|
|
93
|
+
el.removeAttribute?.(name);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function removeBoundaryScaffolding(sentinel, scriptEl, startMarker, endMarker) {
|
|
99
|
+
if (!scriptEl) {
|
|
100
|
+
safeRemove(sentinel);
|
|
101
|
+
safeRemove(endMarker);
|
|
102
|
+
safeRemove(startMarker);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let node = sentinel.previousElementSibling;
|
|
106
|
+
safeRemove(sentinel);
|
|
107
|
+
for (let hops = 0; node && hops <= MAX_BOUNDARY_SCRIPT_SCAN; hops++) {
|
|
108
|
+
const previous = node.previousElementSibling;
|
|
109
|
+
const wasBoundaryScript = node === scriptEl;
|
|
110
|
+
safeRemove(node);
|
|
111
|
+
if (wasBoundaryScript)
|
|
112
|
+
break;
|
|
113
|
+
node = previous;
|
|
114
|
+
}
|
|
115
|
+
safeRemove(endMarker);
|
|
116
|
+
safeRemove(startMarker);
|
|
117
|
+
}
|
|
118
|
+
export function firstNodeWithin(root) {
|
|
119
|
+
return root.shadowRoot?.firstChild ?? root.firstChild;
|
|
120
|
+
}
|
|
121
|
+
export function nextWithinRoot(node, root) {
|
|
122
|
+
if (node.nodeType === 1) {
|
|
123
|
+
const shadowRoot = node.shadowRoot;
|
|
124
|
+
if (shadowRoot?.firstChild)
|
|
125
|
+
return shadowRoot.firstChild;
|
|
126
|
+
}
|
|
127
|
+
if (node.firstChild)
|
|
128
|
+
return node.firstChild;
|
|
129
|
+
return nextAfterSubtreeWithin(node, root);
|
|
130
|
+
}
|
|
131
|
+
export function nextAfterSubtreeWithin(node, root) {
|
|
132
|
+
let current = node;
|
|
133
|
+
while (current && current !== root) {
|
|
134
|
+
if (current.nodeType === 11) {
|
|
135
|
+
const host = current.host ?? null;
|
|
136
|
+
if (host) {
|
|
137
|
+
if (host.firstChild)
|
|
138
|
+
return host.firstChild;
|
|
139
|
+
current = host;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (current.nextSibling)
|
|
144
|
+
return current.nextSibling;
|
|
145
|
+
current = current.parentNode;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { beginStreamingGate } from './lifecycle.js';
|
|
2
|
+
import { installTemplateElementRuntime } from './static-host.js';
|
|
3
|
+
import { enqueueStreamingSentinel, installStreamingTruncationGuard, } from './streaming-coordinator.js';
|
|
4
|
+
import { isStreamingHydrationMode } from './streaming-mode.js';
|
|
5
|
+
const SENTINEL_TAG = 'webui-hydrate';
|
|
6
|
+
let installed = false;
|
|
7
|
+
class WebUiHydrateSentinel extends HTMLElement {
|
|
8
|
+
connectedCallback() {
|
|
9
|
+
enqueueStreamingSentinel(this);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function installStreamingCoordinator() {
|
|
13
|
+
if (installed || !isStreamingHydrationMode())
|
|
14
|
+
return;
|
|
15
|
+
installed = true;
|
|
16
|
+
if (!customElements.get(SENTINEL_TAG)) {
|
|
17
|
+
customElements.define(SENTINEL_TAG, WebUiHydrateSentinel);
|
|
18
|
+
}
|
|
19
|
+
beginStreamingGate();
|
|
20
|
+
installStreamingTruncationGuard();
|
|
21
|
+
installTemplateElementRuntime();
|
|
22
|
+
}
|
|
23
|
+
export function resetStreamingInstallForTests() {
|
|
24
|
+
installed = false;
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const STREAMING_BOUNDARY_ACTIVATE: unique symbol;
|
|
2
|
+
export declare const PENDING_ROOT_CONNECTED: unique symbol;
|
|
3
|
+
export declare const STREAMED_HOST_ATTR = "data-ws";
|
|
4
|
+
export declare function isStreamingHydrationMode(): boolean;
|
|
5
|
+
export declare function resetStreamingModeForTests(): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
let cached;
|
|
2
|
+
export const STREAMING_BOUNDARY_ACTIVATE = Symbol.for('microsoft.webui.boundaryActivate');
|
|
3
|
+
export const PENDING_ROOT_CONNECTED = Symbol.for('microsoft.webui.pendingRootConnected');
|
|
4
|
+
export const STREAMED_HOST_ATTR = 'data-ws';
|
|
5
|
+
export function isStreamingHydrationMode() {
|
|
6
|
+
if (cached !== undefined)
|
|
7
|
+
return cached;
|
|
8
|
+
cached = typeof document !== 'undefined' &&
|
|
9
|
+
!!document.querySelector('meta[name="webui-streaming"][content="1"]');
|
|
10
|
+
return cached;
|
|
11
|
+
}
|
|
12
|
+
export function resetStreamingModeForTests() {
|
|
13
|
+
cached = undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TemplateMeta } from './template.js';
|
|
2
|
+
export interface BoundaryBootstrap {
|
|
3
|
+
state?: Record<string, unknown>;
|
|
4
|
+
templates?: Record<string, TemplateMeta>;
|
|
5
|
+
inventory?: string;
|
|
6
|
+
nonce?: string;
|
|
7
|
+
chain?: unknown[];
|
|
8
|
+
css?: string[];
|
|
9
|
+
styles?: string[];
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export declare const RECORD_KIND_FINAL_CHECKPOINT = 0;
|
|
13
|
+
export declare const RECORD_KIND_UPDATABLE_CHECKPOINT = 1;
|
|
14
|
+
export declare const RECORD_KIND_STATE_UPDATE = 2;
|
|
15
|
+
export declare const RECORD_KIND_TERMINAL = 3;
|
|
16
|
+
export type BoundaryRecordKind = typeof RECORD_KIND_FINAL_CHECKPOINT | typeof RECORD_KIND_UPDATABLE_CHECKPOINT | typeof RECORD_KIND_STATE_UPDATE | typeof RECORD_KIND_TERMINAL;
|
|
17
|
+
export type BoundaryRecordPayload = BoundaryBootstrap | Record<string, unknown>;
|
|
18
|
+
export type BoundaryEnvelope = readonly [
|
|
19
|
+
version: number,
|
|
20
|
+
recordSequence: number,
|
|
21
|
+
kind: BoundaryRecordKind,
|
|
22
|
+
target: number,
|
|
23
|
+
payload: BoundaryRecordPayload
|
|
24
|
+
];
|
|
25
|
+
export type ParseBoundaryEnvelopeResult = {
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
readonly envelope: BoundaryEnvelope;
|
|
28
|
+
} | {
|
|
29
|
+
readonly ok: false;
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function parseBoundaryEnvelope(text: string): ParseBoundaryEnvelopeResult;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const SUPPORTED_VERSION = 1;
|
|
2
|
+
export const RECORD_KIND_FINAL_CHECKPOINT = 0;
|
|
3
|
+
export const RECORD_KIND_UPDATABLE_CHECKPOINT = 1;
|
|
4
|
+
export const RECORD_KIND_STATE_UPDATE = 2;
|
|
5
|
+
export const RECORD_KIND_TERMINAL = 3;
|
|
6
|
+
function invalid(reason) {
|
|
7
|
+
return { ok: false, reason };
|
|
8
|
+
}
|
|
9
|
+
export function parseBoundaryEnvelope(text) {
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = JSON.parse(text);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return invalid('boundary payload is not valid JSON');
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(parsed) || parsed.length !== 5) {
|
|
18
|
+
return invalid('boundary envelope must be a 5-element [version, recordSequence, kind, target, payload] array');
|
|
19
|
+
}
|
|
20
|
+
if (parsed[0] !== SUPPORTED_VERSION) {
|
|
21
|
+
return invalid(`unsupported boundary envelope version ${JSON.stringify(parsed[0])}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
envelope: parsed,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { enqueueStreamingSentinel as __enqueueSentinelForTests, elementHasPendingStateForTests as __elementHasPendingStateForTests, installStreamingTruncationGuard as __installTruncationGuardForTests, isStreamingHaltedForTests as __isHaltedForTests, pendingTagWaiterCountForTests as __pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests as __pendingUndefinedRootCountForTests, streamingRetentionStateForTests as __streamingRetentionStateForTests, } from './streaming-coordinator.js';
|
|
2
|
+
export { installStreamingCoordinator } from './streaming-install.js';
|
|
3
|
+
export { parseBoundaryEnvelope } from './streaming-protocol.js';
|
|
4
|
+
export type { BoundaryBootstrap, BoundaryEnvelope, ParseBoundaryEnvelopeResult, } from './streaming-protocol.js';
|
|
5
|
+
export declare function __resetStreamingCoordinatorForTests(): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resetStreamingCoordinatorStateForTests, } from './streaming-coordinator.js';
|
|
2
|
+
import { resetStreamingInstallForTests } from './streaming-install.js';
|
|
3
|
+
export { enqueueStreamingSentinel as __enqueueSentinelForTests, elementHasPendingStateForTests as __elementHasPendingStateForTests, installStreamingTruncationGuard as __installTruncationGuardForTests, isStreamingHaltedForTests as __isHaltedForTests, pendingTagWaiterCountForTests as __pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests as __pendingUndefinedRootCountForTests, streamingRetentionStateForTests as __streamingRetentionStateForTests, } from './streaming-coordinator.js';
|
|
4
|
+
export { installStreamingCoordinator } from './streaming-install.js';
|
|
5
|
+
export { parseBoundaryEnvelope } from './streaming-protocol.js';
|
|
6
|
+
export function __resetStreamingCoordinatorForTests() {
|
|
7
|
+
resetStreamingCoordinatorStateForTests();
|
|
8
|
+
resetStreamingInstallForTests();
|
|
9
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { TemplateBlockMeta, TemplateNodePath } from './template.js';
|
|
2
|
+
import { STREAMING_BOUNDARY_ACTIVATE } from './streaming-mode.js';
|
|
2
3
|
import type { ScopeFrame, TemplateInstance } from './element/types.js';
|
|
3
4
|
declare const WEBUI_SET_STATE_KEY: unique symbol;
|
|
5
|
+
declare const STREAMING_BOUNDARY_ABANDON: unique symbol;
|
|
4
6
|
export declare class TemplateElement extends HTMLElement {
|
|
5
7
|
private $root;
|
|
6
8
|
private $meta?;
|
|
7
9
|
private $ready;
|
|
8
10
|
private $hydrated;
|
|
11
|
+
private $hydratedCallbackCalled;
|
|
12
|
+
private $deferredSSR;
|
|
13
|
+
private $activatingDeferredSSR;
|
|
14
|
+
private $hasUnknownScopes;
|
|
9
15
|
private $templateState;
|
|
10
16
|
private $dirtyPaths;
|
|
11
17
|
private $pendingFlush;
|
|
@@ -14,20 +20,30 @@ export declare class TemplateElement extends HTMLElement {
|
|
|
14
20
|
private $pathIndex?;
|
|
15
21
|
private $wildcardBindings?;
|
|
16
22
|
[WEBUI_SET_STATE_KEY](key: string, value: unknown): boolean;
|
|
23
|
+
[STREAMING_BOUNDARY_ACTIVATE](state?: Record<string, unknown>): number;
|
|
24
|
+
[STREAMING_BOUNDARY_ABANDON](): void;
|
|
17
25
|
static define(tagName: string): void;
|
|
18
26
|
connectedCallback(): void;
|
|
19
27
|
private $mount;
|
|
20
28
|
disconnectedCallback(): void;
|
|
21
29
|
$destroy(): void;
|
|
22
|
-
private $
|
|
23
|
-
private $
|
|
30
|
+
private $clearRepeatBinding;
|
|
31
|
+
private $clearInstance;
|
|
24
32
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
25
33
|
setState(state: Record<string, unknown>): void;
|
|
26
34
|
protected $observableNames(): Set<string>;
|
|
35
|
+
protected $beforeExternalStateWrite(): void;
|
|
36
|
+
protected $afterExternalStateWrite(_applied: boolean): void;
|
|
37
|
+
protected $shouldDeferSSRHydration(): boolean;
|
|
38
|
+
protected $shouldActivateOnBoundaryCommit(): boolean;
|
|
39
|
+
protected $activateDeferredSSR(state?: Record<string, unknown>): void;
|
|
40
|
+
protected $shouldApplySSRBootstrapState(): boolean;
|
|
27
41
|
protected $shouldApplySSRState(_key: string): boolean;
|
|
28
42
|
protected $syncAuthoredAttributes(): void;
|
|
43
|
+
protected hydratedCallback(): void;
|
|
44
|
+
private $notifyHydrated;
|
|
29
45
|
protected $shouldApplyTemplateStateFromSSR(_key: string): boolean;
|
|
30
|
-
protected $setTemplateState(key: string, value: unknown):
|
|
46
|
+
protected $setTemplateState(key: string, value: unknown): boolean;
|
|
31
47
|
private $writeTemplateState;
|
|
32
48
|
private $templateMeta;
|
|
33
49
|
private $templateRootForAttribute;
|
|
@@ -44,33 +60,36 @@ export declare class TemplateElement extends HTMLElement {
|
|
|
44
60
|
private $parseTemplate;
|
|
45
61
|
private $createStagingRoot;
|
|
46
62
|
private $appendStagedChildren;
|
|
47
|
-
private $
|
|
63
|
+
private $replaceInstanceContainer;
|
|
48
64
|
private $wire;
|
|
49
65
|
private $hydrate;
|
|
50
|
-
private $
|
|
66
|
+
private $collectConditionalRange;
|
|
67
|
+
private $hasRootStructuralSlot;
|
|
51
68
|
private $hydrateCondContent;
|
|
52
69
|
private $findMarker;
|
|
53
|
-
private $hasContentAfterMarker;
|
|
54
70
|
private $findSSRText;
|
|
55
71
|
private $findSSRSlotRef;
|
|
56
72
|
private $rootTag;
|
|
57
73
|
private $wireAttrs;
|
|
58
74
|
protected $finalize(_instance: TemplateInstance, _root: Node, _meta: TemplateBlockMeta, _resolver: (root: Node, path: TemplateNodePath) => Node | null, _scope?: ScopeFrame): void;
|
|
59
75
|
private $makeAttr;
|
|
60
|
-
private $repeatMaps;
|
|
61
76
|
private $buildPathIndex;
|
|
62
77
|
private $updateBindings;
|
|
78
|
+
private $scopeIsKnown;
|
|
63
79
|
$updateInstance(instance: TemplateInstance): void;
|
|
64
80
|
private $patchText;
|
|
65
81
|
private $patchAttr;
|
|
66
82
|
private $toggleCond;
|
|
67
83
|
$resolveValue(path: string, scope?: ScopeFrame): unknown;
|
|
84
|
+
$hasStateRoot(path: string, scope?: ScopeFrame): boolean;
|
|
68
85
|
private $resolveComponentRoot;
|
|
69
86
|
private $resolveParts;
|
|
70
87
|
$block(blockIndex: number): TemplateBlockMeta | undefined;
|
|
71
|
-
$createBlockInstance(blockIndex: number, scope?: ScopeFrame): TemplateInstance | null;
|
|
88
|
+
$createBlockInstance(blockIndex: number, scope?: ScopeFrame, parent?: TemplateInstance, container?: ParentNode & Node): TemplateInstance | null;
|
|
72
89
|
$removeInstance(instance: TemplateInstance): void;
|
|
90
|
+
private $disposeInstance;
|
|
91
|
+
private $compactNodeArray;
|
|
92
|
+
$changeStructure(removedFrom?: TemplateInstance): void;
|
|
73
93
|
$insertInstanceAfter(cursor: Node | null, container: ParentNode & Node, instance: TemplateInstance): Node | null;
|
|
74
|
-
private $singleDynamic;
|
|
75
94
|
}
|
|
76
95
|
export {};
|