octane 0.2.13 → 0.2.14

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.
@@ -248,7 +248,7 @@ function importedProjectionCall(node, imports, lexical, parameterScope) {
248
248
  const callee = unwrap(node?.callee);
249
249
  let importedRoot = callee;
250
250
  while (importedRoot?.type === 'MemberExpression' && !importedRoot.computed)
251
- importedRoot = importedRoot.object;
251
+ importedRoot = unwrap(importedRoot.object);
252
252
  const namespace =
253
253
  importedRoot !== callee &&
254
254
  importedRoot?.type === 'Identifier' &&
@@ -695,6 +695,8 @@ function planView(fn, filename, source, imports, lexical, native = null) {
695
695
  const signalIndices = [];
696
696
  const styleIndices = [];
697
697
  const projectionGroups = [];
698
+ const providerBindings = new Set();
699
+ const unboundAttributes = new Set();
698
700
  const classAttributes = new Map();
699
701
  const id = `d:${strongHash(`octane:dom-bindings:2\0${filename}\0${fn.id.name}\0${source}`)}`;
700
702
  const parameterScope = lexical.nodeScopes.get(fn.body) ?? lexical.rootScope;
@@ -702,14 +704,15 @@ function planView(fn, filename, source, imports, lexical, native = null) {
702
704
  let addressed = false;
703
705
  const markUnbound = (expression, opaque = false) => {
704
706
  const value = unwrap(expression);
707
+ const callee = unwrap(value?.callee);
705
708
  const external =
706
- value?.type === 'CallExpression' && value.callee.type === 'Identifier'
707
- ? imports.get(value.callee.name)
709
+ value?.type === 'CallExpression' && callee?.type === 'Identifier'
710
+ ? imports.get(callee.name)
708
711
  : null;
709
712
  if (
710
713
  external?.source !== 'octane/behavior' ||
711
714
  external.imported !== 'unbound' ||
712
- lexical.resolveBinding(lexical.nodeScopes.get(value.callee), value.callee.name)?.scope !==
715
+ lexical.resolveBinding(lexical.nodeScopes.get(callee), callee.name)?.scope !==
713
716
  lexical.rootScope
714
717
  )
715
718
  return false;
@@ -918,7 +921,9 @@ function planView(fn, filename, source, imports, lexical, native = null) {
918
921
  const externalNames = new Set();
919
922
  const knownFields = new Set();
920
923
  for (const attr of element.openingElement?.attributes ?? element.attributes ?? []) {
921
- if (attr._octaneKnownAttributeSpread) {
924
+ // A provider proves normal-renderer fields, not early ownership. External
925
+ // spreads retain the ordinary unbound checks and are stripped below.
926
+ if (attr._octaneKnownAttributeSpread && !attr._octaneKnownAttributeSpread.unbound) {
922
927
  const argument = attr.argument ?? attr.value.expression;
923
928
  // The compiler verified the exact imported factory against its
924
929
  // provider contract. Its arguments still need the ordinary proof.
@@ -956,6 +961,7 @@ function planView(fn, filename, source, imports, lexical, native = null) {
956
961
  if (!group) signalIndices.push(bindings.length);
957
962
  bindings.push([index, bindingKind(tag, name), name]);
958
963
  }
964
+ providerBindings.add(bindings.length - 1);
959
965
  values.push(
960
966
  group
961
967
  ? inheritHookMemoOrigin(
@@ -979,26 +985,31 @@ function planView(fn, filename, source, imports, lexical, native = null) {
979
985
  error(filename, attr, 'binding attribute spreads must be explicitly unbound');
980
986
  if (bindings.some((binding) => binding[0] === index))
981
987
  error(filename, attr, 'unbound attribute spreads must precede owned binding attributes');
988
+ const addExternalName = (raw, node) => {
989
+ const name = (
990
+ raw === 'className' ? 'class' : (ATTRIBUTE_ALIASES.get(raw) ?? raw)
991
+ ).toLowerCase();
992
+ if (
993
+ FORBIDDEN_ATTRS.has(name) ||
994
+ name.startsWith('on') ||
995
+ name.startsWith('data-octane-class-')
996
+ )
997
+ error(
998
+ filename,
999
+ node,
1000
+ `unbound spreads cannot supply reserved or structural attribute ${JSON.stringify(raw)}`,
1001
+ );
1002
+ externalNames.add(name);
1003
+ };
982
1004
  const external = unwrap(unwrap(attr.argument).arguments[0]);
983
- if (external?.type === 'ObjectExpression') {
1005
+ if (attr._octaneKnownAttributeSpread) {
1006
+ for (const raw of attr._octaneKnownAttributeSpread.fields) addExternalName(raw, attr);
1007
+ } else if (external?.type === 'ObjectExpression') {
984
1008
  for (const property of external.properties) {
985
1009
  if (property.type !== 'Property' || property.computed) continue;
986
1010
  const raw = property.key.name ?? property.key.value;
987
1011
  if (typeof raw !== 'string') continue;
988
- const name = (
989
- raw === 'className' ? 'class' : (ATTRIBUTE_ALIASES.get(raw) ?? raw)
990
- ).toLowerCase();
991
- if (
992
- FORBIDDEN_ATTRS.has(name) ||
993
- name.startsWith('on') ||
994
- name.startsWith('data-octane-class-')
995
- )
996
- error(
997
- filename,
998
- property,
999
- `unbound spreads cannot supply reserved or structural attribute ${JSON.stringify(raw)}`,
1000
- );
1001
- externalNames.add(name);
1012
+ addExternalName(raw, property);
1002
1013
  }
1003
1014
  }
1004
1015
  continue;
@@ -1022,6 +1033,16 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1022
1033
  nodes[index][3] = null;
1023
1034
  continue;
1024
1035
  }
1036
+ const value =
1037
+ attr.value === null
1038
+ ? b.literal(true)
1039
+ : unwrap(
1040
+ attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value,
1041
+ );
1042
+ if ((lower === 'ref' || /^on[A-Z]/.test(raw)) && markUnbound(value)) {
1043
+ unboundAttributes.add(lower);
1044
+ continue;
1045
+ }
1025
1046
  if (
1026
1047
  !/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(name) ||
1027
1048
  lower.startsWith('on') ||
@@ -1030,13 +1051,10 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1030
1051
  ) {
1031
1052
  error(filename, attr, `attribute ${JSON.stringify(raw)} is not supported in binding views`);
1032
1053
  }
1033
- const value =
1034
- attr.value === null
1035
- ? b.literal(true)
1036
- : unwrap(
1037
- attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value,
1038
- );
1039
- if (markUnbound(value)) continue;
1054
+ if (markUnbound(value)) {
1055
+ unboundAttributes.add(lower);
1056
+ continue;
1057
+ }
1040
1058
  if (value?.type !== 'Literal' && externalNames.has(lower))
1041
1059
  error(
1042
1060
  filename,
@@ -1144,6 +1162,28 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1144
1162
  }
1145
1163
  };
1146
1164
  visit(render, -1, native?.namespace ?? 0, native?.ancestors ?? []);
1165
+ // Opaque descendants are outside this receipt. Only a scalar native host's
1166
+ // declared presentation channels can transfer independently of those children.
1167
+ const hostHandoff =
1168
+ nodes.length === 1 &&
1169
+ nodes[0][3] === null &&
1170
+ !addressed &&
1171
+ !['textarea', 'input', 'select'].includes(nodes[0][1]) &&
1172
+ bindings.length > 0 &&
1173
+ bindings.every(
1174
+ (binding, index) =>
1175
+ binding[0] === 0 &&
1176
+ (['class', 'styleProperty', 'styleAttribute', 'styleObject'].includes(binding[1]) ||
1177
+ providerBindings.has(index)),
1178
+ ) &&
1179
+ [...(render.openingElement?.attributes ?? render.attributes ?? [])].every(
1180
+ (attr) =>
1181
+ !['JSXSpreadAttribute', 'SpreadAttribute'].includes(attr.type) ||
1182
+ (attr._octaneKnownAttributeSpread && !attr._octaneKnownAttributeSpread.unbound),
1183
+ ) &&
1184
+ !(render.openingElement?.attributes ?? render.attributes ?? []).some(
1185
+ (attr) => attrName(attr)?.toLowerCase() === 'dangerouslysetinnerhtml',
1186
+ );
1147
1187
  return {
1148
1188
  fn,
1149
1189
  render,
@@ -1159,6 +1199,8 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1159
1199
  unbound,
1160
1200
  classAttributes,
1161
1201
  id,
1202
+ hostHandoff,
1203
+ unboundAttributes,
1162
1204
  };
1163
1205
  }
1164
1206
 
@@ -1181,6 +1223,7 @@ function scalarProperties(
1181
1223
  b.prop('init', b.id('nodes'), literalData(plan.nodes)),
1182
1224
  b.prop('init', b.id('bindings'), literalData(plan.bindings)),
1183
1225
  b.prop('init', b.id('project'), project),
1226
+ ...(plan.hostHandoff ? [b.prop('init', b.id('handoff'), b.literal('host'))] : []),
1184
1227
  ...(plan.signalIndices.length
1185
1228
  ? [
1186
1229
  b.prop('init', b.id('signalIndices'), literalData(plan.signalIndices)),
@@ -1876,12 +1919,13 @@ export function prepareDomBindings(ast, source, filename, selectedExport, helper
1876
1919
  : new Map();
1877
1920
  const isUnbound = (expression) => {
1878
1921
  const value = unwrap(expression);
1879
- if (value?.type !== 'CallExpression' || value.callee.type !== 'Identifier') return false;
1880
- const imported = imports.get(value.callee.name);
1922
+ const callee = unwrap(value?.callee);
1923
+ if (value?.type !== 'CallExpression' || callee?.type !== 'Identifier') return false;
1924
+ const imported = imports.get(callee.name);
1881
1925
  return (
1882
1926
  imported?.source === 'octane/behavior' &&
1883
1927
  imported.imported === 'unbound' &&
1884
- lexical.resolveBinding(lexical.nodeScopes.get(value.callee), value.callee.name)?.scope ===
1928
+ lexical.resolveBinding(lexical.nodeScopes.get(callee), callee.name)?.scope ===
1885
1929
  lexical.rootScope
1886
1930
  );
1887
1931
  };
@@ -2132,6 +2176,9 @@ export function prepareDomBindings(ast, source, filename, selectedExport, helper
2132
2176
  _octanePresentationHydration: {
2133
2177
  id: plan.id,
2134
2178
  supported: fixedPresentation || structuralPresentation,
2179
+ ...(field('nodes').elements.some((node) => node.elements[5]?.value === 'value')
2180
+ ? { nativeControl: true }
2181
+ : {}),
2135
2182
  ...(structuralPresentation || conditionalRest ? { structural: true } : {}),
2136
2183
  ...(conditionalRest ? { conditionalRest: true } : {}),
2137
2184
  },
@@ -2207,6 +2254,16 @@ export function prepareDomBindings(ast, source, filename, selectedExport, helper
2207
2254
  );
2208
2255
  }
2209
2256
  for (const [node, replacement] of rewritten) replacements.set(node, replacement);
2257
+ if (plan.hostHandoff)
2258
+ replacements.set(node, {
2259
+ ...mapCow(node, rewritten),
2260
+ _octanePresentationHydration: {
2261
+ id: plan.id,
2262
+ supported: true,
2263
+ host: true,
2264
+ unboundAttributes: plan.unboundAttributes,
2265
+ },
2266
+ });
2210
2267
  });
2211
2268
  if (selectedExport !== null) {
2212
2269
  const plan = plans.get(selectedExport);
@@ -31829,11 +31829,10 @@ var OCTANE_PLATFORM = {
31829
31829
  }
31830
31830
  };
31831
31831
  var octaneTransform = createJsxTransform(OCTANE_PLATFORM);
31832
- function projectNativeAttributeReads(ast, contracts) {
31832
+ function projectNativeDomReads(ast, contracts) {
31833
31833
  const attributes = new Set(
31834
31834
  contracts?.flatMap((contract) => contract.jsxAttribute ? [contract.jsxAttribute] : [])
31835
31835
  );
31836
- if (attributes.size === 0) return ast;
31837
31836
  const names = /* @__PURE__ */ new Set();
31838
31837
  const walk2 = (node, replace) => {
31839
31838
  if (!node || typeof node !== "object") return node;
@@ -31858,20 +31857,54 @@ function projectNativeAttributeReads(ast, contracts) {
31858
31857
  let helper = "__octane_nativeAttributeValue";
31859
31858
  while (names.has(helper)) helper += "_";
31860
31859
  let used = false;
31860
+ const read = (value) => {
31861
+ used = true;
31862
+ return inheritHookMemoOrigin(builders_exports.call(builders_exports.id(helper), value), value);
31863
+ };
31864
+ const projectText = (node) => {
31865
+ if (!node) return node;
31866
+ if (["TSAsExpression", "TSTypeAssertion", "TSSatisfiesExpression"].includes(node.type) && node.typeAnnotation?.type === "TSStringKeyword") {
31867
+ used = true;
31868
+ const expression = projectText(node.expression);
31869
+ return {
31870
+ ...node,
31871
+ expression: expression === node.expression ? builders_exports.call(builders_exports.id(helper), expression) : expression
31872
+ };
31873
+ }
31874
+ if ([
31875
+ "TSAsExpression",
31876
+ "TSTypeAssertion",
31877
+ "TSNonNullExpression",
31878
+ "TSSatisfiesExpression",
31879
+ "TSInstantiationExpression",
31880
+ "ParenthesizedExpression"
31881
+ ].includes(node.type)) {
31882
+ const expression = projectText(node.expression);
31883
+ return expression === node.expression ? node : { ...node, expression };
31884
+ }
31885
+ return node;
31886
+ };
31861
31887
  const projected = walk2(ast, (node) => {
31862
- if (node.type !== "JSXOpeningElement" && node.type !== "Element") return null;
31888
+ if (["JSXElement", "JSXFragment", "Element", "Fragment"].includes(node.type)) {
31889
+ node = {
31890
+ ...node,
31891
+ children: node.children.map((child) => {
31892
+ if (child.type !== "JSXExpressionContainer") return child;
31893
+ const expression = projectText(child.expression);
31894
+ return expression === child.expression ? child : { ...child, expression };
31895
+ })
31896
+ };
31897
+ }
31898
+ if (node.type !== "JSXOpeningElement" && node.type !== "Element") return node;
31863
31899
  const name = node.type === "Element" ? node.id : node.name;
31864
31900
  if (name?.type !== "JSXIdentifier" && name?.type !== "Identifier" || typeof name.name !== "string" || name.name[0] !== name.name[0].toLowerCase())
31865
- return null;
31901
+ return node;
31866
31902
  return {
31867
31903
  ...node,
31868
31904
  attributes: node.attributes.map((attribute) => {
31869
31905
  if (!attributes.has(attribute.name?.name ?? attribute.name) || attribute.value?.type !== "JSXExpressionContainer")
31870
31906
  return attribute;
31871
- const result = lowerNativeAttributeReads(attribute.value.expression, (value) => {
31872
- used = true;
31873
- return inheritHookMemoOrigin(builders_exports.call(builders_exports.id(helper), value), value);
31874
- });
31907
+ const result = lowerNativeAttributeReads(attribute.value.expression, read);
31875
31908
  return { ...attribute, value: { ...attribute.value, expression: result.expression } };
31876
31909
  })
31877
31910
  };
@@ -32099,7 +32132,7 @@ function compileToVolarMappings(source, filename, options) {
32099
32132
  const transform = selectOctaneTransform(ast);
32100
32133
  const transformed = transform(
32101
32134
  projectServerContextCalls(
32102
- renderer.target === "dom" ? projectNativeAttributeReads(transformAst, options?.knownAttributeSpreads) : transformAst,
32135
+ renderer.target === "dom" ? projectNativeDomReads(transformAst, options?.knownAttributeSpreads) : transformAst,
32103
32136
  filename
32104
32137
  ),
32105
32138
  source,
@@ -32251,14 +32284,22 @@ function compileTypesInspection(source, filename, options) {
32251
32284
  const renderer = resolveRendererForFile(rendererConfig, filename ?? "untitled.tsrx");
32252
32285
  const rendererPragma = authoredLeadingPragma(ast, comments) ? null : createRendererTypePragma(renderer, ast);
32253
32286
  const transform = selectOctaneTransform(ast);
32254
- const transformed = transform(projectServerContextCalls(ast, filename), source, filename, {
32255
- collect: true,
32256
- loose: true,
32257
- typeOnly: true,
32258
- inspect: true,
32259
- errors,
32260
- comments: rendererPragma === null ? comments : [rendererPragma, ...comments]
32261
- });
32287
+ const transformed = transform(
32288
+ projectServerContextCalls(
32289
+ renderer.target === "dom" ? projectNativeDomReads(ast) : ast,
32290
+ filename
32291
+ ),
32292
+ source,
32293
+ filename,
32294
+ {
32295
+ collect: true,
32296
+ loose: true,
32297
+ typeOnly: true,
32298
+ inspect: true,
32299
+ errors,
32300
+ comments: rendererPragma === null ? comments : [rendererPragma, ...comments]
32301
+ }
32302
+ );
32262
32303
  markNativeTemplateBodies(ast);
32263
32304
  return {
32264
32305
  code: transformed.code,
@@ -50,7 +50,7 @@ export declare const HYDRATION_TEXT_SEP = " ";
50
50
  * render order) so a hydrating boundary returns synchronously instead of
51
51
  * re-suspending. Shared so server emit and client read stay byte-identical.
52
52
  */
53
- export declare const SUSPENSE_SCRIPT_ATTR = "data-octane-suspense";
53
+ export { SUSPENSE_SCRIPT_ATTR } from './stream-protocol.js';
54
54
  export declare const SUSPENSE_RESOLVED_COMMENT = "oct-suspense:";
55
55
  export declare const SUSPENSE_RESOLVED_SEED_ATTR = "data-octane-suspense-seeds";
56
56
  export declare const SUSPENSE_RESOLVED_NATIVE_ATTR = "data-octane-suspense-signals";
@@ -105,7 +105,7 @@ export declare const STREAM_SEGMENT_ATTR = "data-oct-s";
105
105
  /** Per-boundary seed-JSON script attribute (inside the segment). */
106
106
  export declare const STREAM_SEED_ATTR = "data-oct-seed";
107
107
  /** Renderer-owned executable/data scripts emitted by the streaming protocol. */
108
- export declare const STREAM_SCRIPT_ATTR = "data-octane-stream";
108
+ export { STREAM_SCRIPT_ATTR } from './stream-protocol.js';
109
109
  /** Hidden carrier for Float sheet resources discovered after the shell; the
110
110
  * inline `$OCTRH` call hoists its tags into document.head. */
111
111
  export declare const STREAM_RESOURCE_ATTR = "data-oct-fr";
package/dist/constants.js CHANGED
@@ -8,7 +8,7 @@ const FOR_BLOCK_OPEN_EMPTY = `<!--${HYDRATION_FOR_EMPTY}-->`;
8
8
  const FOR_BLOCK_OPEN_ITEMS = `<!--${HYDRATION_FOR_ITEMS}-->`;
9
9
  const EMPTY_COMMENT = "<!---->";
10
10
  const HYDRATION_TEXT_SEP = " ";
11
- const SUSPENSE_SCRIPT_ATTR = "data-octane-suspense";
11
+ import { SUSPENSE_SCRIPT_ATTR } from "./stream-protocol.js";
12
12
  const SUSPENSE_RESOLVED_COMMENT = "oct-suspense:";
13
13
  const SUSPENSE_RESOLVED_SEED_ATTR = "data-octane-suspense-seeds";
14
14
  const SUSPENSE_RESOLVED_NATIVE_ATTR = "data-octane-suspense-signals";
@@ -37,7 +37,7 @@ import {
37
37
  import { STREAM_BOUNDARY_ATTR } from "./stream-protocol.js";
38
38
  const STREAM_SEGMENT_ATTR = "data-oct-s";
39
39
  const STREAM_SEED_ATTR = "data-oct-seed";
40
- const STREAM_SCRIPT_ATTR = "data-octane-stream";
40
+ import { STREAM_SCRIPT_ATTR } from "./stream-protocol.js";
41
41
  const STREAM_RESOURCE_ATTR = "data-oct-fr";
42
42
  const STREAM_SEED_COMMENT = "oct-seed:";
43
43
  import {
@@ -1,5 +1,5 @@
1
- /** Renderer-independent capability carried by an adopted, compiler-proven view. */
2
- export declare const BINDING_HANDOFF: unique symbol;
1
+ import { BINDING_HANDOFF } from './signals/control-handoff.js';
2
+ export { BINDING_HANDOFF };
3
3
  /** Current early-owned range, not a license to render a different shape. */
4
4
  export interface BindingHandoffRange {
5
5
  readonly kind: 'if' | 'view' | 'slot' | 'text';
@@ -24,13 +24,15 @@ export interface BindingHandoff {
24
24
  readonly root: Node;
25
25
  readonly anchor: Node;
26
26
  readonly end?: Node;
27
- /** Allocated only for a claimed structural presentation. -1 means publication is in progress. */
27
+ /** A scalar host's declared native channels; descendants belong to other owners. */
28
+ readonly host?: ReadonlySet<string>;
29
+ /** Revisioned presentations return -1 while publication is in progress. */
28
30
  revision?(): number;
29
31
  ranges?(): ReadonlyMap<Node, BindingHandoffRange>;
30
32
  view?(root: Node): BindingHandoffView | undefined;
31
33
  rest?(element: Element, site: number): BindingHandoffRest | undefined;
32
34
  valid?(): boolean;
33
- /** One deferred retry; only structural leases allocate publication listeners. */
35
+ /** One deferred retry; listener storage is allocated only when requested. */
34
36
  afterPublication?(callback: () => void): () => void;
35
37
  active(): boolean;
36
38
  retire(publish?: () => void): void;
@@ -1,4 +1,4 @@
1
- const BINDING_HANDOFF = /* @__PURE__ */ Symbol.for("octane.binding-handoff");
1
+ import { BINDING_HANDOFF } from "./signals/control-handoff.js";
2
2
  const events = /* @__PURE__ */ new WeakMap();
3
3
  const delivered = /* @__PURE__ */ new WeakMap();
4
4
  function registerBindingEvent(node, type) {
@@ -15,7 +15,8 @@ export type BindingProgramNode = readonly [
15
15
  kind: 'element',
16
16
  tag: string,
17
17
  namespace: 0 | 1,
18
- children: number | null
18
+ children: number | null,
19
+ nativeControlContent?: 'value'
19
20
  ] | readonly [parent: number, kind: 'text', text: string] | readonly [parent: number, kind: 'region', site: string];
20
21
  export interface BindingRange {
21
22
  readonly start: Comment;
@@ -1250,15 +1250,30 @@ function bindProgram(root, descriptor, source, options, mount, list = descriptor
1250
1250
  dispose,
1251
1251
  () => !busy
1252
1252
  );
1253
- if (mount || descriptor.root.handoff !== true || !instance || descriptor.root.regions.length !== 0 || descriptor.root.bindings.some((binding) => binding[1] === "control") || descriptor.root.nodes.some((node) => node[1] === "element" && node[4] === null))
1253
+ if (mount || descriptor.root.handoff !== true || !instance || descriptor.root.regions.length !== 0 || descriptor.root.bindings.some((binding) => binding[1] === "control") || descriptor.root.nodes.some(
1254
+ (node, index) => node[1] === "element" && node[4] === null && !(node[2] === "textarea" && node[3] === 0 && node[5] === "value" && Array.from(instance.nodes[index].childNodes).every((child) => child.nodeType === 3))
1255
+ ))
1254
1256
  throw new Error(
1255
1257
  "Hydration binding leases require an adopted fixed native view without structural regions or controls."
1256
1258
  );
1259
+ const topology = handoff === void 0 && descriptor.root.nodes.some((node) => node[1] === "element" && node[5] === "value") ? [instance.range.start, ...instance.nodes, instance.range.end].map(
1260
+ (node) => [node, node.parentNode, node.previousSibling, node.nextSibling]
1261
+ ) : void 0;
1257
1262
  return handoff ??= {
1258
1263
  id: descriptor.id,
1259
1264
  root: instance.range.start,
1260
1265
  anchor: instance.nodes.find((node) => node.nodeType === 1) ?? instance.range.start,
1261
1266
  active: () => !transaction.disposed,
1267
+ ...topology === void 0 ? {} : {
1268
+ end: instance.range.end,
1269
+ valid: () => topology.every(
1270
+ ([node, parent, before, after]) => node.parentNode === parent && (node === instance.range.start || node.previousSibling === before) && (node === instance.range.end || node.nextSibling === after)
1271
+ ) && descriptor.root.nodes.every(
1272
+ (proof, index) => proof[1] !== "element" || (proof[4] === null ? Array.from(instance.nodes[index].childNodes).every(
1273
+ (child) => child.nodeType === 3
1274
+ ) : instance.nodes[index].childNodes.length === proof[4])
1275
+ )
1276
+ },
1262
1277
  retire: (publish) => {
1263
1278
  transaction.preservePresentation = true;
1264
1279
  dispose(void 0, publish);
@@ -59,6 +59,8 @@ export type BindingValue = string | null | BindingStyleSnapshot;
59
59
  /** @internal No application function is invoked to discover the DOM topology. */
60
60
  export interface CompiledBindings<Props> {
61
61
  readonly id: string;
62
+ /** Compiler-proven single native host, with no descendant ownership. */
63
+ readonly handoff?: 'host';
62
64
  /** Targets carry compiler-issued addresses when unbound siblings may change. */
63
65
  readonly addressed?: true;
64
66
  readonly nodes: readonly BindingNode[];
@@ -1,5 +1,8 @@
1
1
  import { normalizeClass } from "./class-names.js";
2
2
  import { sanitizeURL } from "./sanitize-url.js";
3
+ import { STREAM_SCRIPT_ATTR, SUSPENSE_SCRIPT_ATTR } from "./stream-protocol.js";
4
+ import { NATIVE_SIGNAL_SEED_ATTR } from "./signals/native-read-seeds.js";
5
+ import { BINDING_HANDOFF } from "./signals/control-handoff.js";
3
6
  import { captureSignalOwner, currentSignalOwner } from "./signals/owner-context.js";
4
7
  import {
5
8
  NATIVE_TRANSITION_CONSUMER,
@@ -254,16 +257,28 @@ function __adoptBindings(root, descriptor, source, options) {
254
257
  let dirty = false;
255
258
  let revision = 0;
256
259
  let unsubscribe;
260
+ let publicationRetries;
261
+ let published;
257
262
  const signal = options?.signal;
258
- const dispose = () => {
259
- if (disposed) return;
263
+ const dispose = (_disposal, publish, preservePresentation = false) => {
264
+ if (disposed) {
265
+ publish?.();
266
+ return;
267
+ }
260
268
  disposed = true;
269
+ publicationRetries?.clear();
261
270
  signal?.removeEventListener("abort", dispose);
262
271
  let failed = false;
263
272
  let failure;
273
+ try {
274
+ publish?.();
275
+ } catch (error) {
276
+ failed = true;
277
+ failure = error;
278
+ }
264
279
  for (const connection of signalConnections?.values() ?? []) {
265
280
  try {
266
- connection.dispose();
281
+ connection.dispose(preservePresentation);
267
282
  } catch (error) {
268
283
  if (!failed) {
269
284
  failed = true;
@@ -273,7 +288,7 @@ function __adoptBindings(root, descriptor, source, options) {
273
288
  }
274
289
  for (const connection of projections ? new Set(projections.values()) : []) {
275
290
  try {
276
- connection.dispose();
291
+ connection.dispose(preservePresentation);
277
292
  } catch (error) {
278
293
  if (!failed) {
279
294
  failed = true;
@@ -298,9 +313,9 @@ function __adoptBindings(root, descriptor, source, options) {
298
313
  for (let i = 0; i < owned.length; i++) {
299
314
  const [node, name] = owned[i];
300
315
  try {
301
- groups?.get(i)?.dispose();
302
- styles?.get(i)?.dispose();
303
- if (bindings[i][1] === "classToken" && previous[i] === "")
316
+ groups?.get(i)?.dispose(preservePresentation);
317
+ styles?.get(i)?.dispose(preservePresentation);
318
+ if (!preservePresentation && bindings[i][1] === "classToken" && previous[i] === "")
304
319
  node.classList.remove(bindings[i][2]);
305
320
  } catch (error) {
306
321
  if (!failed) {
@@ -571,6 +586,7 @@ function __adoptBindings(root, descriptor, source, options) {
571
586
  writePrepared(next, indices, preparedControls, preparedGroups);
572
587
  } finally {
573
588
  busy = false;
589
+ published?.();
574
590
  }
575
591
  }
576
592
  };
@@ -593,6 +609,7 @@ function __adoptBindings(root, descriptor, source, options) {
593
609
  for (const index of previousUpdates ?? []) signalUpdates.add(index);
594
610
  }
595
611
  busy = false;
612
+ if (!preview) published?.();
596
613
  }
597
614
  };
598
615
  const refresh = () => {
@@ -608,6 +625,52 @@ function __adoptBindings(root, descriptor, source, options) {
608
625
  prepare: () => run(() => drain(true))
609
626
  };
610
627
  const handle = { refresh, dispose };
628
+ if (descriptor.handoff === "host") {
629
+ published = () => {
630
+ if (!publicationRetries?.size) return;
631
+ const pending = [...publicationRetries];
632
+ publicationRetries.clear();
633
+ for (const retry of pending)
634
+ queueMicrotask(() => {
635
+ if (!disposed) retry();
636
+ });
637
+ };
638
+ const host = root;
639
+ const document = host.ownerDocument;
640
+ const ancestry = [];
641
+ for (let node = host; node !== null; node = node.parentNode)
642
+ ancestry.push([node, node.parentNode]);
643
+ const sibling = (node, previous2) => {
644
+ let next = previous2 ? node.previousSibling : node.nextSibling;
645
+ while (next?.nodeType === 1 && next.localName === "script" && (next.hasAttribute(NATIVE_SIGNAL_SEED_ATTR) || next.hasAttribute(SUSPENSE_SCRIPT_ATTR) || next.hasAttribute(STREAM_SCRIPT_ATTR)))
646
+ next = previous2 ? next.previousSibling : next.nextSibling;
647
+ return next;
648
+ };
649
+ const previousSibling = sibling(host, true);
650
+ const nextSibling = sibling(host, false);
651
+ let handoff;
652
+ handle[BINDING_HANDOFF] = () => {
653
+ if (handoff !== void 0) return handoff;
654
+ return handoff = {
655
+ id: descriptor.id,
656
+ root: host,
657
+ anchor: host,
658
+ host: new Set(
659
+ bindings.map(
660
+ (binding) => binding[1] === "styleProperty" ? "style:" + binding[2] : binding[1].startsWith("style") ? "style" : binding[2]
661
+ )
662
+ ),
663
+ revision: () => busy ? -1 : revision,
664
+ afterPublication: (callback) => {
665
+ (publicationRetries ??= /* @__PURE__ */ new Set()).add(callback);
666
+ return () => publicationRetries.delete(callback);
667
+ },
668
+ 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,
669
+ active: () => !disposed,
670
+ retire: (publish) => dispose(void 0, publish, true)
671
+ };
672
+ };
673
+ }
611
674
  if (signal?.aborted) {
612
675
  dispose();
613
676
  return handle;