@microsoft/webui-framework 0.0.24 → 0.0.26

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.
Files changed (53) hide show
  1. package/README.md +209 -53
  2. package/dist/component-asset/asset.d.ts +4 -2
  3. package/dist/component-asset/asset.js +19 -3
  4. package/dist/component-asset/generated-manifest.d.ts +1 -0
  5. package/dist/component-asset/generated-manifest.js +32 -0
  6. package/dist/component-asset/loader.d.ts +2 -1
  7. package/dist/component-asset/loader.js +98 -22
  8. package/dist/component-asset/manifest.d.ts +2 -1
  9. package/dist/component-asset.d.ts +1 -1
  10. package/dist/component-asset.js +12 -2
  11. package/dist/element/link-styles.d.ts +20 -0
  12. package/dist/element/link-styles.js +1030 -0
  13. package/dist/element/markers.d.ts +4 -0
  14. package/dist/element/markers.js +60 -5
  15. package/dist/element/styles.d.ts +39 -1
  16. package/dist/element/styles.js +623 -13
  17. package/dist/element/types.d.ts +5 -3
  18. package/dist/element.js +9 -1
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +1 -0
  21. package/dist/interaction-hydration.d.ts +8 -0
  22. package/dist/interaction-hydration.js +167 -0
  23. package/dist/static-host.js +43 -1
  24. package/dist/streaming-activation.d.ts +2 -2
  25. package/dist/streaming-activation.js +4 -2
  26. package/dist/streaming-bootstrap.d.ts +2 -2
  27. package/dist/streaming-bootstrap.js +7 -1
  28. package/dist/streaming-cleanup.d.ts +1 -0
  29. package/dist/streaming-cleanup.js +66 -14
  30. package/dist/streaming-coordinator.d.ts +3 -2
  31. package/dist/streaming-coordinator.js +107 -47
  32. package/dist/streaming-deferred.d.ts +14 -4
  33. package/dist/streaming-deferred.js +200 -87
  34. package/dist/streaming-dom.d.ts +12 -3
  35. package/dist/streaming-dom.js +41 -27
  36. package/dist/streaming-mode.d.ts +5 -0
  37. package/dist/streaming-mode.js +4 -0
  38. package/dist/streaming-protocol.d.ts +19 -3
  39. package/dist/streaming-protocol.js +4 -3
  40. package/dist/streaming-spans.d.ts +9 -0
  41. package/dist/streaming-spans.js +200 -0
  42. package/dist/template-content.d.ts +17 -0
  43. package/dist/template-content.js +106 -0
  44. package/dist/template-element.d.ts +17 -2
  45. package/dist/template-element.js +493 -177
  46. package/dist/template-events.d.ts +7 -0
  47. package/dist/template-events.js +7 -1
  48. package/dist/template-types.d.ts +1 -2
  49. package/dist/template.d.ts +15 -8
  50. package/dist/template.js +79 -16
  51. package/package.json +9 -3
  52. package/dist/component-asset/resources.d.ts +0 -4
  53. package/dist/component-asset/resources.js +0 -83
@@ -1,22 +1,28 @@
1
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';
2
+ import { abandonDeferredDescendants, abandonDeferredElement, removeStreamingAttributes, } from './streaming-cleanup.js';
3
+ import { firstNodeWithin, MAX_ELEMENTS_PER_BOUNDARY, MAX_MARKER_SCAN_NODES, nextAfterSubtreeWithin, nextWithinRoot, STREAMING_ENCLOSING_SPAN_ATTR, streamingErrorMessage, } from './streaming-dom.js';
4
+ import { ACTIVATION_ACTIVATED, ACTIVATION_ANCESTOR_BARRIER, ACTIVATION_MISSING_TEMPLATE, ACTIVATION_STATIC_HOST_OPT_OUT, PENDING_ROOT_CONNECTED, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
5
5
  import { applyStateUpdate } from './streaming-state.js';
6
- const ACTIVATION_ACTIVATED = 1;
7
- const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
8
- export const ACTIVATION_MISSING_TEMPLATE = 3;
9
- export const ELEMENT_IGNORED = 0;
10
- export const ELEMENT_DEFERRED = 4;
11
- export const ELEMENT_LIMIT_FAILURE = 5;
6
+ export const ELEMENT_IGNORED = 10;
7
+ export const ELEMENT_DEFERRED = 11;
8
+ export const ELEMENT_LIMIT_FAILURE = 12;
9
+ export const ELEMENT_ACTIVATED_FROM_PENDING = 13;
10
+ export const ELEMENT_BARRIER_LIMIT_FAILURE = 14;
11
+ export const ELEMENT_INVALID_OUTCOME = 15;
12
12
  export const MAX_PENDING_UNDEFINED_ROOTS = 50_000;
13
+ export const MAX_PENDING_BARRIER_ROOTS = 50_000;
14
+ function retainsBoundaryUpdates(el, outcome) {
15
+ return outcome === ACTIVATION_ACTIVATED
16
+ || (outcome === ACTIVATION_STATIC_HOST_OPT_OUT
17
+ && typeof el.setState === 'function');
18
+ }
13
19
  const pendingTagWaiters = new Map();
20
+ const pendingBarrierRoots = new Set();
14
21
  let pendingUndefinedRoots = 0;
15
22
  let activationGeneration = 0;
16
23
  let failureHandler = null;
17
- const PENDING_BOUNDARY_STATE = Symbol();
18
- const PENDING_BOUNDARY_UPDATES = Symbol();
19
- const NO_BOUNDARY_STATE = Symbol();
24
+ let invalidActivationOutcome;
25
+ const PENDING_RECORD = Symbol();
20
26
  export function configureStreamingFailureHandler(handler) {
21
27
  failureHandler = handler;
22
28
  }
@@ -26,22 +32,54 @@ function fail(reason) {
26
32
  }
27
33
  failureHandler(reason);
28
34
  }
35
+ function tagOf(el) {
36
+ return el.tagName.toLowerCase();
37
+ }
38
+ function missingTemplateReason(tag) {
39
+ return `template metadata missing while activating <${tag}>`;
40
+ }
41
+ function invalidOutcomeReason(tag) {
42
+ return `<${tag}> returned an unrecognized streaming activation outcome ${String(invalidActivationOutcome)}`;
43
+ }
44
+ function barrierLimitReason() {
45
+ return `pending ancestor-barrier root count exceeds ${MAX_PENDING_BARRIER_ROOTS}`;
46
+ }
29
47
  function requireStateUpdate(el, patch) {
30
48
  if (applyStateUpdate(el, patch))
31
49
  return;
32
- fail(`<${el.tagName.toLowerCase()}> activated without a setState() method`);
50
+ fail(`<${tagOf(el)}> activated without a setState() method`);
51
+ }
52
+ function joinBoundaryUpdates(el, updates) {
53
+ if (updates.active)
54
+ updates.roots.push(el);
55
+ if (updates.patch)
56
+ requireStateUpdate(el, updates.patch);
33
57
  }
34
- function activateMarkedElement(el, state, updates) {
35
- const tag = el.tagName.toLowerCase();
58
+ function activateMarkedElement(el, state, updates, bypass) {
59
+ const tag = tagOf(el);
36
60
  if (tag.indexOf('-') === -1)
37
61
  return ELEMENT_IGNORED;
38
- if (customElements.get(tag))
39
- return invokeActivationHook(el, state);
40
- if (!hasPendingState(el) &&
41
- pendingUndefinedRoots >= MAX_PENDING_UNDEFINED_ROOTS) {
42
- return ELEMENT_LIMIT_FAILURE;
62
+ if (customElements.get(tag)) {
63
+ if (pendingBarrierRoots.has(el)) {
64
+ return activatePendingBarrierRoot(el);
65
+ }
66
+ if (hasPendingRecord(el))
67
+ return ELEMENT_DEFERRED;
68
+ const outcome = invokeActivationHook(el, state, bypass);
69
+ if (outcome !== ACTIVATION_ANCESTOR_BARRIER)
70
+ return outcome;
71
+ if (pendingBarrierRoots.size >= MAX_PENDING_BARRIER_ROOTS) {
72
+ return ELEMENT_BARRIER_LIMIT_FAILURE;
73
+ }
74
+ deferBehindBarrier(el, state, updates, bypass);
75
+ return ELEMENT_DEFERRED;
76
+ }
77
+ if (!hasPendingRecord(el)) {
78
+ if (pendingUndefinedRoots >= MAX_PENDING_UNDEFINED_ROOTS) {
79
+ return ELEMENT_LIMIT_FAILURE;
80
+ }
81
+ stashPendingRecord(el, state, updates, bypass);
43
82
  }
44
- stashPendingState(el, state, updates);
45
83
  let waiter = pendingTagWaiters.get(tag);
46
84
  if (!waiter) {
47
85
  waiter = { generation: activationGeneration, roots: new Set() };
@@ -59,6 +97,58 @@ function activateMarkedElement(el, state, updates) {
59
97
  el[PENDING_ROOT_CONNECTED] = resumePendingRoot;
60
98
  return ELEMENT_DEFERRED;
61
99
  }
100
+ function deferBehindBarrier(el, state, updates, bypass) {
101
+ stashPendingRecord(el, state, updates, bypass);
102
+ pendingBarrierRoots.add(el);
103
+ el[PENDING_ROOT_CONNECTED] = resumeBarrierRoot;
104
+ }
105
+ function activatePendingBarrierRoot(el) {
106
+ if (!pendingBarrierRoots.delete(el))
107
+ return ELEMENT_IGNORED;
108
+ delete el[PENDING_ROOT_CONNECTED];
109
+ const record = takePendingRecord(el);
110
+ const updates = releaseUpdates(record);
111
+ try {
112
+ const outcome = resumeRetainedRoot(el, record, updates);
113
+ if (outcome === ACTIVATION_ANCESTOR_BARRIER)
114
+ return ELEMENT_DEFERRED;
115
+ return outcome === ACTIVATION_MISSING_TEMPLATE ||
116
+ outcome === ELEMENT_INVALID_OUTCOME
117
+ ? outcome
118
+ : ELEMENT_ACTIVATED_FROM_PENDING;
119
+ }
120
+ finally {
121
+ if (updates?.pendingRoots === 0)
122
+ updates.patch = undefined;
123
+ }
124
+ }
125
+ function resumeRetainedRoot(el, record, updates) {
126
+ const outcome = invokeActivationHook(el, record?.state, record?.bypass);
127
+ if (outcome === ACTIVATION_ANCESTOR_BARRIER) {
128
+ deferBehindBarrier(el, record?.state, updates, record?.bypass);
129
+ }
130
+ else if (updates && retainsBoundaryUpdates(el, outcome)) {
131
+ joinBoundaryUpdates(el, updates);
132
+ }
133
+ return outcome;
134
+ }
135
+ function resumeBarrierRoot() {
136
+ try {
137
+ const outcome = activatePendingBarrierRoot(this);
138
+ if (outcome === ACTIVATION_MISSING_TEMPLATE) {
139
+ abandonDeferredTree(this);
140
+ fail(missingTemplateReason(tagOf(this)));
141
+ }
142
+ else if (outcome === ELEMENT_INVALID_OUTCOME) {
143
+ abandonDeferredTree(this);
144
+ fail(invalidOutcomeReason(tagOf(this)));
145
+ }
146
+ }
147
+ catch (error) {
148
+ abandonDeferredDescendants(this);
149
+ reportActivationFailure(tagOf(this), error);
150
+ }
151
+ }
62
152
  function onTagDefined(tag, generation) {
63
153
  if (generation !== activationGeneration)
64
154
  return;
@@ -85,7 +175,7 @@ function onTagDefined(tag, generation) {
85
175
  }
86
176
  }
87
177
  function resumePendingRoot() {
88
- const tag = this.tagName.toLowerCase();
178
+ const tag = tagOf(this);
89
179
  const waiter = pendingTagWaiters.get(tag);
90
180
  if (!waiter ||
91
181
  waiter.generation !== activationGeneration ||
@@ -100,23 +190,30 @@ function activatePendingRoot(tag, waiter, el) {
100
190
  return;
101
191
  pendingUndefinedRoots--;
102
192
  delete el[PENDING_ROOT_CONNECTED];
103
- let updates;
193
+ const record = takePendingRecord(el);
194
+ const updates = releaseUpdates(record);
195
+ const state = record?.state;
196
+ const bypass = record?.bypass;
104
197
  try {
105
- updates = takePendingUpdates(el);
106
- const state = takePendingState(el);
107
- const outcome = invokeActivationHook(el, state);
198
+ const outcome = resumeRetainedRoot(el, record, updates);
108
199
  if (outcome === ACTIVATION_MISSING_TEMPLATE) {
109
- abandonDeferredDescendants(el);
110
- abandonDeferredElement(el);
111
- fail(`template metadata missing while activating <${tag}>`);
200
+ abandonDeferredTree(el);
201
+ fail(missingTemplateReason(tag));
112
202
  return;
113
203
  }
114
- if (updates) {
115
- updates.roots.push(el);
116
- if (updates.patch)
117
- requireStateUpdate(el, updates.patch);
204
+ if (outcome === ELEMENT_INVALID_OUTCOME) {
205
+ abandonDeferredTree(el);
206
+ fail(invalidOutcomeReason(tag));
207
+ return;
208
+ }
209
+ if (outcome === ACTIVATION_ANCESTOR_BARRIER) {
210
+ if (pendingBarrierRoots.size > MAX_PENDING_BARRIER_ROOTS) {
211
+ abandonDeferredTree(el);
212
+ fail(barrierLimitReason());
213
+ }
214
+ return;
118
215
  }
119
- const failure = activateDeferredTree(firstNodeWithin(el), el, null, state, updates ? { updates } : undefined);
216
+ const failure = activateDeferredTree(firstNodeWithin(el), el, null, state, updates || bypass ? { updates, bypass } : undefined);
120
217
  if (failure)
121
218
  fail(failure);
122
219
  }
@@ -136,6 +233,7 @@ function activatePendingRoot(tag, waiter, el) {
136
233
  }
137
234
  export function activateDeferredTree(first, root, end, state, options) {
138
235
  const updates = options?.updates;
236
+ const bypass = options?.bypass;
139
237
  const countRetention = options?.countRetention === true && updates !== undefined;
140
238
  let node = first;
141
239
  let resumeAfterDeferred = null;
@@ -170,28 +268,30 @@ export function activateDeferredTree(first, root, end, state, options) {
170
268
  if (marked) {
171
269
  const el = node;
172
270
  try {
173
- const outcome = activateMarkedElement(el, state, updates);
271
+ const outcome = activateMarkedElement(el, state, updates, bypass);
174
272
  if (outcome === ACTIVATION_MISSING_TEMPLATE) {
175
- return `template metadata missing while activating <${el.tagName.toLowerCase()}>`;
273
+ return missingTemplateReason(tagOf(el));
274
+ }
275
+ if (outcome === ELEMENT_INVALID_OUTCOME) {
276
+ return invalidOutcomeReason(tagOf(el));
176
277
  }
177
278
  if (outcome === ELEMENT_LIMIT_FAILURE) {
178
279
  return `pending undefined root count exceeds ${MAX_PENDING_UNDEFINED_ROOTS}`;
179
280
  }
281
+ if (outcome === ELEMENT_BARRIER_LIMIT_FAILURE) {
282
+ return barrierLimitReason();
283
+ }
180
284
  if (outcome === ELEMENT_DEFERRED) {
181
285
  resumeAfterDeferred = nextAfterSubtreeWithin(node, root);
182
286
  skippingDeferredDescendants = true;
183
287
  }
184
288
  else if (updates &&
185
- (outcome === ACTIVATION_ACTIVATED ||
186
- outcome === ACTIVATION_STATIC_HOST_OPT_OUT)) {
187
- updates.roots.push(el);
188
- if (updates.patch) {
189
- requireStateUpdate(el, updates.patch);
190
- }
289
+ retainsBoundaryUpdates(el, outcome)) {
290
+ joinBoundaryUpdates(el, updates);
191
291
  }
192
292
  }
193
293
  catch (error) {
194
- reportActivationFailure(el.tagName.toLowerCase(), error);
294
+ reportActivationFailure(tagOf(el), error);
195
295
  }
196
296
  }
197
297
  }
@@ -204,73 +304,83 @@ export function activateDeferredTree(first, root, end, state, options) {
204
304
  function reportActivationFailure(tag, error) {
205
305
  console.error(`[WebUI] streaming: late activation failed for <${tag}>: ${streamingErrorMessage(error)}`);
206
306
  }
307
+ function abandonDeferredTree(el) {
308
+ abandonDeferredDescendants(el);
309
+ abandonDeferredElement(el);
310
+ }
207
311
  export function abandonPendingWaiters() {
208
- if (pendingTagWaiters.size === 0) {
209
- pendingUndefinedRoots = 0;
210
- return;
211
- }
212
- for (const waiter of pendingTagWaiters.values()) {
213
- for (const el of waiter.roots)
312
+ if (pendingBarrierRoots.size !== 0) {
313
+ for (const el of pendingBarrierRoots)
214
314
  clearPendingRoot(el);
215
- settleLateActivation();
315
+ pendingBarrierRoots.clear();
316
+ }
317
+ if (pendingTagWaiters.size !== 0) {
318
+ for (const waiter of pendingTagWaiters.values()) {
319
+ for (const el of waiter.roots)
320
+ clearPendingRoot(el);
321
+ settleLateActivation();
322
+ }
323
+ pendingTagWaiters.clear();
216
324
  }
217
- pendingTagWaiters.clear();
218
325
  pendingUndefinedRoots = 0;
219
326
  }
220
327
  function clearPendingRoot(el) {
221
- if (hasPendingState(el))
222
- takePendingState(el);
223
- takePendingUpdates(el);
328
+ releaseUpdates(takePendingRecord(el));
224
329
  delete el[PENDING_ROOT_CONNECTED];
225
- abandonDeferredDescendants(el);
226
- abandonDeferredElement(el);
330
+ abandonDeferredTree(el);
227
331
  }
228
- function stashPendingState(el, state, updates) {
229
- const store = el;
230
- store[PENDING_BOUNDARY_STATE] =
231
- state === undefined ? NO_BOUNDARY_STATE : state;
232
- if (updates) {
233
- store[PENDING_BOUNDARY_UPDATES] = updates;
332
+ function stashPendingRecord(el, state, updates, bypass) {
333
+ el[PENDING_RECORD] = {
334
+ state,
335
+ updates,
336
+ bypass,
337
+ };
338
+ if (updates)
234
339
  updates.pendingRoots++;
235
- }
236
340
  }
237
- function hasPendingState(el) {
238
- return Object.prototype.hasOwnProperty.call(el, PENDING_BOUNDARY_STATE);
341
+ function hasPendingRecord(el) {
342
+ return Object.prototype.hasOwnProperty.call(el, PENDING_RECORD);
239
343
  }
240
- function takePendingState(el) {
344
+ function takePendingRecord(el) {
241
345
  const store = el;
242
- const stored = store[PENDING_BOUNDARY_STATE];
243
- delete store[PENDING_BOUNDARY_STATE];
244
- return stored === NO_BOUNDARY_STATE
245
- ? undefined
246
- : stored;
346
+ const record = store[PENDING_RECORD];
347
+ delete store[PENDING_RECORD];
348
+ return record;
247
349
  }
248
- function takePendingUpdates(el) {
249
- const store = el;
250
- const updates = store[PENDING_BOUNDARY_UPDATES];
251
- delete store[PENDING_BOUNDARY_UPDATES];
252
- if (updates)
253
- updates.pendingRoots--;
254
- return updates;
350
+ function releaseUpdates(record) {
351
+ const updates = record?.updates;
352
+ if (!updates)
353
+ return undefined;
354
+ updates.pendingRoots--;
355
+ return updates.active || updates.patch !== undefined ? updates : undefined;
255
356
  }
256
- function invokeActivationHook(el, state) {
357
+ function invokeActivationHook(el, state, bypass) {
257
358
  const hook = el[STREAMING_BOUNDARY_ACTIVATE];
258
359
  if (typeof hook !== 'function')
259
360
  return ACTIVATION_MISSING_TEMPLATE;
361
+ const bypassAncestor = bypass !== undefined &&
362
+ el.getAttribute(STREAMING_ENCLOSING_SPAN_ATTR) === bypass.id
363
+ ? bypass.host
364
+ : undefined;
260
365
  let outcome;
261
366
  try {
262
- outcome = hook.call(el, state);
367
+ outcome = hook.call(el, state, bypassAncestor);
263
368
  }
264
369
  catch (error) {
265
- safeRemoveAttribute(el, STREAMED_HOST_ATTR);
370
+ removeStreamingAttributes(el);
266
371
  throw error;
267
372
  }
268
- if (outcome !== ACTIVATION_ACTIVATED &&
269
- outcome !== ACTIVATION_STATIC_HOST_OPT_OUT) {
270
- return ACTIVATION_MISSING_TEMPLATE;
373
+ if (outcome === ACTIVATION_ACTIVATED ||
374
+ outcome === ACTIVATION_STATIC_HOST_OPT_OUT) {
375
+ removeStreamingAttributes(el);
376
+ return outcome;
271
377
  }
272
- safeRemoveAttribute(el, STREAMED_HOST_ATTR);
273
- return outcome;
378
+ if (outcome === ACTIVATION_ANCESTOR_BARRIER ||
379
+ outcome === ACTIVATION_MISSING_TEMPLATE) {
380
+ return outcome;
381
+ }
382
+ invalidActivationOutcome = outcome;
383
+ return ELEMENT_INVALID_OUTCOME;
274
384
  }
275
385
  export function resetDeferredActivationForTests() {
276
386
  abandonPendingWaiters();
@@ -282,6 +392,9 @@ export function pendingTagWaiterCountForTests() {
282
392
  export function pendingUndefinedRootCountForTests() {
283
393
  return pendingUndefinedRoots;
284
394
  }
395
+ export function pendingBarrierRootCountForTests() {
396
+ return pendingBarrierRoots.size;
397
+ }
285
398
  export function elementHasPendingStateForTests(el) {
286
- return hasPendingState(el);
399
+ return hasPendingRecord(el);
287
400
  }
@@ -2,7 +2,14 @@ export declare const BOUNDARY_SCRIPT_ATTR = "data-webui-boundary";
2
2
  export declare const MAX_ELEMENTS_PER_BOUNDARY = 10000;
3
3
  export declare const MAX_MARKER_SCAN_NODES = 50000;
4
4
  export declare const MAX_BOUNDARY_SCRIPT_SCAN = 8;
5
+ export declare const STREAMING_SPAN_HOST_ATTR = "data-ws-span";
6
+ export declare const STREAMING_ENCLOSING_SPAN_ATTR = "data-ws-enclosing";
5
7
  export declare function streamingErrorMessage(error: unknown): string;
8
+ export declare const BOUNDARY_START_PREFIX = "wb:";
9
+ export declare const BOUNDARY_END_PREFIX = "/wb:";
10
+ export declare const SPAN_START_PREFIX = "ws:";
11
+ export declare const SPAN_END_PREFIX = "/ws:";
12
+ export declare function isRangeEndMarker(data: string): boolean;
6
13
  export interface HydrationRange {
7
14
  readonly start: Comment | null;
8
15
  readonly end: Comment | null;
@@ -20,10 +27,12 @@ export type RangeResolution = {
20
27
  readonly truncated: true;
21
28
  readonly start: Comment | null;
22
29
  };
23
- export declare function resolveBoundaryRange(scriptEl: Element, sequence: number, terminal: boolean): RangeResolution;
30
+ export declare function resolveBoundaryRange(scriptEl: Element, instanceId: number): RangeResolution;
31
+ export declare function resolveSpanRange(scriptEl: Element, instanceId: number): RangeResolution;
32
+ export declare function markerlessRecordViolation(scriptEl: Element, kind: string): string | null;
24
33
  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;
34
+ export declare function findRangeEndMarkerByPrefix(scriptEl: Element): Comment | null;
35
+ export declare function findRangeStartMarkerByPrefix(endMarker: Comment): Comment | null;
27
36
  export declare function safeRemove(node: Node | null | undefined): void;
28
37
  export declare function safeRemoveAttribute(el: Element, name: string): void;
29
38
  export declare function removeBoundaryScaffolding(sentinel: Element, scriptEl: Element | null, startMarker: Comment | null, endMarker: Comment | null): void;
@@ -2,41 +2,52 @@ export const BOUNDARY_SCRIPT_ATTR = 'data-webui-boundary';
2
2
  export const MAX_ELEMENTS_PER_BOUNDARY = 10_000;
3
3
  export const MAX_MARKER_SCAN_NODES = 50_000;
4
4
  export const MAX_BOUNDARY_SCRIPT_SCAN = 8;
5
+ export const STREAMING_SPAN_HOST_ATTR = 'data-ws-span';
6
+ export const STREAMING_ENCLOSING_SPAN_ATTR = 'data-ws-enclosing';
5
7
  export function streamingErrorMessage(error) {
6
8
  return error instanceof Error ? error.message : String(error);
7
9
  }
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);
10
+ export const BOUNDARY_START_PREFIX = 'wb:';
11
+ export const BOUNDARY_END_PREFIX = '/wb:';
12
+ export const SPAN_START_PREFIX = 'ws:';
13
+ export const SPAN_END_PREFIX = '/ws:';
14
+ export function isRangeEndMarker(data) {
15
+ return data.startsWith(BOUNDARY_END_PREFIX) ||
16
+ data.startsWith(SPAN_END_PREFIX);
17
+ }
18
+ const MARKER_KIND_BOUNDARY = 'boundary';
19
+ const MARKER_KIND_SPAN = 'span';
20
+ export function resolveBoundaryRange(scriptEl, instanceId) {
21
+ return resolveMarkerRange(scriptEl, instanceId, BOUNDARY_START_PREFIX, BOUNDARY_END_PREFIX, MARKER_KIND_BOUNDARY);
22
+ }
23
+ export function resolveSpanRange(scriptEl, instanceId) {
24
+ return resolveMarkerRange(scriptEl, instanceId, SPAN_START_PREFIX, SPAN_END_PREFIX, MARKER_KIND_SPAN);
25
+ }
26
+ function resolveMarkerRange(scriptEl, instanceId, startPrefix, endPrefix, kind) {
27
+ const end = findEndMarker(scriptEl, instanceId, endPrefix);
13
28
  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);
29
+ const start = findStartMarkerBefore(end, instanceId, startPrefix);
22
30
  if (!start) {
23
31
  return {
24
32
  ok: false,
25
- reason: `missing start marker for boundary ${sequence}`,
33
+ reason: `missing start marker for ${kind} ${instanceId}`,
26
34
  truncated: false,
27
35
  };
28
36
  }
29
37
  return { ok: true, range: { start, end } };
30
38
  }
31
- if (terminal)
32
- return { ok: true, range: MARKERLESS_RANGE };
33
39
  return {
34
40
  ok: false,
35
- reason: `missing end marker for boundary ${sequence}`,
41
+ reason: `missing end marker for ${kind} ${instanceId}`,
36
42
  truncated: true,
37
- start: findStartMarkerBefore(scriptEl, sequence),
43
+ start: findStartMarkerBefore(scriptEl, instanceId, startPrefix),
38
44
  };
39
45
  }
46
+ export function markerlessRecordViolation(scriptEl, kind) {
47
+ return previousRangeEndMarker(scriptEl)
48
+ ? `${kind} record must be markerless`
49
+ : null;
50
+ }
40
51
  export function findBoundaryScript(sentinel) {
41
52
  let node = sentinel.previousElementSibling;
42
53
  for (let hops = 0; node && hops < MAX_BOUNDARY_SCRIPT_SCAN; hops++) {
@@ -48,21 +59,24 @@ export function findBoundaryScript(sentinel) {
48
59
  }
49
60
  return null;
50
61
  }
51
- function findEndMarker(scriptEl, sequence) {
62
+ function findEndMarker(scriptEl, instanceId, prefix) {
52
63
  const marker = previousComment(scriptEl);
53
- return marker?.data === `${BOUNDARY_END_PREFIX}${sequence}`
64
+ return marker?.data === `${prefix}${instanceId}`
54
65
  ? marker
55
66
  : null;
56
67
  }
57
- function findStartMarkerBefore(nodeBefore, sequence) {
58
- return findCommentBefore(nodeBefore, `${BOUNDARY_START_PREFIX}${sequence}`);
68
+ function findStartMarkerBefore(nodeBefore, instanceId, prefix) {
69
+ return findCommentBefore(nodeBefore, `${prefix}${instanceId}`);
59
70
  }
60
- export function findEndMarkerByPrefix(scriptEl) {
61
- const marker = previousComment(scriptEl);
62
- return marker?.data.startsWith(BOUNDARY_END_PREFIX) ? marker : null;
71
+ export function findRangeEndMarkerByPrefix(scriptEl) {
72
+ return previousRangeEndMarker(scriptEl);
73
+ }
74
+ export function findRangeStartMarkerByPrefix(endMarker) {
75
+ return findCommentBefore(endMarker, endMarker.data.slice(1));
63
76
  }
64
- export function findStartMarkerByPrefix(endMarker) {
65
- return findCommentBefore(endMarker, `${BOUNDARY_START_PREFIX}${endMarker.data.slice(BOUNDARY_END_PREFIX.length)}`);
77
+ function previousRangeEndMarker(node) {
78
+ const marker = previousComment(node);
79
+ return marker && isRangeEndMarker(marker.data) ? marker : null;
66
80
  }
67
81
  function previousComment(node) {
68
82
  const previous = node.previousSibling;
@@ -1,5 +1,10 @@
1
1
  export declare const STREAMING_BOUNDARY_ACTIVATE: unique symbol;
2
2
  export declare const PENDING_ROOT_CONNECTED: unique symbol;
3
3
  export declare const STREAMED_HOST_ATTR = "data-ws";
4
+ export declare const ACTIVATION_ACTIVATED = 1;
5
+ export declare const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
6
+ export declare const ACTIVATION_MISSING_TEMPLATE = 3;
7
+ export declare const ACTIVATION_ANCESTOR_BARRIER = 4;
8
+ export type ActivationOutcome = typeof ACTIVATION_ACTIVATED | typeof ACTIVATION_STATIC_HOST_OPT_OUT | typeof ACTIVATION_MISSING_TEMPLATE | typeof ACTIVATION_ANCESTOR_BARRIER;
4
9
  export declare function isStreamingHydrationMode(): boolean;
5
10
  export declare function resetStreamingModeForTests(): void;
@@ -2,6 +2,10 @@ let cached;
2
2
  export const STREAMING_BOUNDARY_ACTIVATE = Symbol.for('microsoft.webui.boundaryActivate');
3
3
  export const PENDING_ROOT_CONNECTED = Symbol.for('microsoft.webui.pendingRootConnected');
4
4
  export const STREAMED_HOST_ATTR = 'data-ws';
5
+ export const ACTIVATION_ACTIVATED = 1;
6
+ export const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
7
+ export const ACTIVATION_MISSING_TEMPLATE = 3;
8
+ export const ACTIVATION_ANCESTOR_BARRIER = 4;
5
9
  export function isStreamingHydrationMode() {
6
10
  if (cached !== undefined)
7
11
  return cached;
@@ -1,5 +1,9 @@
1
1
  import type { TemplateMeta } from './template.js';
2
+ import type { ComponentStyles } from './element/styles.js';
3
+ export declare const STREAMING_PROTOCOL_VERSION = 2;
2
4
  export interface BoundaryBootstrap {
5
+ declarationId: number;
6
+ enclosingSpanInstanceId?: number;
3
7
  state?: Record<string, unknown>;
4
8
  templates?: Record<string, TemplateMeta>;
5
9
  inventory?: string;
@@ -7,14 +11,26 @@ export interface BoundaryBootstrap {
7
11
  chain?: unknown[];
8
12
  css?: string[];
9
13
  styles?: string[];
14
+ componentStyles: ComponentStyles;
10
15
  [key: string]: unknown;
11
16
  }
12
17
  export declare const RECORD_KIND_FINAL_CHECKPOINT = 0;
13
18
  export declare const RECORD_KIND_UPDATABLE_CHECKPOINT = 1;
14
19
  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>;
20
+ export declare const RECORD_KIND_SPAN_COMPLETION = 3;
21
+ export declare const RECORD_KIND_TERMINAL = 4;
22
+ export type BoundaryRecordKind = typeof RECORD_KIND_FINAL_CHECKPOINT | typeof RECORD_KIND_UPDATABLE_CHECKPOINT | typeof RECORD_KIND_STATE_UPDATE | typeof RECORD_KIND_SPAN_COMPLETION | typeof RECORD_KIND_TERMINAL;
23
+ export interface SpanCompletionPayload {
24
+ state?: Record<string, unknown>;
25
+ templates?: Record<string, TemplateMeta>;
26
+ inventory?: string;
27
+ nonce?: string;
28
+ chain?: unknown[];
29
+ css?: string[];
30
+ styles?: string[];
31
+ [key: string]: unknown;
32
+ }
33
+ export type BoundaryRecordPayload = BoundaryBootstrap | SpanCompletionPayload | Record<string, unknown>;
18
34
  export type BoundaryEnvelope = readonly [
19
35
  version: number,
20
36
  recordSequence: number,
@@ -1,8 +1,9 @@
1
- const SUPPORTED_VERSION = 1;
1
+ export const STREAMING_PROTOCOL_VERSION = 2;
2
2
  export const RECORD_KIND_FINAL_CHECKPOINT = 0;
3
3
  export const RECORD_KIND_UPDATABLE_CHECKPOINT = 1;
4
4
  export const RECORD_KIND_STATE_UPDATE = 2;
5
- export const RECORD_KIND_TERMINAL = 3;
5
+ export const RECORD_KIND_SPAN_COMPLETION = 3;
6
+ export const RECORD_KIND_TERMINAL = 4;
6
7
  function invalid(reason) {
7
8
  return { ok: false, reason };
8
9
  }
@@ -17,7 +18,7 @@ export function parseBoundaryEnvelope(text) {
17
18
  if (!Array.isArray(parsed) || parsed.length !== 5) {
18
19
  return invalid('boundary envelope must be a 5-element [version, recordSequence, kind, target, payload] array');
19
20
  }
20
- if (parsed[0] !== SUPPORTED_VERSION) {
21
+ if (parsed[0] !== STREAMING_PROTOCOL_VERSION) {
21
22
  return invalid(`unsupported boundary envelope version ${JSON.stringify(parsed[0])}`);
22
23
  }
23
24
  return {
@@ -0,0 +1,9 @@
1
+ import type { HydrationRange } from './streaming-dom.js';
2
+ export declare const MAX_SPAN_NESTING = 32;
3
+ export declare function spanHostFor(id: number): Element | undefined;
4
+ export declare function registerEnclosingSpans(renderRoot: Node, enclosingSpanInstanceId: number): string | null;
5
+ export declare function prepareSpanCompletion(id: number, range: HydrationRange): string | null;
6
+ export declare function completeSpan(id: number): void;
7
+ export declare function abandonOpenSpans(): void;
8
+ export declare function openSpanCountForTests(): number;
9
+ export declare function hasOpenSpans(): boolean;