@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.
- package/README.md +61 -16
- package/dist/element.d.ts +0 -4
- package/dist/element.js +8 -67
- package/dist/lifecycle.d.ts +17 -0
- package/dist/lifecycle.js +66 -3
- package/dist/static-host.js +4 -2
- 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 +9 -1
- package/dist/template-element.js +112 -42
- package/dist/template.d.ts +1 -0
- package/dist/template.js +46 -19
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -126,6 +126,36 @@ The plugin alone preserves full server state. To emit exact `@observable` and
|
|
|
126
126
|
`--projection-manifest`. The manifest tooling is build-only; this runtime
|
|
127
127
|
package does not depend on esbuild or TypeScript.
|
|
128
128
|
|
|
129
|
+
### Progressive streaming hydration
|
|
130
|
+
|
|
131
|
+
Streaming applications opt into a separate side-effect entry:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import '@microsoft/webui-framework/streaming.js';
|
|
135
|
+
import './counter-card.js';
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Import it before component registration modules and load the application entry
|
|
139
|
+
early with `<script type="module" async>` in `<head>`. The server must render
|
|
140
|
+
authored `<boundary>` directives through
|
|
141
|
+
`WebUIHandler::render_streaming`. The default
|
|
142
|
+
`@microsoft/webui-framework` entry has no dependency on the coordinator, so
|
|
143
|
+
normal applications pay no streaming bundle or initialization cost.
|
|
144
|
+
|
|
145
|
+
Each committed boundary receives its own ephemeral state object directly during
|
|
146
|
+
activation. The coordinator does not publish that state to
|
|
147
|
+
`window.__webui.state`, and it removes generated checkpoint scaffolding after
|
|
148
|
+
commit. Every commit also emits a `performance.mark()` — `webui:boundary:<id>`,
|
|
149
|
+
`webui:boundary:<id>:update`, or `webui:streaming:terminal` — which needs no
|
|
150
|
+
flag and no listener, so tooling that loads after hydration can still read it.
|
|
151
|
+
Set `window.__WEBUI_STREAMING_DEBUG__ = true` only when tooling needs the live
|
|
152
|
+
`webui:boundary-hydrated` event as well.
|
|
153
|
+
|
|
154
|
+
Set `window.__WEBUI_STREAMING_SLICE_MS__` to a positive millisecond budget to
|
|
155
|
+
make the coordinator yield between boundaries instead of draining its queue in
|
|
156
|
+
one pass. That is for pages where an intermediary coalesces the response into a
|
|
157
|
+
single chunk; it costs total hydration time, so leave it unset otherwise.
|
|
158
|
+
|
|
129
159
|
### Property binding lifecycle
|
|
130
160
|
|
|
131
161
|
Property bindings use the `:` prefix to pass values directly to child DOM properties:
|
|
@@ -145,18 +175,26 @@ re-render it. An `@observable` written before hydration finishes — in a field
|
|
|
145
175
|
initializer, the `constructor`, or before `super.connectedCallback()` — cannot
|
|
146
176
|
update that DOM, so the write is dropped and the runtime logs a
|
|
147
177
|
`[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
|
|
148
|
-
the SSR state, or assign them
|
|
178
|
+
the SSR state, or assign them from `hydratedCallback()`. The warning is
|
|
149
179
|
development-only and is dead-code-eliminated from production bundles via the
|
|
150
180
|
`__WEBUI_DEV__` compile-time flag (on by default; `webui-press build` sets it to
|
|
151
181
|
`false`). See the
|
|
152
182
|
[Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
|
|
153
183
|
|
|
154
|
-
`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
184
|
+
Override the protected `hydratedCallback()` hook for work that requires the
|
|
185
|
+
component's bindings, events, and `w-ref` references to be ready. It runs
|
|
186
|
+
synchronously exactly once after the first successful ordinary SSR hydration,
|
|
187
|
+
client-created mount, deferred streamed activation, or dormant static-host wake.
|
|
188
|
+
Its once-latch is set before author code runs, so a thrown callback is not
|
|
189
|
+
retried on reconnect.
|
|
190
|
+
|
|
191
|
+
`connectedCallback()` remains a native per-connection lifecycle. On ordinary
|
|
192
|
+
SSR and client-created mounts, `super.connectedCallback()` hydrates
|
|
193
|
+
synchronously, but a streamed `data-ws` root returns while still deferred and
|
|
194
|
+
hydrates only when its boundary commits. Therefore `connectedCallback()` cannot
|
|
195
|
+
be used as a universal post-hydration signal. Descendants must not structurally
|
|
196
|
+
mutate a containing component's SSR subtree before it hydrates, because
|
|
197
|
+
hydration relies on stable compiled paths.
|
|
160
198
|
|
|
161
199
|
### DOM strategy (`--dom`)
|
|
162
200
|
|
|
@@ -186,6 +224,7 @@ Base class for framework components.
|
|
|
186
224
|
| Member | Purpose |
|
|
187
225
|
|--------|---------|
|
|
188
226
|
| `static define(tagName)` | Register the class as a custom element |
|
|
227
|
+
| `protected hydratedCallback()` | Run once after the first successful hydration or client mount |
|
|
189
228
|
| `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
|
|
190
229
|
| `$update()` | Force a reactive update (normally called automatically) |
|
|
191
230
|
| `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
|
|
@@ -360,9 +399,9 @@ resource-constrained devices.
|
|
|
360
399
|
|
|
361
400
|
5. **Single-pass hydration via path mapping.**
|
|
362
401
|
SSR DOM is matched to compiled template bindings through
|
|
363
|
-
template-parallel traversal (`$resolveSSR`).
|
|
364
|
-
data attributes
|
|
365
|
-
touches each DOM node exactly once.
|
|
402
|
+
template-parallel traversal (`$resolveSSR`). Ordinary buffered hydration
|
|
403
|
+
needs no marker comments or data attributes for binding resolution. The
|
|
404
|
+
hydration walk touches each DOM node exactly once.
|
|
366
405
|
|
|
367
406
|
6. **Keep the framework out of the GC's way.**
|
|
368
407
|
Fewer JS objects = fewer GC pauses. Binding arrays are pre-built at
|
|
@@ -424,7 +463,10 @@ Angular all require a JavaScript runtime on the server. This framework's SSR
|
|
|
424
463
|
is driven by data (template metadata + state values), not code. Any language
|
|
425
464
|
that can read the compiled metadata and produce HTML can serve as the SSR
|
|
426
465
|
backend. No comment markers or data attributes are needed — the runtime
|
|
427
|
-
resolves SSR
|
|
466
|
+
resolves ordinary buffered SSR nodes via template-parallel path traversal.
|
|
467
|
+
Progressive streaming uses temporary checkpoint scaffolding only to delay
|
|
468
|
+
activation until a complete region arrives; it removes that scaffolding after
|
|
469
|
+
commit.
|
|
428
470
|
|
|
429
471
|
### Build → Serve → Hydrate → Update
|
|
430
472
|
|
|
@@ -490,8 +532,8 @@ When the server renders a component, it emits HTML content (as a declarative
|
|
|
490
532
|
shadow root or as light DOM children) along with an inert `#webui-data`
|
|
491
533
|
JSON payload. The browser parses this DOM before any JavaScript runs.
|
|
492
534
|
When the component's JS loads and `connectedCallback` fires, the framework
|
|
493
|
-
uses compiled template paths to resolve SSR DOM nodes without
|
|
494
|
-
|
|
535
|
+
uses compiled template paths to resolve ordinary buffered SSR DOM nodes without
|
|
536
|
+
binding markers:
|
|
495
537
|
|
|
496
538
|
```mermaid
|
|
497
539
|
sequenceDiagram
|
|
@@ -511,6 +553,7 @@ sequenceDiagram
|
|
|
511
553
|
FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
|
|
512
554
|
FW->>FW: $wireEvents() + $wireRefs()
|
|
513
555
|
FW->>FW: $buildPathIndex(), $ready = true
|
|
556
|
+
FW->>CE: hydratedCallback() (once)
|
|
514
557
|
Note over FW: DOM is already correct from SSR.<br/>No $update() call needed.
|
|
515
558
|
```
|
|
516
559
|
|
|
@@ -535,6 +578,7 @@ sequenceDiagram
|
|
|
535
578
|
FW->>FW: $wireEvents() + $wireRefs()
|
|
536
579
|
FW->>FW: $buildPathIndex(), $ready = true
|
|
537
580
|
FW->>FW: $update() — flush initial property values
|
|
581
|
+
FW->>CE: hydratedCallback() (once)
|
|
538
582
|
```
|
|
539
583
|
|
|
540
584
|
---
|
|
@@ -734,10 +778,11 @@ stylesheet specifier for a component.
|
|
|
734
778
|
|
|
735
779
|
## Path-Based Binding Resolution
|
|
736
780
|
|
|
737
|
-
Unlike frameworks that use comment markers or data attributes to locate
|
|
738
|
-
dynamic
|
|
781
|
+
Unlike frameworks that use comment markers or data attributes to locate each
|
|
782
|
+
dynamic binding, this framework uses **compiled template paths** — arrays of
|
|
739
783
|
child-node indices that describe exactly where each binding lives in the DOM
|
|
740
|
-
tree.
|
|
784
|
+
tree. Progressive streaming's temporary boundary markers locate complete
|
|
785
|
+
activation regions, not individual bindings.
|
|
741
786
|
|
|
742
787
|
### Client-created resolution (`$resolve`)
|
|
743
788
|
|
package/dist/element.d.ts
CHANGED
|
@@ -8,14 +8,10 @@ export declare class WebUIElement extends TemplateElement {
|
|
|
8
8
|
$emit(name: string, detail?: unknown): boolean;
|
|
9
9
|
protected $finalize(instance: TemplateInstance, root: Node, meta: TemplateBlockMeta, resolver: (root: Node, path: TemplateNodePath) => Node | null, scope?: ScopeFrame): void;
|
|
10
10
|
private $wireEvents;
|
|
11
|
-
private $resolveDelegatedEvents;
|
|
12
11
|
private $wireRoot;
|
|
13
|
-
private $addDelegatedEvent;
|
|
14
|
-
private $dispatchDelegatedEvent;
|
|
15
12
|
private $addEvent;
|
|
16
13
|
private $addCleanup;
|
|
17
14
|
private $callEventHandler;
|
|
18
|
-
private $callEventHandlerWithCurrentTarget;
|
|
19
15
|
private $resolveEventArgs;
|
|
20
16
|
private $resolveEventArg;
|
|
21
17
|
private $wireRefs;
|
package/dist/element.js
CHANGED
|
@@ -29,32 +29,16 @@ export class WebUIElement extends TemplateElement {
|
|
|
29
29
|
const groups = meta.eg;
|
|
30
30
|
if (!groups)
|
|
31
31
|
return;
|
|
32
|
-
const delegateTarget = this.shadowRoot ?? this;
|
|
33
32
|
for (let i = 0; i < groups.length; i++) {
|
|
34
33
|
const [eventName, bindings] = groups[i];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const [handlerName, args, target, usesEvent] = bindings[i];
|
|
43
|
-
const el = resolver(root, target);
|
|
44
|
-
if (!el || el.nodeType !== 1)
|
|
45
|
-
continue;
|
|
46
|
-
const method = this[handlerName];
|
|
47
|
-
if (typeof method !== 'function')
|
|
48
|
-
continue;
|
|
49
|
-
entries.push({
|
|
50
|
-
target: el,
|
|
51
|
-
method: method,
|
|
52
|
-
args,
|
|
53
|
-
usesEvent: usesEvent === 1,
|
|
54
|
-
scope,
|
|
55
|
-
});
|
|
34
|
+
for (let j = 0; j < bindings.length; j++) {
|
|
35
|
+
const [handlerName, args, target] = bindings[j];
|
|
36
|
+
const el = resolver(root, target);
|
|
37
|
+
if (el && el.nodeType === 1) {
|
|
38
|
+
this.$addEvent(instance, el, eventName, handlerName, args, scope);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
56
41
|
}
|
|
57
|
-
return entries;
|
|
58
42
|
}
|
|
59
43
|
$wireRoot(instance, re) {
|
|
60
44
|
const target = this.shadowRoot ?? this;
|
|
@@ -62,27 +46,6 @@ export class WebUIElement extends TemplateElement {
|
|
|
62
46
|
this.$addEvent(instance, target, re[i][0], re[i][1], re[i][2], undefined);
|
|
63
47
|
}
|
|
64
48
|
}
|
|
65
|
-
$addDelegatedEvent(instance, target, eventName, entries) {
|
|
66
|
-
if (entries.length === 0)
|
|
67
|
-
return;
|
|
68
|
-
const listener = (event) => {
|
|
69
|
-
this.$dispatchDelegatedEvent(entries, event);
|
|
70
|
-
};
|
|
71
|
-
target.addEventListener(eventName, listener);
|
|
72
|
-
this.$addCleanup(instance, () => target.removeEventListener(eventName, listener));
|
|
73
|
-
}
|
|
74
|
-
$dispatchDelegatedEvent(entries, event) {
|
|
75
|
-
let current = event.target;
|
|
76
|
-
while (current) {
|
|
77
|
-
for (let i = 0; i < entries.length; i++) {
|
|
78
|
-
const entry = entries[i];
|
|
79
|
-
if (entry.target === current) {
|
|
80
|
-
this.$callEventHandler(entry.method, entry.args, event, entry.scope, entry.target, entry.usesEvent);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
current = current.parentNode;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
49
|
$addEvent(instance, target, eventName, handlerName, args, scope) {
|
|
87
50
|
const method = this[handlerName];
|
|
88
51
|
if (typeof method !== 'function')
|
|
@@ -94,11 +57,7 @@ export class WebUIElement extends TemplateElement {
|
|
|
94
57
|
$addCleanup(instance, cleanup) {
|
|
95
58
|
(instance.cleanups ??= []).push(cleanup);
|
|
96
59
|
}
|
|
97
|
-
$callEventHandler(method, args, event, scope
|
|
98
|
-
if (currentTarget && usesEvent) {
|
|
99
|
-
this.$callEventHandlerWithCurrentTarget(method, args, event, scope, currentTarget);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
60
|
+
$callEventHandler(method, args, event, scope) {
|
|
102
61
|
switch (args.length) {
|
|
103
62
|
case 0:
|
|
104
63
|
method.call(this);
|
|
@@ -119,24 +78,6 @@ export class WebUIElement extends TemplateElement {
|
|
|
119
78
|
method.apply(this, this.$resolveEventArgs(args, event, scope));
|
|
120
79
|
}
|
|
121
80
|
}
|
|
122
|
-
$callEventHandlerWithCurrentTarget(method, args, event, scope, currentTarget) {
|
|
123
|
-
const descriptor = Object.getOwnPropertyDescriptor(event, 'currentTarget');
|
|
124
|
-
Object.defineProperty(event, 'currentTarget', {
|
|
125
|
-
configurable: true,
|
|
126
|
-
value: currentTarget,
|
|
127
|
-
});
|
|
128
|
-
try {
|
|
129
|
-
this.$callEventHandler(method, args, event, scope);
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
if (descriptor) {
|
|
133
|
-
Object.defineProperty(event, 'currentTarget', descriptor);
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
delete event.currentTarget;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
81
|
$resolveEventArgs(args, event, scope) {
|
|
141
82
|
const resolved = [];
|
|
142
83
|
for (let i = 0; i < args.length; i++) {
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
export declare function hydrationStart(): void;
|
|
2
2
|
export declare function hydrationEnd(): void;
|
|
3
|
+
export declare function beginStreamingGate(): void;
|
|
4
|
+
export declare function abortStreamingGate(): void;
|
|
5
|
+
export declare function markBoundaryPending(): void;
|
|
6
|
+
export declare function markBoundaryCommitted(terminal: boolean): void;
|
|
7
|
+
export declare function markLateActivationPending(): void;
|
|
8
|
+
export declare function settleLateActivation(): void;
|
|
9
|
+
export declare function __getLifecycleStateForTests(): {
|
|
10
|
+
pendingCount: number;
|
|
11
|
+
started: boolean;
|
|
12
|
+
completed: boolean;
|
|
13
|
+
streamingGateActive: boolean;
|
|
14
|
+
streamingGateAborted: boolean;
|
|
15
|
+
terminalReached: boolean;
|
|
16
|
+
pendingBoundaries: number;
|
|
17
|
+
pendingLateActivations: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function __resetLifecycleForTests(): void;
|
package/dist/lifecycle.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
let pendingCount = 0;
|
|
2
2
|
let started = false;
|
|
3
3
|
let completed = false;
|
|
4
|
+
let streamingGateActive = false;
|
|
5
|
+
let terminalReached = false;
|
|
6
|
+
let streamingGateAborted = false;
|
|
7
|
+
let pendingBoundaries = 0;
|
|
8
|
+
let pendingLateActivations = 0;
|
|
4
9
|
export function hydrationStart() {
|
|
5
10
|
if (!started) {
|
|
6
11
|
performance.mark('webui:hydrate:total:start');
|
|
@@ -9,11 +14,69 @@ export function hydrationStart() {
|
|
|
9
14
|
pendingCount++;
|
|
10
15
|
}
|
|
11
16
|
export function hydrationEnd() {
|
|
17
|
+
if (pendingCount === 0)
|
|
18
|
+
return;
|
|
12
19
|
pendingCount--;
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
tryComplete();
|
|
21
|
+
}
|
|
22
|
+
export function beginStreamingGate() {
|
|
23
|
+
streamingGateActive = true;
|
|
24
|
+
}
|
|
25
|
+
export function abortStreamingGate() {
|
|
26
|
+
streamingGateAborted = true;
|
|
27
|
+
}
|
|
28
|
+
export function markBoundaryPending() {
|
|
29
|
+
pendingBoundaries++;
|
|
30
|
+
}
|
|
31
|
+
export function markBoundaryCommitted(terminal) {
|
|
32
|
+
if (pendingBoundaries === 0)
|
|
33
|
+
return;
|
|
34
|
+
pendingBoundaries--;
|
|
35
|
+
if (terminal)
|
|
36
|
+
terminalReached = true;
|
|
37
|
+
tryComplete();
|
|
38
|
+
}
|
|
39
|
+
export function markLateActivationPending() {
|
|
40
|
+
pendingLateActivations++;
|
|
41
|
+
}
|
|
42
|
+
export function settleLateActivation() {
|
|
43
|
+
if (pendingLateActivations === 0)
|
|
44
|
+
return;
|
|
45
|
+
pendingLateActivations--;
|
|
46
|
+
tryComplete();
|
|
47
|
+
}
|
|
48
|
+
function tryComplete() {
|
|
49
|
+
if (completed || streamingGateAborted || pendingCount !== 0)
|
|
50
|
+
return;
|
|
51
|
+
if (streamingGateActive &&
|
|
52
|
+
(!terminalReached || pendingBoundaries !== 0 || pendingLateActivations !== 0))
|
|
53
|
+
return;
|
|
54
|
+
completed = true;
|
|
55
|
+
if (started) {
|
|
15
56
|
performance.mark('webui:hydrate:total:end');
|
|
16
57
|
performance.measure('webui:hydrate:total', 'webui:hydrate:total:start', 'webui:hydrate:total:end');
|
|
17
|
-
window.dispatchEvent(new Event('webui:hydration-complete'));
|
|
18
58
|
}
|
|
59
|
+
window.dispatchEvent(new Event('webui:hydration-complete'));
|
|
60
|
+
}
|
|
61
|
+
export function __getLifecycleStateForTests() {
|
|
62
|
+
return {
|
|
63
|
+
pendingCount,
|
|
64
|
+
started,
|
|
65
|
+
completed,
|
|
66
|
+
streamingGateActive,
|
|
67
|
+
streamingGateAborted,
|
|
68
|
+
terminalReached,
|
|
69
|
+
pendingBoundaries,
|
|
70
|
+
pendingLateActivations,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function __resetLifecycleForTests() {
|
|
74
|
+
pendingCount = 0;
|
|
75
|
+
started = false;
|
|
76
|
+
completed = false;
|
|
77
|
+
streamingGateActive = false;
|
|
78
|
+
streamingGateAborted = false;
|
|
79
|
+
terminalReached = false;
|
|
80
|
+
pendingBoundaries = 0;
|
|
81
|
+
pendingLateActivations = 0;
|
|
19
82
|
}
|
package/dist/static-host.js
CHANGED
|
@@ -22,6 +22,9 @@ function defineTemplateHost(tag, meta) {
|
|
|
22
22
|
$shouldApplySSRBootstrapState() {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
|
+
$shouldActivateOnBoundaryCommit() {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
25
28
|
}
|
|
26
29
|
StaticTemplateHost.define(tag);
|
|
27
30
|
}
|
|
@@ -58,9 +61,8 @@ export function installTemplateElementRuntime() {
|
|
|
58
61
|
return;
|
|
59
62
|
defineTemplateHosts(detail.templates);
|
|
60
63
|
});
|
|
64
|
+
defineTemplateHosts();
|
|
61
65
|
if (document.readyState === 'loading') {
|
|
62
66
|
document.addEventListener('DOMContentLoaded', () => defineTemplateHosts(), { once: true });
|
|
63
|
-
return;
|
|
64
67
|
}
|
|
65
|
-
defineTemplateHosts();
|
|
66
68
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { abandonDeferredRange, } from './streaming-cleanup.js';
|
|
2
|
+
import { activateDeferredTree, } from './streaming-deferred.js';
|
|
3
|
+
export function activateRootsBetween(startMarker, endMarker, state, updates) {
|
|
4
|
+
const root = startMarker.parentNode;
|
|
5
|
+
if (!root) {
|
|
6
|
+
throw new Error('boundary start marker was detached before activation');
|
|
7
|
+
}
|
|
8
|
+
const failure = activateDeferredTree(startMarker.nextSibling, root, endMarker, state, updates ? { updates, retainedRoots: updates.roots } : undefined);
|
|
9
|
+
if (!failure)
|
|
10
|
+
return;
|
|
11
|
+
abandonDeferredRange(startMarker, endMarker);
|
|
12
|
+
throw new Error(failure);
|
|
13
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { registerTemplateData } from './template.js';
|
|
2
|
+
export function applyBoundaryBootstrap(bootstrap) {
|
|
3
|
+
if (bootstrap.templates)
|
|
4
|
+
registerTemplateData(bootstrap.templates);
|
|
5
|
+
const w = window;
|
|
6
|
+
if (!w.__webui)
|
|
7
|
+
w.__webui = {};
|
|
8
|
+
const keys = Object.keys(bootstrap);
|
|
9
|
+
for (let i = 0; i < keys.length; i++) {
|
|
10
|
+
const key = keys[i];
|
|
11
|
+
if (key === 'templates' || key === 'state')
|
|
12
|
+
continue;
|
|
13
|
+
if (key === 'inventory') {
|
|
14
|
+
w.__webui.inventory = mergeInventory(w.__webui.inventory, bootstrap.inventory);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (key === 'css' || key === 'styles') {
|
|
18
|
+
appendUniqueStrings(w.__webui, key, bootstrap[key]);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
w.__webui[key] = bootstrap[key];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function mergeInventory(existing, delta) {
|
|
25
|
+
const current = typeof existing === 'string' ? existing : '';
|
|
26
|
+
const next = delta ?? '';
|
|
27
|
+
const length = Math.max(current.length, next.length);
|
|
28
|
+
let merged = '';
|
|
29
|
+
for (let i = 0; i < length; i++) {
|
|
30
|
+
const a = i < current.length ? parseInt(current[i], 16) : 0;
|
|
31
|
+
const b = i < next.length ? parseInt(next[i], 16) : 0;
|
|
32
|
+
merged += (a | b).toString(16);
|
|
33
|
+
}
|
|
34
|
+
return merged;
|
|
35
|
+
}
|
|
36
|
+
function appendUniqueStrings(target, key, delta) {
|
|
37
|
+
if (!delta)
|
|
38
|
+
return;
|
|
39
|
+
const existing = target[key];
|
|
40
|
+
const cumulative = Array.isArray(existing) ? existing : [];
|
|
41
|
+
for (let i = 0; i < delta.length; i++) {
|
|
42
|
+
if (cumulative.indexOf(delta[i]) === -1) {
|
|
43
|
+
cumulative.push(delta[i]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (cumulative !== existing)
|
|
47
|
+
target[key] = cumulative;
|
|
48
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function abandonDeferredElement(el: Element): void;
|
|
2
|
+
export declare function abandonDeferredDescendants(root: Element): void;
|
|
3
|
+
export declare function abandonDeferredRange(startMarker: Comment, endMarker: Node): void;
|
|
4
|
+
export declare function abandonDeferredDocumentRoots(): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { firstNodeWithin, MAX_MARKER_SCAN_NODES, nextWithinRoot, safeRemoveAttribute, streamingErrorMessage, } from './streaming-dom.js';
|
|
2
|
+
import { STREAMED_HOST_ATTR } from './streaming-mode.js';
|
|
3
|
+
const STREAMING_BOUNDARY_ABANDON = Symbol.for('microsoft.webui.boundaryAbandon');
|
|
4
|
+
export function abandonDeferredElement(el) {
|
|
5
|
+
try {
|
|
6
|
+
if (!el.hasAttribute(STREAMED_HOST_ATTR))
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
safeRemoveAttribute(el, STREAMED_HOST_ATTR);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const hook = el[STREAMING_BOUNDARY_ABANDON];
|
|
15
|
+
if (typeof hook === 'function')
|
|
16
|
+
hook.call(el);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(`[WebUI] streaming: abandon failed for <${el.tagName?.toLowerCase?.() ?? '?'}>: ${streamingErrorMessage(error)}`);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
safeRemoveAttribute(el, STREAMED_HOST_ATTR);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function abandonDeferredDescendants(root) {
|
|
26
|
+
abandonDeferredNodes(firstNodeWithin(root), root, null);
|
|
27
|
+
}
|
|
28
|
+
export function abandonDeferredRange(startMarker, endMarker) {
|
|
29
|
+
const root = startMarker.parentNode;
|
|
30
|
+
if (root) {
|
|
31
|
+
abandonDeferredNodes(startMarker.nextSibling, root, endMarker);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function abandonDeferredNodes(first, root, end) {
|
|
35
|
+
let node = first;
|
|
36
|
+
let visited = 0;
|
|
37
|
+
while (node && node !== end && visited < MAX_MARKER_SCAN_NODES) {
|
|
38
|
+
visited++;
|
|
39
|
+
if (node.nodeType === 1) {
|
|
40
|
+
abandonDeferredElement(node);
|
|
41
|
+
}
|
|
42
|
+
node = nextWithinRoot(node, root);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function abandonDeferredDocumentRoots() {
|
|
46
|
+
if (typeof document === 'undefined' ||
|
|
47
|
+
typeof document.getElementsByTagName !== 'function') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const elements = document.getElementsByTagName('*');
|
|
51
|
+
let visited = 0;
|
|
52
|
+
for (let i = 0; i < elements.length && visited < MAX_MARKER_SCAN_NODES; i++) {
|
|
53
|
+
const el = elements[i];
|
|
54
|
+
visited++;
|
|
55
|
+
abandonDeferredElement(el);
|
|
56
|
+
const shadowRoot = el.shadowRoot;
|
|
57
|
+
let node = shadowRoot?.firstChild ?? null;
|
|
58
|
+
while (node && visited < MAX_MARKER_SCAN_NODES) {
|
|
59
|
+
visited++;
|
|
60
|
+
if (node.nodeType === 1) {
|
|
61
|
+
abandonDeferredElement(node);
|
|
62
|
+
}
|
|
63
|
+
node = nextWithinRoot(node, shadowRoot);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { elementHasPendingStateForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests } from './streaming-deferred.js';
|
|
2
|
+
export declare function enqueueStreamingSentinel(sentinel: Element): void;
|
|
3
|
+
export declare function installStreamingTruncationGuard(): void;
|
|
4
|
+
export declare function resetStreamingCoordinatorStateForTests(): void;
|
|
5
|
+
export declare function isStreamingHaltedForTests(): boolean;
|
|
6
|
+
export declare function streamingRetentionStateForTests(): readonly [number, number];
|
|
7
|
+
export { elementHasPendingStateForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests, };
|