@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 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 version-2
273
- `[2, sequence, kind, target, payload]` records for final checkpoints,
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 [, sequence, kind, target, payload] = parsed.envelope;
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, bootstrap.state, boundary, bypass);
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, payload.state);
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 !== 5) {
19
- return invalid('boundary envelope must be a 5-element [version, recordSequence, kind, target, payload] array');
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.27",
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.27"
49
+ "@microsoft/webui-test-support": "0.0.28"
47
50
  },
48
51
  "scripts": {
49
52
  "build": "tsc",