@microsoft/webui-framework 0.0.27 → 0.0.28
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 +8 -2
- package/dist/streaming-bootstrap.js +2 -0
- package/dist/streaming-coordinator.js +12 -7
- package/dist/streaming-protocol.d.ts +4 -2
- package/dist/streaming-protocol.js +2 -6
- package/dist/streaming-record-state.d.ts +6 -0
- package/dist/streaming-record-state.js +54 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -269,8 +269,8 @@ activation. The coordinator does not publish that state to
|
|
|
269
269
|
scaffolding after commit. Updates apply state to retained roots and never insert
|
|
270
270
|
markup or rerun hydration.
|
|
271
271
|
|
|
272
|
-
The browser reads
|
|
273
|
-
`[
|
|
272
|
+
The browser reads the single unversioned
|
|
273
|
+
`[sequence, kind, target, payload]` contract for final checkpoints,
|
|
274
274
|
updatable checkpoints, updates, span completions, and terminal. Every commit
|
|
275
275
|
also emits a `performance.mark()` - `webui:boundary:<id>`,
|
|
276
276
|
`webui:boundary:<id>:update`, `webui:span:<id>`, or
|
|
@@ -278,6 +278,12 @@ also emits a `performance.mark()` - `webui:boundary:<id>`,
|
|
|
278
278
|
Set `window.__WEBUI_STREAMING_DEBUG__ = true` only when tooling needs the live
|
|
279
279
|
`webui:boundary-hydrated` event as well.
|
|
280
280
|
|
|
281
|
+
A range record can reuse the exact preceding range state with `stateRef` and
|
|
282
|
+
carry only a top-level `stateDelta`. References are backward-only and resolved
|
|
283
|
+
before activation; missing, stale, forward, or malformed references halt and
|
|
284
|
+
clean up the stream. The reference base is released on terminal, cancellation,
|
|
285
|
+
failure, or coordinator reset.
|
|
286
|
+
|
|
281
287
|
Set `window.__WEBUI_STREAMING_SLICE_MS__` to a positive millisecond budget to
|
|
282
288
|
make the coordinator yield between boundaries instead of draining its queue in
|
|
283
289
|
one pass. That is for pages where an intermediary coalesces the response into a
|
|
@@ -12,6 +12,8 @@ export function applyBoundaryBootstrap(bootstrap) {
|
|
|
12
12
|
const key = keys[i];
|
|
13
13
|
if (key === 'templates' ||
|
|
14
14
|
key === 'state' ||
|
|
15
|
+
key === 'stateRef' ||
|
|
16
|
+
key === 'stateDelta' ||
|
|
15
17
|
key === 'componentStyles' ||
|
|
16
18
|
key === 'declarationId' ||
|
|
17
19
|
key === 'enclosingSpanInstanceId')
|
|
@@ -6,6 +6,7 @@ import { abandonPendingWaiters, configureStreamingFailureHandler, elementHasPend
|
|
|
6
6
|
import { findBoundaryScript, findRangeEndMarkerByPrefix, findRangeStartMarkerByPrefix, markerlessRecordViolation, removeBoundaryScaffolding, resolveBoundaryRange, resolveSpanRange, streamingErrorMessage, } from './streaming-dom.js';
|
|
7
7
|
import { parseBoundaryEnvelope, RECORD_KIND_FINAL_CHECKPOINT, RECORD_KIND_SPAN_COMPLETION, RECORD_KIND_STATE_UPDATE, RECORD_KIND_TERMINAL, RECORD_KIND_UPDATABLE_CHECKPOINT, } from './streaming-protocol.js';
|
|
8
8
|
import { applyStateUpdate } from './streaming-state.js';
|
|
9
|
+
import { clearRangeState, resolveRangeState, } from './streaming-record-state.js';
|
|
9
10
|
import { abandonOpenSpans, completeSpan, hasOpenSpans, openSpanCountForTests, prepareSpanCompletion, registerEnclosingSpans, spanHostFor, } from './streaming-spans.js';
|
|
10
11
|
const MAX_QUEUED_BOUNDARIES = 512;
|
|
11
12
|
const MAX_UPDATABLE_BOUNDARIES = 128;
|
|
@@ -110,6 +111,7 @@ function fail(reason) {
|
|
|
110
111
|
abandonPendingWaiters();
|
|
111
112
|
abandonOpenSpans();
|
|
112
113
|
clearUpdatableBoundaries();
|
|
114
|
+
clearRangeState();
|
|
113
115
|
settlePendingTerminal(false);
|
|
114
116
|
for (let i = queueHead; i < queue.length; i++) {
|
|
115
117
|
discardRejectedBoundary(queue[i]);
|
|
@@ -151,7 +153,7 @@ function processSentinel(sentinel) {
|
|
|
151
153
|
failBoundary(sentinel, parsed.reason);
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
|
-
const [
|
|
156
|
+
const [sequence, kind, target, payload] = parsed.envelope;
|
|
155
157
|
if (sequence !== nextExpectedRecordSequence) {
|
|
156
158
|
failBoundary(sentinel, `expected streaming record sequence ${nextExpectedRecordSequence}, received ${sequence}`);
|
|
157
159
|
return;
|
|
@@ -213,11 +215,12 @@ function commitRecord(span, payload, range, sequence, target, updatable, sentine
|
|
|
213
215
|
markBoundaryPending();
|
|
214
216
|
let committed = false;
|
|
215
217
|
try {
|
|
218
|
+
const state = resolveRangeState(payload, sequence);
|
|
216
219
|
if (span) {
|
|
217
|
-
hydrateSpanCompletion(payload, range, target);
|
|
220
|
+
hydrateSpanCompletion(payload, state, range, target);
|
|
218
221
|
}
|
|
219
222
|
else {
|
|
220
|
-
hydrateCheckpoint(payload, range, target, updatable);
|
|
223
|
+
hydrateCheckpoint(payload, state, range, target, updatable);
|
|
221
224
|
}
|
|
222
225
|
committed = true;
|
|
223
226
|
}
|
|
@@ -235,7 +238,7 @@ function commitRecord(span, payload, range, sequence, target, updatable, sentine
|
|
|
235
238
|
markBoundaryCommitted(false);
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
|
-
function hydrateCheckpoint(bootstrap, range, target, updatable) {
|
|
241
|
+
function hydrateCheckpoint(bootstrap, state, range, target, updatable) {
|
|
239
242
|
let bypass;
|
|
240
243
|
const enclosing = bootstrap.enclosingSpanInstanceId;
|
|
241
244
|
if (enclosing !== undefined && range.start?.parentNode) {
|
|
@@ -251,7 +254,7 @@ function hydrateCheckpoint(bootstrap, range, target, updatable) {
|
|
|
251
254
|
const boundary = updatable
|
|
252
255
|
? newUpdatableBoundary()
|
|
253
256
|
: undefined;
|
|
254
|
-
activateRootsBetween(range.start, range.end,
|
|
257
|
+
activateRootsBetween(range.start, range.end, state, boundary, bypass);
|
|
255
258
|
if (boundary)
|
|
256
259
|
retainUpdatableBoundary(target, boundary);
|
|
257
260
|
}
|
|
@@ -262,12 +265,12 @@ function hydrateCheckpoint(bootstrap, range, target, updatable) {
|
|
|
262
265
|
function newUpdatableBoundary() {
|
|
263
266
|
return { roots: [], active: true, retained: 0, pendingRoots: 0 };
|
|
264
267
|
}
|
|
265
|
-
function hydrateSpanCompletion(payload, range, target) {
|
|
268
|
+
function hydrateSpanCompletion(payload, state, range, target) {
|
|
266
269
|
const invalid = prepareSpanCompletion(target, range);
|
|
267
270
|
if (invalid)
|
|
268
271
|
throw new Error(invalid);
|
|
269
272
|
applyBoundaryBootstrap(payload);
|
|
270
|
-
activateRootsBetween(range.start, range.end,
|
|
273
|
+
activateRootsBetween(range.start, range.end, state);
|
|
271
274
|
completeSpan(target);
|
|
272
275
|
}
|
|
273
276
|
function failRangeResolution(sentinel, scriptEl, resolved) {
|
|
@@ -321,6 +324,7 @@ function commitTerminal(sequence, sentinel, scriptEl) {
|
|
|
321
324
|
removeBoundaryScaffolding(sentinel, scriptEl, null, null);
|
|
322
325
|
terminalCommitted = true;
|
|
323
326
|
pendingTerminalSequence = sequence;
|
|
327
|
+
clearRangeState();
|
|
324
328
|
clearUpdatableBoundaries(true);
|
|
325
329
|
scheduleTerminalValidation();
|
|
326
330
|
}
|
|
@@ -403,6 +407,7 @@ export function resetStreamingCoordinatorStateForTests() {
|
|
|
403
407
|
abandonOpenSpans();
|
|
404
408
|
settlePendingTerminal(false);
|
|
405
409
|
clearUpdatableBoundaries();
|
|
410
|
+
clearRangeState();
|
|
406
411
|
coordinatorGeneration++;
|
|
407
412
|
queue.length = 0;
|
|
408
413
|
queueHead = 0;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { TemplateMeta } from './template.js';
|
|
2
2
|
import type { ComponentStyles } from './element/styles.js';
|
|
3
|
-
export declare const STREAMING_PROTOCOL_VERSION = 2;
|
|
4
3
|
export interface BoundaryBootstrap {
|
|
5
4
|
declarationId: number;
|
|
6
5
|
enclosingSpanInstanceId?: number;
|
|
7
6
|
state?: Record<string, unknown>;
|
|
7
|
+
stateRef?: number;
|
|
8
|
+
stateDelta?: Record<string, unknown>;
|
|
8
9
|
templates?: Record<string, TemplateMeta>;
|
|
9
10
|
inventory?: string;
|
|
10
11
|
nonce?: string;
|
|
@@ -22,6 +23,8 @@ export declare const RECORD_KIND_TERMINAL = 4;
|
|
|
22
23
|
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
24
|
export interface SpanCompletionPayload {
|
|
24
25
|
state?: Record<string, unknown>;
|
|
26
|
+
stateRef?: number;
|
|
27
|
+
stateDelta?: Record<string, unknown>;
|
|
25
28
|
templates?: Record<string, TemplateMeta>;
|
|
26
29
|
inventory?: string;
|
|
27
30
|
nonce?: string;
|
|
@@ -32,7 +35,6 @@ export interface SpanCompletionPayload {
|
|
|
32
35
|
}
|
|
33
36
|
export type BoundaryRecordPayload = BoundaryBootstrap | SpanCompletionPayload | Record<string, unknown>;
|
|
34
37
|
export type BoundaryEnvelope = readonly [
|
|
35
|
-
version: number,
|
|
36
38
|
recordSequence: number,
|
|
37
39
|
kind: BoundaryRecordKind,
|
|
38
40
|
target: number,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export const STREAMING_PROTOCOL_VERSION = 2;
|
|
2
1
|
export const RECORD_KIND_FINAL_CHECKPOINT = 0;
|
|
3
2
|
export const RECORD_KIND_UPDATABLE_CHECKPOINT = 1;
|
|
4
3
|
export const RECORD_KIND_STATE_UPDATE = 2;
|
|
@@ -15,11 +14,8 @@ export function parseBoundaryEnvelope(text) {
|
|
|
15
14
|
catch {
|
|
16
15
|
return invalid('boundary payload is not valid JSON');
|
|
17
16
|
}
|
|
18
|
-
if (!Array.isArray(parsed) || parsed.length !==
|
|
19
|
-
return invalid('boundary envelope must be a
|
|
20
|
-
}
|
|
21
|
-
if (parsed[0] !== STREAMING_PROTOCOL_VERSION) {
|
|
22
|
-
return invalid(`unsupported boundary envelope version ${JSON.stringify(parsed[0])}`);
|
|
17
|
+
if (!Array.isArray(parsed) || parsed.length !== 4) {
|
|
18
|
+
return invalid('boundary envelope must be a 4-element [recordSequence, kind, target, payload] array');
|
|
23
19
|
}
|
|
24
20
|
return {
|
|
25
21
|
ok: true,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BoundaryBootstrap, SpanCompletionPayload } from './streaming-protocol.js';
|
|
2
|
+
type RangePayload = BoundaryBootstrap | SpanCompletionPayload;
|
|
3
|
+
type StreamedState = Record<string, unknown> | undefined;
|
|
4
|
+
export declare function resolveRangeState(payload: RangePayload, sequence: number): StreamedState;
|
|
5
|
+
export declare function clearRangeState(): void;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
let lastRangeSequence;
|
|
2
|
+
let lastRangeState;
|
|
3
|
+
export function resolveRangeState(payload, sequence) {
|
|
4
|
+
const reference = payload.stateRef;
|
|
5
|
+
if (reference === undefined) {
|
|
6
|
+
if (payload.stateDelta !== undefined) {
|
|
7
|
+
throw new Error('streaming state delta is missing its stateRef');
|
|
8
|
+
}
|
|
9
|
+
const state = payload.state;
|
|
10
|
+
if (state !== undefined && !isStateObject(state)) {
|
|
11
|
+
throw new Error('streaming range state must be an object');
|
|
12
|
+
}
|
|
13
|
+
lastRangeSequence = sequence;
|
|
14
|
+
lastRangeState = cloneState(state);
|
|
15
|
+
return state;
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isSafeInteger(reference) ||
|
|
18
|
+
reference < 0 ||
|
|
19
|
+
reference >= sequence) {
|
|
20
|
+
throw new Error(`invalid streaming state reference ${String(reference)}`);
|
|
21
|
+
}
|
|
22
|
+
if (payload.state !== undefined) {
|
|
23
|
+
throw new Error('referenced streaming state must not include complete state');
|
|
24
|
+
}
|
|
25
|
+
if (lastRangeSequence === undefined || reference !== lastRangeSequence) {
|
|
26
|
+
throw new Error(`streaming state reference ${reference} does not match prior range record ${lastRangeSequence === undefined ? 'none' : lastRangeSequence}`);
|
|
27
|
+
}
|
|
28
|
+
const delta = payload.stateDelta;
|
|
29
|
+
if (delta !== undefined && !isStateObject(delta)) {
|
|
30
|
+
throw new Error('streaming state delta must be an object');
|
|
31
|
+
}
|
|
32
|
+
const state = mergeStateDelta(lastRangeState, delta);
|
|
33
|
+
lastRangeSequence = sequence;
|
|
34
|
+
lastRangeState = state;
|
|
35
|
+
return cloneState(state);
|
|
36
|
+
}
|
|
37
|
+
export function clearRangeState() {
|
|
38
|
+
lastRangeSequence = undefined;
|
|
39
|
+
lastRangeState = undefined;
|
|
40
|
+
}
|
|
41
|
+
function isStateObject(value) {
|
|
42
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function mergeStateDelta(base, delta) {
|
|
45
|
+
if (delta === undefined || Object.keys(delta).length === 0)
|
|
46
|
+
return base;
|
|
47
|
+
if (base === undefined) {
|
|
48
|
+
throw new Error('streaming state delta references missing base state');
|
|
49
|
+
}
|
|
50
|
+
return { ...base, ...delta };
|
|
51
|
+
}
|
|
52
|
+
function cloneState(state) {
|
|
53
|
+
return state === undefined ? undefined : structuredClone(state);
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/webui-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"tag": "latest"
|
|
6
|
+
},
|
|
4
7
|
"type": "module",
|
|
5
8
|
"description": "WebUI Framework Next — Preact-inspired lightweight Web Component runtime with SSR hydration and compiled-template path mapping.",
|
|
6
9
|
"license": "MIT",
|
|
@@ -43,7 +46,7 @@
|
|
|
43
46
|
"@playwright/test": "^1.61.1",
|
|
44
47
|
"@types/node": "^26.1.0",
|
|
45
48
|
"typescript": "7.0.2",
|
|
46
|
-
"@microsoft/webui-test-support": "0.0.
|
|
49
|
+
"@microsoft/webui-test-support": "0.0.28"
|
|
47
50
|
},
|
|
48
51
|
"scripts": {
|
|
49
52
|
"build": "tsc",
|