octane 0.1.49 → 0.1.50

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
@@ -43,6 +43,43 @@ taking ownership of the existing markup. See the
43
43
  For the full story, see the
44
44
  [main README](https://github.com/octanejs/octane#readme).
45
45
 
46
+ ## React interoperability
47
+
48
+ `octane/react` exports both hosting directions: `ReactCompat` runs real React
49
+ components inside Octane, and `OctaneCompat` runs compiled Octane components
50
+ inside React 19. `ReactCompat` needs matching React and React DOM versions,
51
+ 19.2 or newer in the React 19 series.
52
+
53
+ ```tsrx
54
+ // App.tsrx — compiled by Octane.
55
+ import { ReactCompat } from 'octane/react';
56
+ import { Counter } from './Counter.react';
57
+
58
+ export function App() @{
59
+ <ReactCompat><Counter start={3} /></ReactCompat>
60
+ }
61
+ ```
62
+
63
+ `Counter.react.tsx` stays an ordinary React module, using hooks from `react` and
64
+ React's JSX transform with `/** @jsxImportSource react */`. Native `.tsrx`
65
+ components stay with Octane. In a mixed build, use `requireDirective: true` with
66
+ both compilers and mark Octane-owned `.tsx` and hook helper modules with
67
+ `/** @jsxImportSource octane */`. Do not alias React to Octane.
68
+
69
+ React retains its own state, events, refs, and component types, including class,
70
+ `memo`, `lazy`, and `forwardRef` components. Use
71
+ `bridgeReactContext(OctaneContext, ReactContext)` and `ReactCompat`'s `contexts`
72
+ prop to map native context into React. Within `OctaneCompat`, Octane's `use` or
73
+ `useContext` can read a real React context directly.
74
+
75
+ Both server implementations are exported from `octane/react/server`. Octane's
76
+ server compiler selects that entry automatically; React-owned server entries
77
+ that bypass it must select it explicitly. Each renderer commits its own work:
78
+ Octane transitions and `flushSync()` do not synchronously commit a
79
+ React root. The [React interoperability guide](https://octanejs.dev/docs/react-compat)
80
+ and [full ReactCompat reference](https://github.com/octanejs/octane/blob/main/docs/react-compat.md)
81
+ cover setup, pending updates, SSR buffering, hydration, and nesting limits.
82
+
46
83
  ## Browser compatibility
47
84
 
48
85
  See the [browser support guide](https://octanejs.dev/docs/browser-support) for
@@ -106,6 +106,7 @@ __export(runtime_exports, {
106
106
  finishNativeReadWitness: () => finishNativeReadWitness,
107
107
  flushSync: () => flushSync,
108
108
  forBlock: () => forBlock,
109
+ getRendererOwnerVisibility: () => getRendererOwnerVisibility,
109
110
  getRootRenderRetryKey: () => getRootRenderRetryKey,
110
111
  getTransitionFallbackTimeout: () => getTransitionFallbackTimeout,
111
112
  hasPendingWork: () => hasPendingWork,
@@ -186,6 +187,7 @@ __export(runtime_exports, {
186
187
  renderClientContextProvider: () => renderClientContextProvider,
187
188
  replaceRef: () => replaceRef,
188
189
  replayNativeReadWitness: () => replayNativeReadWitness,
190
+ reportRendererOwnerError: () => reportRendererOwnerError,
189
191
  requestFormReset: () => requestFormReset,
190
192
  resetFloatResourceState: () => resetFloatResourceState,
191
193
  scheduleRenderCleanup: () => scheduleRenderCleanup,
@@ -660,6 +662,7 @@ function runEffectCleanupCallback(callback) {
660
662
  }
661
663
  }
662
664
  const QUEUE = [];
665
+ let QUEUE_REINDEX_EPOCH = 0;
663
666
  let scheduled = false;
664
667
  let syncFlush = false;
665
668
  let inFlush = false;
@@ -2126,27 +2129,41 @@ function belongsToBlockTree(block, root) {
2126
2129
  }
2127
2130
  function drainHydrationRenderPhaseUpdates(root) {
2128
2131
  let renders = null;
2129
- for (; ; ) {
2130
- let index = -1;
2131
- for (let i = 0; i < QUEUE.length; i++) {
2132
- if (belongsToBlockTree(QUEUE[i], root)) {
2133
- index = i;
2134
- break;
2132
+ let read = 0;
2133
+ let write = 0;
2134
+ let reindexEpoch = QUEUE_REINDEX_EPOCH;
2135
+ try {
2136
+ while (read < QUEUE.length) {
2137
+ const block = QUEUE[read++];
2138
+ if (!belongsToBlockTree(block, root)) {
2139
+ QUEUE[write++] = block;
2140
+ continue;
2141
+ }
2142
+ if (!block.pending || block.disposed) continue;
2143
+ const seen = (renders ??= /* @__PURE__ */ new Map()).get(block) ?? 0;
2144
+ if (seen >= RENDER_PHASE_UPDATE_LIMIT) {
2145
+ throw new Error((0, import_error_codes_client_generated.formatClientError)(9));
2146
+ }
2147
+ renders.set(block, seen + 1);
2148
+ block.crossRenderUpdate = false;
2149
+ try {
2150
+ renderBlock(block);
2151
+ } catch (error) {
2152
+ handleRenderError(block, error);
2153
+ }
2154
+ if (reindexEpoch !== QUEUE_REINDEX_EPOCH) {
2155
+ reindexEpoch = QUEUE_REINDEX_EPOCH;
2156
+ read = 0;
2157
+ write = 0;
2135
2158
  }
2136
2159
  }
2137
- if (index === -1) return;
2138
- const block = QUEUE.splice(index, 1)[0];
2139
- if (!block.pending || block.disposed) continue;
2140
- const seen = (renders ??= /* @__PURE__ */ new Map()).get(block) ?? 0;
2141
- if (seen >= RENDER_PHASE_UPDATE_LIMIT) {
2142
- throw new Error((0, import_error_codes_client_generated.formatClientError)(9));
2143
- }
2144
- renders.set(block, seen + 1);
2145
- block.crossRenderUpdate = false;
2146
- try {
2147
- renderBlock(block);
2148
- } catch (error) {
2149
- handleRenderError(block, error);
2160
+ } finally {
2161
+ if (reindexEpoch === QUEUE_REINDEX_EPOCH) {
2162
+ while (read < QUEUE.length) {
2163
+ QUEUE[write++] = QUEUE[read++];
2164
+ }
2165
+ QUEUE.length = write;
2166
+ QUEUE_REINDEX_EPOCH++;
2150
2167
  }
2151
2168
  }
2152
2169
  }
@@ -2282,6 +2299,7 @@ function drainQueue() {
2282
2299
  }
2283
2300
  }
2284
2301
  QUEUE.length = 0;
2302
+ QUEUE_REINDEX_EPOCH++;
2285
2303
  if (activitiesToRehide !== null) {
2286
2304
  for (const activity of activitiesToRehide) SCHEDULED_VISIBILITY_DRIVER.rehide(activity);
2287
2305
  }
@@ -6314,6 +6332,21 @@ function readContextFromScope(scope, context) {
6314
6332
  recordContextDependency(scope.block, context);
6315
6333
  return readContextFrom(scope, scope.block, context);
6316
6334
  }
6335
+ function getRendererOwnerVisibility(scope) {
6336
+ if (findHiddenActivity(scope.block) !== null) return "activity";
6337
+ return findSuspenseHiddenTry(scope.block) !== null ? "suspense" : "visible";
6338
+ }
6339
+ function reportRendererOwnerError(scope, error) {
6340
+ const origin = scope.block;
6341
+ let owner = origin;
6342
+ while (owner !== null && blockSubtreeDisposed(owner)) owner = owner.parentBlock;
6343
+ const handler = findTryHandler(owner);
6344
+ if (handler !== null) reportCaughtError(owner, error, handler(error));
6345
+ else if (!reportUncaughtError(origin, error)) {
6346
+ if (typeof reportError === "function") reportError(error);
6347
+ else console.error(error);
6348
+ }
6349
+ }
6317
6350
  function useContextInternal(context) {
6318
6351
  recordContextDependency(CURRENT_BLOCK, context);
6319
6352
  return readContextFrom(CURRENT_SCOPE, CURRENT_BLOCK, context);
@@ -7292,6 +7325,8 @@ class HydrationCapability {
7292
7325
  abandoned = false;
7293
7326
  freshNodes = /* @__PURE__ */ new WeakSet();
7294
7327
  unframedRootRanges = /* @__PURE__ */ new WeakMap();
7328
+ /** Pairs discovered while matching an outer range; released with this hydration pass. */
7329
+ matchingCloses = null;
7295
7330
  /** First unclaimed root sibling after a compiled root clone; undefined until known. */
7296
7331
  rootRemainder;
7297
7332
  rootCleanupBoundary = null;
@@ -7328,7 +7363,8 @@ class HydrationCapability {
7328
7363
  return isBlockClose(node);
7329
7364
  }
7330
7365
  close(open) {
7331
- const found = findMatchingClose(open);
7366
+ const known = this.matchingCloses?.get(open);
7367
+ const found = known ?? findMatchingClose(open, this.matchingCloses ??= /* @__PURE__ */ new WeakMap());
7332
7368
  if (!this.hasAdjacentRangePair && isBlockOpen(open.previousSibling) && isBlockClose(found.nextSibling)) {
7333
7369
  this.hasAdjacentRangePair = true;
7334
7370
  }
@@ -7981,8 +8017,8 @@ function isBlockClose(node) {
7981
8017
  function isTextSeparator(node) {
7982
8018
  return node !== null && node.nodeType === 8 && node.data === import_constants.HYDRATION_TEXT_SEP;
7983
8019
  }
7984
- function findMatchingClose(open) {
7985
- let depth = 0;
8020
+ function findMatchingClose(open, matches) {
8021
+ let nested = null;
7986
8022
  let node = getNextSibling(open);
7987
8023
  for (; ; ) {
7988
8024
  if (node.nodeType === 8) {
@@ -7998,12 +8034,14 @@ function findMatchingClose(open) {
7998
8034
  }
7999
8035
  }
8000
8036
  if (close) {
8001
- if (depth === 0) {
8002
- return node;
8037
+ const found = node;
8038
+ if (nested === null || nested.length === 0) {
8039
+ matches.set(open, found);
8040
+ return found;
8003
8041
  }
8004
- depth -= 1;
8042
+ matches.set(nested.pop(), found);
8005
8043
  } else if (nestedOpen) {
8006
- depth += 1;
8044
+ (nested ??= []).push(node);
8007
8045
  }
8008
8046
  }
8009
8047
  node = getNextSibling(node);
@@ -9616,7 +9654,7 @@ function namespaceHeadElement(headKey, tag, attrs, text, authoredKey) {
9616
9654
  if (key !== void 0) config.key = key;
9617
9655
  return createElement(namespaceHead, config);
9618
9656
  }
9619
- function injectStyle(id, css) {
9657
+ function injectStyle(id, css, nonce) {
9620
9658
  if (_injectedStyles.has(id)) return;
9621
9659
  if (typeof document !== "undefined" && document.querySelector(`style[data-octane="${id}"], style[data-href="octane-${id}"]`)) {
9622
9660
  _injectedStyles.add(id);
@@ -9625,6 +9663,7 @@ function injectStyle(id, css) {
9625
9663
  _injectedStyles.add(id);
9626
9664
  const el = document.createElement("style");
9627
9665
  el.setAttribute("data-octane", id);
9666
+ if (nonce !== void 0) el.nonce = nonce;
9628
9667
  el.textContent = css;
9629
9668
  document.head.appendChild(el);
9630
9669
  }
@@ -19102,8 +19141,21 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
19102
19141
  const blockGroups = /* @__PURE__ */ new WeakMap();
19103
19142
  const scopeGroups = /* @__PURE__ */ new WeakMap();
19104
19143
  const ownerGroups = /* @__PURE__ */ new WeakMap();
19144
+ const groups = [];
19105
19145
  const seenBlocks = /* @__PURE__ */ new WeakSet();
19106
19146
  const seenScopes = /* @__PURE__ */ new WeakSet();
19147
+ let redundantMarkers = null;
19148
+ let removalRange = null;
19149
+ function canonicalGroup(group) {
19150
+ let canonical = group;
19151
+ while (canonical.parent !== null) canonical = canonical.parent;
19152
+ while (group.parent !== null && group.parent !== canonical) {
19153
+ const parent = group.parent;
19154
+ group.parent = canonical;
19155
+ group = parent;
19156
+ }
19157
+ return canonical;
19158
+ }
19107
19159
  function makeGroup(startNode, endNode, block, liteScope, owner) {
19108
19160
  if (!isBlockOpen(startNode) || !isBlockClose(endNode) || startNode === endNode) return null;
19109
19161
  if (startNode.parentNode === null || startNode.parentNode !== endNode.parentNode) return null;
@@ -19114,43 +19166,51 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
19114
19166
  start: startNode,
19115
19167
  end: endNode,
19116
19168
  depth: openDepth,
19169
+ parent: null,
19117
19170
  blocks: block === void 0 ? [] : [block],
19118
19171
  liteScopes: liteScope === void 0 ? [] : [liteScope],
19119
19172
  owners: owner === void 0 ? [] : [owner]
19120
19173
  };
19174
+ groups.push(group);
19121
19175
  if (block !== void 0) blockGroups.set(block, group);
19122
19176
  if (liteScope !== void 0) scopeGroups.set(liteScope, group);
19123
19177
  if (owner !== void 0) ownerGroups.set(owner, group);
19124
19178
  return group;
19125
19179
  }
19126
- function appendUnique(target, source) {
19127
- for (let i = 0; i < source.length; i++) {
19128
- if (target.indexOf(source[i]) === -1) target.push(source[i]);
19129
- }
19130
- }
19131
- function remapGroup(from, to) {
19132
- for (let i = 0; i < from.blocks.length; i++) blockGroups.set(from.blocks[i], to);
19133
- for (let i = 0; i < from.liteScopes.length; i++) scopeGroups.set(from.liteScopes[i], to);
19134
- for (let i = 0; i < from.owners.length; i++) ownerGroups.set(from.owners[i], to);
19135
- }
19136
19180
  function writeMultiplicity(group) {
19137
19181
  group.start.data = group.depth === 1 ? import_constants.HYDRATION_START : import_constants.HYDRATION_START + String(group.depth);
19138
19182
  group.end.data = group.depth === 1 ? import_constants.HYDRATION_END : import_constants.HYDRATION_END + String(group.depth);
19139
19183
  }
19184
+ function removeRedundantMarkerRun(anchor, after) {
19185
+ if (redundantMarkers === null) return;
19186
+ const adjacent = after ? anchor.nextSibling : anchor.previousSibling;
19187
+ if (adjacent === null || adjacent.nodeType !== 8 || !redundantMarkers.has(adjacent)) {
19188
+ return;
19189
+ }
19190
+ let edge = adjacent;
19191
+ for (; ; ) {
19192
+ const next = after ? edge.nextSibling : edge.previousSibling;
19193
+ if (next === null || next.nodeType !== 8 || !redundantMarkers.has(next)) break;
19194
+ edge = next;
19195
+ }
19196
+ const range = removalRange ??= anchor.ownerDocument.createRange();
19197
+ range.setStartBefore(after ? adjacent : edge);
19198
+ range.setEndAfter(after ? edge : adjacent);
19199
+ range.deleteContents();
19200
+ }
19140
19201
  function unifySharedPair(outer, inner) {
19202
+ outer = canonicalGroup(outer);
19203
+ inner = canonicalGroup(inner);
19141
19204
  if (outer === inner) return outer;
19142
19205
  outer.depth = Math.max(outer.depth, inner.depth);
19143
- appendUnique(outer.blocks, inner.blocks);
19144
- appendUnique(outer.liteScopes, inner.liteScopes);
19145
- appendUnique(outer.owners, inner.owners);
19146
- remapGroup(inner, outer);
19206
+ inner.parent = outer;
19147
19207
  writeMultiplicity(outer);
19148
19208
  return outer;
19149
19209
  }
19150
19210
  function rangesAreExactlyNested(outer, inner) {
19151
19211
  return outer.start.parentNode !== null && outer.start.parentNode === inner.start.parentNode && outer.end.parentNode === outer.start.parentNode && inner.end.parentNode === outer.start.parentNode && outer.start.nextSibling === inner.start && inner.end.nextSibling === outer.end;
19152
19212
  }
19153
- function borrowInnerRange(outer, inner) {
19213
+ function borrowGroupMembers(outer, inner) {
19154
19214
  for (let i = 0; i < inner.blocks.length; i++) {
19155
19215
  const block = inner.blocks[i];
19156
19216
  block.startMarker = outer.start;
@@ -19178,6 +19238,8 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
19178
19238
  }
19179
19239
  }
19180
19240
  function mergeExactRanges(outer, inner) {
19241
+ outer = canonicalGroup(outer);
19242
+ inner = canonicalGroup(inner);
19181
19243
  if (outer === inner) return outer;
19182
19244
  if (outer.start === inner.start && outer.end === inner.end) {
19183
19245
  return unifySharedPair(outer, inner);
@@ -19185,19 +19247,15 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
19185
19247
  if (!rangesAreExactlyNested(outer, inner)) return outer;
19186
19248
  const mergedDepth = outer.depth + inner.depth;
19187
19249
  if (!Number.isSafeInteger(mergedDepth)) return outer;
19188
- borrowInnerRange(outer, inner);
19189
- inner.start.remove();
19190
- inner.end.remove();
19250
+ (redundantMarkers ??= /* @__PURE__ */ new Set()).add(inner.start).add(inner.end);
19191
19251
  outer.depth = mergedDepth;
19192
- appendUnique(outer.blocks, inner.blocks);
19193
- appendUnique(outer.liteScopes, inner.liteScopes);
19194
- appendUnique(outer.owners, inner.owners);
19195
- remapGroup(inner, outer);
19252
+ inner.parent = outer;
19196
19253
  writeMultiplicity(outer);
19197
19254
  return outer;
19198
19255
  }
19199
19256
  function attachOwner(group, owner) {
19200
19257
  if (group === null) return;
19258
+ group = canonicalGroup(group);
19201
19259
  if (group.owners.indexOf(owner) === -1) group.owners.push(owner);
19202
19260
  ownerGroups.set(owner, group);
19203
19261
  }
@@ -19223,7 +19281,8 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
19223
19281
  }
19224
19282
  function mappedGroup(value) {
19225
19283
  if (value === null || typeof value !== "object") return void 0;
19226
- return ownerGroups.get(value) ?? scopeGroups.get(value);
19284
+ const group = ownerGroups.get(value) ?? scopeGroups.get(value);
19285
+ return group === void 0 ? void 0 : canonicalGroup(group);
19227
19286
  }
19228
19287
  function soleRangeCandidate(scope) {
19229
19288
  let only = void 0;
@@ -19241,7 +19300,8 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
19241
19300
  }
19242
19301
  const registered = scope._slots;
19243
19302
  if (registered !== null && registered.length === 1 && scope.children === null && registered[0].__kind === "childSlot" && registered[0].compactable && mayBorrowCandidate(registered[0])) {
19244
- return ownerGroups.get(registered[0]) ?? null;
19303
+ const group = ownerGroups.get(registered[0]);
19304
+ return group === void 0 ? null : canonicalGroup(group);
19245
19305
  }
19246
19306
  return null;
19247
19307
  }
@@ -19324,6 +19384,21 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
19324
19384
  for (let i = 0; i < registered.length; i++) visitSlot(registered[i]);
19325
19385
  }
19326
19386
  visitBlock(rootBlock);
19387
+ for (let i = 0; i < groups.length; i++) {
19388
+ const group = groups[i];
19389
+ const canonical = canonicalGroup(group);
19390
+ if (group.start !== canonical.start || group.end !== canonical.end) {
19391
+ borrowGroupMembers(canonical, group);
19392
+ }
19393
+ }
19394
+ if (redundantMarkers !== null) {
19395
+ for (let i = 0; i < groups.length; i++) {
19396
+ const group = groups[i];
19397
+ if (group.parent !== null) continue;
19398
+ removeRedundantMarkerRun(group.start, true);
19399
+ removeRedundantMarkerRun(group.end, false);
19400
+ }
19401
+ }
19327
19402
  }
19328
19403
  let ROOT_ERROR_HANDLERS = null;
19329
19404
  function registerRootErrorHandlers(root, options) {
@@ -20507,6 +20582,7 @@ function scriptResource(attrs) {
20507
20582
  finishNativeReadWitness,
20508
20583
  flushSync,
20509
20584
  forBlock,
20585
+ getRendererOwnerVisibility,
20510
20586
  getRootRenderRetryKey,
20511
20587
  getTransitionFallbackTimeout,
20512
20588
  hasPendingWork,
@@ -20587,6 +20663,7 @@ function scriptResource(attrs) {
20587
20663
  renderClientContextProvider,
20588
20664
  replaceRef,
20589
20665
  replayNativeReadWitness,
20666
+ reportRendererOwnerError,
20590
20667
  requestFormReset,
20591
20668
  resetFloatResourceState,
20592
20669
  scheduleRenderCleanup,