octane 0.2.13 → 0.2.15

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 (36) hide show
  1. package/dist/cjs/constants.cjs +7 -7
  2. package/dist/cjs/dom-binding-claims.cjs +28 -0
  3. package/dist/cjs/dom-binding-handoff.cjs +2 -2
  4. package/dist/cjs/dom-bindings.cjs +105 -13
  5. package/dist/cjs/runtime.cjs +311 -55
  6. package/dist/cjs/server/streamed-signals.cjs +3 -2
  7. package/dist/cjs/signals/control-binding.cjs +33 -9
  8. package/dist/cjs/signals/control-handoff.cjs +39 -0
  9. package/dist/cjs/stream-protocol.cjs +6 -0
  10. package/dist/cjs/version.cjs +1 -1
  11. package/dist/compiler/compile.js +86 -24
  12. package/dist/compiler/dom-binding-program.js +30 -3
  13. package/dist/compiler/dom-bindings.js +115 -34
  14. package/dist/compiler/volar.js +58 -17
  15. package/dist/constants.d.ts +2 -2
  16. package/dist/constants.js +2 -2
  17. package/dist/dom-binding-claims.d.ts +1 -0
  18. package/dist/dom-binding-claims.js +4 -0
  19. package/dist/dom-binding-handoff.d.ts +6 -4
  20. package/dist/dom-binding-handoff.js +1 -1
  21. package/dist/dom-binding-program.d.ts +2 -1
  22. package/dist/dom-binding-program.js +16 -1
  23. package/dist/dom-bindings.d.ts +5 -2
  24. package/dist/dom-bindings.js +105 -13
  25. package/dist/runtime.d.ts +34 -3
  26. package/dist/runtime.js +314 -55
  27. package/dist/server/streamed-signals.js +3 -2
  28. package/dist/signals/control-binding.d.ts +9 -5
  29. package/dist/signals/control-binding.js +38 -6
  30. package/dist/signals/control-handoff.d.ts +26 -0
  31. package/dist/signals/control-handoff.js +12 -0
  32. package/dist/signals/index.d.ts +1 -0
  33. package/dist/stream-protocol.d.ts +4 -0
  34. package/dist/stream-protocol.js +4 -0
  35. package/dist/version.js +1 -1
  36. package/package.json +1 -1
@@ -1,5 +1,9 @@
1
1
  import { normalizeClass } from "./class-names.js";
2
+ import { domBindingClaims } from "./dom-binding-claims.js";
2
3
  import { sanitizeURL } from "./sanitize-url.js";
4
+ import { STREAM_SCRIPT_ATTR, SUSPENSE_SCRIPT_ATTR } from "./stream-protocol.js";
5
+ import { NATIVE_SIGNAL_SEED_ATTR } from "./signals/native-read-seeds.js";
6
+ import { BINDING_HANDOFF } from "./signals/control-handoff.js";
3
7
  import { captureSignalOwner, currentSignalOwner } from "./signals/owner-context.js";
4
8
  import {
5
9
  NATIVE_TRANSITION_CONSUMER,
@@ -59,6 +63,11 @@ function __releaseBinding(node, name) {
59
63
  const channels = claims.get(node);
60
64
  channels.delete(name);
61
65
  if (channels.size === 0) claims.delete(node);
66
+ const published = domBindingClaims.get(node);
67
+ if (published) {
68
+ published.delete(name);
69
+ if (published.size === 0) domBindingClaims.delete(node);
70
+ }
62
71
  }
63
72
  function resolveNodes(root, descriptor) {
64
73
  if (root?.nodeType !== 1 || root.getAttribute("data-octane-bindings") !== descriptor.id || descriptor.nodes.length === 0) {
@@ -68,9 +77,9 @@ function resolveNodes(root, descriptor) {
68
77
  const nodes = [];
69
78
  const childIndices = [];
70
79
  for (let i = 0; i < descriptor.nodes.length; i++) {
71
- const [parent, tag, namespace, children] = descriptor.nodes[i];
80
+ const [parent, tag, namespace, children, , text] = descriptor.nodes[i];
72
81
  const node = i === 0 ? parent === -1 ? root : void 0 : parent >= 0 && parent < i && descriptor.nodes[parent][3] !== null ? nodes[parent]?.children[childIndices[parent]++] : void 0;
73
- if (node === void 0 || descriptor.nodes[i][4] === true || node.localName !== tag || node.namespaceURI !== namespaces[namespace] || children !== null && (node.childNodes.length !== children || node.children.length !== children)) {
82
+ if (node === void 0 || descriptor.nodes[i][4] === true || node.localName !== tag || node.namespaceURI !== namespaces[namespace] || (text ? node.childNodes.length > 1 || node.firstChild !== null && node.firstChild.nodeType !== 3 : children !== null && (node.childNodes.length !== children || node.children.length !== children))) {
74
83
  throw new Error("DOM bindings cannot adopt a mismatched static element topology.");
75
84
  }
76
85
  nodes.push(node);
@@ -111,9 +120,9 @@ function resolveAddressedNodes(root, descriptor) {
111
120
  }
112
121
  const childCounts = new Array(descriptor.nodes.length).fill(0);
113
122
  for (let i = 0; i < descriptor.nodes.length; i++) {
114
- const [parent, tag, namespace, children, openChildren] = descriptor.nodes[i];
123
+ const [parent, tag, namespace, children, openChildren, text] = descriptor.nodes[i];
115
124
  const node = nodes[i];
116
- if (node === void 0 || node.localName !== tag || node.namespaceURI !== namespaces[namespace] || (i === 0 ? parent !== -1 || node !== root : parent < 0 || parent >= i || descriptor.nodes[parent][3] === null || node.parentElement !== nodes[parent] || descriptor.nodes[parent][4] !== true && nodes[parent].children[childCounts[parent]] !== node) || openChildren && children === null || children !== null && !openChildren && (node.childNodes.length !== children || node.children.length !== children)) {
125
+ if (node === void 0 || node.localName !== tag || node.namespaceURI !== namespaces[namespace] || (i === 0 ? parent !== -1 || node !== root : parent < 0 || parent >= i || descriptor.nodes[parent][3] === null || node.parentElement !== nodes[parent] || descriptor.nodes[parent][4] !== true && nodes[parent].children[childCounts[parent]] !== node) || openChildren && children === null || (text ? node.childNodes.length > 1 || node.firstChild !== null && node.firstChild.nodeType !== 3 : children !== null && !openChildren && (node.childNodes.length !== children || node.children.length !== children))) {
117
126
  throw new Error("DOM bindings cannot adopt a mismatched addressed element topology.");
118
127
  }
119
128
  if (parent !== -1) childCounts[parent]++;
@@ -128,6 +137,11 @@ function resolveAddressedNodes(root, descriptor) {
128
137
  function normalize(binding, value) {
129
138
  const type = typeof value;
130
139
  switch (binding[1]) {
140
+ case "text":
141
+ if (value == null || value === false) return "";
142
+ if (type !== "string" && type !== "number" && type !== "bigint" && type !== "boolean")
143
+ throw new TypeError("DOM binding text must be a synchronous scalar.");
144
+ return String(value);
131
145
  case "styleObject":
132
146
  return value;
133
147
  case "class":
@@ -155,7 +169,11 @@ function normalize(binding, value) {
155
169
  }
156
170
  function write(node, binding, value) {
157
171
  const name = binding[2];
158
- if (binding[1] === "classToken") {
172
+ if (binding[1] === "text") {
173
+ const text = node.firstChild;
174
+ if (text === null) node.appendChild(node.ownerDocument.createTextNode(value));
175
+ else if (text.nodeValue !== value) text.nodeValue = value;
176
+ } else if (binding[1] === "classToken") {
159
177
  node.classList.toggle(name, value !== null);
160
178
  } else if (binding[1] === "url") {
161
179
  if (value === "" && !(name === "href" && node.localName === "a")) value = null;
@@ -254,16 +272,28 @@ function __adoptBindings(root, descriptor, source, options) {
254
272
  let dirty = false;
255
273
  let revision = 0;
256
274
  let unsubscribe;
275
+ let publicationRetries;
276
+ let published;
257
277
  const signal = options?.signal;
258
- const dispose = () => {
259
- if (disposed) return;
278
+ const dispose = (_disposal, publish, preservePresentation = false) => {
279
+ if (disposed) {
280
+ publish?.();
281
+ return;
282
+ }
260
283
  disposed = true;
284
+ publicationRetries?.clear();
261
285
  signal?.removeEventListener("abort", dispose);
262
286
  let failed = false;
263
287
  let failure;
288
+ try {
289
+ publish?.();
290
+ } catch (error) {
291
+ failed = true;
292
+ failure = error;
293
+ }
264
294
  for (const connection of signalConnections?.values() ?? []) {
265
295
  try {
266
- connection.dispose();
296
+ connection.dispose(preservePresentation);
267
297
  } catch (error) {
268
298
  if (!failed) {
269
299
  failed = true;
@@ -273,7 +303,7 @@ function __adoptBindings(root, descriptor, source, options) {
273
303
  }
274
304
  for (const connection of projections ? new Set(projections.values()) : []) {
275
305
  try {
276
- connection.dispose();
306
+ connection.dispose(preservePresentation);
277
307
  } catch (error) {
278
308
  if (!failed) {
279
309
  failed = true;
@@ -298,9 +328,9 @@ function __adoptBindings(root, descriptor, source, options) {
298
328
  for (let i = 0; i < owned.length; i++) {
299
329
  const [node, name] = owned[i];
300
330
  try {
301
- groups?.get(i)?.dispose();
302
- styles?.get(i)?.dispose();
303
- if (bindings[i][1] === "classToken" && previous[i] === "")
331
+ groups?.get(i)?.dispose(preservePresentation);
332
+ styles?.get(i)?.dispose(preservePresentation);
333
+ if (!preservePresentation && bindings[i][1] === "classToken" && previous[i] === "")
304
334
  node.classList.remove(bindings[i][2]);
305
335
  } catch (error) {
306
336
  if (!failed) {
@@ -356,7 +386,21 @@ function __adoptBindings(root, descriptor, source, options) {
356
386
  styles.set(i, style = __createBindingStyleRestoration(nodes[binding[0]], binding));
357
387
  style.write(next[i]);
358
388
  } else write(nodes[binding[0]], binding, next[i]);
359
- if (!disposed) previous[i] = next[i];
389
+ if (!disposed) {
390
+ previous[i] = next[i];
391
+ if (binding[1] === "attr" || binding[1] === "boolean" || binding[1] === "aria" || binding[1] === "class" || binding[1] === "styleProperty" || binding[1] === "text") {
392
+ const [node, name] = owned[i];
393
+ let published2 = next[i];
394
+ if (binding[1] === "styleProperty") {
395
+ const style = node.style;
396
+ const value = style.getPropertyValue(binding[2]);
397
+ published2 = value === "" ? null : value + (style.getPropertyPriority(binding[2]) ? " !important" : "");
398
+ }
399
+ let claims2 = domBindingClaims.get(node);
400
+ if (claims2 === void 0) domBindingClaims.set(node, claims2 = /* @__PURE__ */ new Map());
401
+ claims2.set(name, published2);
402
+ }
403
+ }
360
404
  }
361
405
  };
362
406
  const drain = (preview = false) => {
@@ -571,6 +615,7 @@ function __adoptBindings(root, descriptor, source, options) {
571
615
  writePrepared(next, indices, preparedControls, preparedGroups);
572
616
  } finally {
573
617
  busy = false;
618
+ published?.();
574
619
  }
575
620
  }
576
621
  };
@@ -593,6 +638,7 @@ function __adoptBindings(root, descriptor, source, options) {
593
638
  for (const index of previousUpdates ?? []) signalUpdates.add(index);
594
639
  }
595
640
  busy = false;
641
+ if (!preview) published?.();
596
642
  }
597
643
  };
598
644
  const refresh = () => {
@@ -608,6 +654,52 @@ function __adoptBindings(root, descriptor, source, options) {
608
654
  prepare: () => run(() => drain(true))
609
655
  };
610
656
  const handle = { refresh, dispose };
657
+ if (descriptor.handoff === "host") {
658
+ published = () => {
659
+ if (!publicationRetries?.size) return;
660
+ const pending = [...publicationRetries];
661
+ publicationRetries.clear();
662
+ for (const retry of pending)
663
+ queueMicrotask(() => {
664
+ if (!disposed) retry();
665
+ });
666
+ };
667
+ const host = root;
668
+ const document = host.ownerDocument;
669
+ const ancestry = [];
670
+ for (let node = host; node !== null; node = node.parentNode)
671
+ ancestry.push([node, node.parentNode]);
672
+ const sibling = (node, previous2) => {
673
+ let next = previous2 ? node.previousSibling : node.nextSibling;
674
+ while (next?.nodeType === 1 && next.localName === "script" && (next.hasAttribute(NATIVE_SIGNAL_SEED_ATTR) || next.hasAttribute(SUSPENSE_SCRIPT_ATTR) || next.hasAttribute(STREAM_SCRIPT_ATTR)))
675
+ next = previous2 ? next.previousSibling : next.nextSibling;
676
+ return next;
677
+ };
678
+ const previousSibling = sibling(host, true);
679
+ const nextSibling = sibling(host, false);
680
+ let handoff;
681
+ handle[BINDING_HANDOFF] = () => {
682
+ if (handoff !== void 0) return handoff;
683
+ return handoff = {
684
+ id: descriptor.id,
685
+ root: host,
686
+ anchor: host,
687
+ host: new Set(
688
+ bindings.map(
689
+ (binding) => binding[1] === "styleProperty" ? "style:" + binding[2] : binding[1].startsWith("style") ? "style" : binding[2]
690
+ )
691
+ ),
692
+ revision: () => busy ? -1 : revision,
693
+ afterPublication: (callback) => {
694
+ (publicationRetries ??= /* @__PURE__ */ new Set()).add(callback);
695
+ return () => publicationRetries.delete(callback);
696
+ },
697
+ valid: () => host.parentNode !== null && host.ownerDocument === document && ancestry.every(([node, parent]) => node.parentNode === parent) && sibling(host, true) === previousSibling && sibling(host, false) === nextSibling && host.getAttribute("data-octane-bindings") === descriptor.id,
698
+ active: () => !disposed,
699
+ retire: (publish) => dispose(void 0, publish, true)
700
+ };
701
+ };
702
+ }
611
703
  if (signal?.aborted) {
612
704
  dispose();
613
705
  return handle;
package/dist/runtime.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import type { BindingHandle } from './dom-bindings.js';
16
16
  import { type BindingHandoff } from './dom-binding-handoff.js';
17
+ import { type ControlHandoff, type SignalControlBinding } from './signals/control-handoff.js';
17
18
  import { EXTERNAL_HYDRATION_PROMISE, HYDRATION_RANGE_BOUNDARY } from './constants.js';
18
19
  import type { HydrateProps } from './hydration/types.js';
19
20
  import type { IndependentHydrateActivator } from './hydration/independent-island.js';
@@ -23,7 +24,7 @@ import { SignalCandidateFrame } from './signals/transition-candidate.js';
23
24
  import { setNativeAdoptionResolver } from './signals/read-protocol.js';
24
25
  import { type NativeAdoptionState } from './signals/native-read-seeds.js';
25
26
  export { documentSignalOwner, installStreamedSignalOwnerActivator, } from './signals/document-owner.js';
26
- import { type SignalOwner, type SignalRendererOwnerIdentity } from './signals/types.js';
27
+ import { type SignalHandle, type SignalOwner, type SignalRendererOwnerIdentity } from './signals/types.js';
27
28
  export { EXTERNAL_HYDRATION_PROMISE, HYDRATION_RANGE_BOUNDARY };
28
29
  export { validateNativeReadWitness };
29
30
  export type ComponentBody<P = any, E = any> = (props: P, scope: Scope, extra: E) => void;
@@ -526,6 +527,7 @@ interface RootRenderOwner {
526
527
  /** Shared document/account data owner; distinct from this root's presentation owner. */
527
528
  signalOwner?: SignalOwner;
528
529
  bindingLeases?: Set<BindingHandoff>;
530
+ controlLeases?: Map<Element, ControlHandoff | undefined>;
529
531
  preservePresentation?: boolean;
530
532
  bindingContainer?: Node;
531
533
  adopt?: (block: Block) => void;
@@ -1428,10 +1430,18 @@ interface PresentationHydrationFrame {
1428
1430
  readToken: number;
1429
1431
  witnessToken: number;
1430
1432
  witnesses: Map<object, NativeReadWitness | null>;
1433
+ /** Successor native writers only; a host receipt never tears down children. */
1434
+ hostSuccessors?: Map<object, () => void>;
1435
+ controls?: Map<Element, {
1436
+ valid: (retired?: boolean) => boolean;
1437
+ binding: DirectSignalBinding;
1438
+ lease: ControlHandoff | undefined;
1439
+ stop?: () => void;
1440
+ }>;
1431
1441
  completed: boolean;
1432
1442
  lease: BindingHandoff;
1433
1443
  revision?: number;
1434
- /** Installed only for structural receipts so ordinary commits do not retain the validator. */
1444
+ /** Installed only for structural/control/host receipts, not ordinary fixed presentations. */
1435
1445
  current?: typeof currentPresentation;
1436
1446
  hydration: HydrationCapability | null;
1437
1447
  }
@@ -1439,7 +1449,7 @@ interface PresentationHydrationFrame {
1439
1449
  export declare function presentationFailure(error: unknown): never;
1440
1450
  declare function currentPresentation(frame: PresentationHydrationFrame): boolean;
1441
1451
  /** @internal Only compiler-proven native views enter this publication boundary. */
1442
- export declare function beginPresentationHydration(scope: Scope, id: string, supported?: boolean, structural?: boolean, conditionalRest?: boolean): PresentationHydrationFrame | null;
1452
+ export declare function beginPresentationHydration(scope: Scope, id: string, supported?: boolean, structural?: boolean, conditionalRest?: boolean, host?: boolean): PresentationHydrationFrame | null;
1443
1453
  /** @internal Restore the read frame even when preparation suspends or throws. */
1444
1454
  export declare function endPresentationHydration(frame: PresentationHydrationFrame | null, completed?: boolean): void;
1445
1455
  /** @internal Only authored host spreads retain generic host preparation. */
@@ -1465,7 +1475,26 @@ export declare function child<T extends Node>(node: T): Node | null;
1465
1475
  */
1466
1476
  export declare function sibling(node: Node, n?: number): Node | null;
1467
1477
  export declare function setText(node: Text, value: any): void;
1478
+ declare const DIRECT_SIGNAL_BINDING: unique symbol;
1479
+ type DirectSignalBindingKind = 'text' | 'textOnlyChild' | 'attribute' | 'value' | 'checked';
1468
1480
  type DirectSignalAttributeKind = 'attr' | 'class' | 'booleanAttr' | 'ariaAttr' | 'stringData';
1481
+ interface DirectSignalBinding {
1482
+ readonly [DIRECT_SIGNAL_BINDING]: true;
1483
+ readonly scope: Scope;
1484
+ readonly kind: DirectSignalBindingKind;
1485
+ readonly target: Node;
1486
+ readonly site: string;
1487
+ readonly name?: string;
1488
+ readonly attributeKind?: DirectSignalAttributeKind;
1489
+ readonly handle: SignalHandle<unknown> | null;
1490
+ text?: Text;
1491
+ value: unknown;
1492
+ unsubscribe?: () => void;
1493
+ input?: EventListener;
1494
+ controlWriterCleanup?: () => void;
1495
+ pendingControl?: boolean;
1496
+ disposed: boolean;
1497
+ }
1469
1498
  /** @internal Compiler target for a direct signal/scalar text binding. */
1470
1499
  export declare function bindSignalText(scope: Scope, previous: unknown, position: Node, value: unknown, site: string, onlyChild?: boolean, seededText?: 1 | undefined, bindingMarker?: string, previousValue?: unknown): unknown;
1471
1500
  /** @internal Compiler target for a direct signal/scalar attribute binding. */
@@ -2284,6 +2313,8 @@ export interface Root {
2284
2313
  export interface RootOptions {
2285
2314
  /** Adopt fixed native early bindings only when their matching hydration capture commits. */
2286
2315
  bindingLeases?: readonly BindingHandle[];
2316
+ /** Offer an existing textarea value owner for accepted presentation hydration, never early disposal. */
2317
+ controlLeases?: readonly SignalControlBinding[];
2287
2318
  /**
2288
2319
  * Shared document/account owner for module signals. Roots borrow this owner;
2289
2320
  * unmounting a presentation root never retires shared data state.