octane 0.1.25 → 0.1.26

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/dist/runtime.js CHANGED
@@ -100,6 +100,14 @@ function profilePortalComponent(rawBody) {
100
100
  function ensureHooks(scope) {
101
101
  return scope.hooks ?? (scope.hooks = /* @__PURE__ */ new Map());
102
102
  }
103
+ function registerHookCleanup(scope, cleanup) {
104
+ if (process.env.NODE_ENV !== "production") {
105
+ if (scope.block.body[HMR] !== void 0) {
106
+ cleanup[HMR] = true;
107
+ }
108
+ }
109
+ (scope.cleanups ??= []).push(cleanup);
110
+ }
103
111
  let nextHookSlot = 0;
104
112
  function hookSlots(count) {
105
113
  const base = nextHookSlot;
@@ -2446,7 +2454,7 @@ function unmountScope(scope, detachDom = true) {
2446
2454
  unmountScopeChildrenAndSlots(scope, detachDom);
2447
2455
  });
2448
2456
  }
2449
- function unmountScopeChildrenAndSlots(scope, detachDom) {
2457
+ function runScopeCleanups(scope) {
2450
2458
  const c = scope.cleanups;
2451
2459
  if (c !== null)
2452
2460
  for (let i = c.length - 1; i >= 0; i--) {
@@ -2456,6 +2464,12 @@ function unmountScopeChildrenAndSlots(scope, detachDom) {
2456
2464
  reportTeardownError(err);
2457
2465
  }
2458
2466
  }
2467
+ }
2468
+ function unmountScopeChildrenAndSlots(scope, detachDom) {
2469
+ runScopeCleanups(scope);
2470
+ unmountScopeChildrenAndSlotsOnly(scope, detachDom);
2471
+ }
2472
+ function unmountScopeChildrenAndSlotsOnly(scope, detachDom) {
2459
2473
  const children = scope.children;
2460
2474
  if (children !== null)
2461
2475
  for (let i = 0, n = children.length; i < n; i++) unmountScope(children[i].scope, detachDom);
@@ -3123,7 +3137,7 @@ function useEffectEvent(fn, slot) {
3123
3137
  s = { impl: fn, active: true };
3124
3138
  ensureHooks(scope).set(slot, s);
3125
3139
  const cell2 = s;
3126
- (scope.cleanups ??= []).push(() => {
3140
+ registerHookCleanup(scope, () => {
3127
3141
  cell2.active = false;
3128
3142
  });
3129
3143
  } else {
@@ -3205,6 +3219,87 @@ function scopedChildrenAsBody(props) {
3205
3219
  return (_props, scope, extra) => childrenAsBody(props.children)(void 0, scope, extra);
3206
3220
  }
3207
3221
  const CHILDREN_DIALECT_SLOT = /* @__PURE__ */ Symbol("octane.childrenDialect");
3222
+ function hasResettableHmrRange(block) {
3223
+ const start = block.startMarker;
3224
+ const end = block.endMarker;
3225
+ if (start === void 0 || end === void 0) return false;
3226
+ if (start === null || end === null) {
3227
+ return start === null && end === null && (block.kind === "root" || block.exclusiveMarkers === true);
3228
+ }
3229
+ if (start !== end) {
3230
+ return start.parentNode === block.parentNode && end.parentNode === block.parentNode;
3231
+ }
3232
+ for (let parent = block.parentBlock; parent !== null; parent = parent.parentBlock) {
3233
+ if (parent.startMarker === start && parent.endMarker === end) return false;
3234
+ }
3235
+ return start.parentNode === block.parentNode;
3236
+ }
3237
+ function promoteHmrBlockRange(block) {
3238
+ const root = block.startMarker;
3239
+ if (root === null || root !== block.endMarker) return;
3240
+ const parent = block.parentNode;
3241
+ const rangeStart = document.createComment("hmr");
3242
+ const rangeEnd = document.createComment("/hmr");
3243
+ parent.insertBefore(rangeStart, root);
3244
+ parent.insertBefore(rangeEnd, root.nextSibling);
3245
+ block.startMarker = rangeStart;
3246
+ block.endMarker = rangeEnd;
3247
+ block.exclusiveMarkers = false;
3248
+ }
3249
+ function resetHmrBlock(block) {
3250
+ if (TEARDOWN_DEPTH === 0) {
3251
+ TEARDOWN_HANDLER = findTryHandler(block.parentBlock) ?? rendererRegionTryHandler(block);
3252
+ }
3253
+ TEARDOWN_DEPTH++;
3254
+ let abortedRefs = null;
3255
+ if (!block.mounted) {
3256
+ abortedRefs = [];
3257
+ collectVisibleSubtreeRefs(block, abortedRefs);
3258
+ }
3259
+ try {
3260
+ withRefDetachSuppression(abortedRefs, () => {
3261
+ if (block.deoptNode !== null) detachDeoptTreeRefs(block.deoptNode, null);
3262
+ const cleanups = block.cleanups;
3263
+ let preserved = null;
3264
+ if (cleanups !== null) {
3265
+ for (let i = cleanups.length - 1; i >= 0; i--) {
3266
+ const cleanup = cleanups[i];
3267
+ if (cleanup[HMR] === true) continue;
3268
+ try {
3269
+ runEffectLifecycleCallback(cleanup);
3270
+ } catch (err) {
3271
+ reportTeardownError(err);
3272
+ }
3273
+ }
3274
+ for (let i = 0; i < cleanups.length; i++) {
3275
+ const cleanup = cleanups[i];
3276
+ if (cleanup[HMR] === true) (preserved ??= []).push(cleanup);
3277
+ }
3278
+ }
3279
+ unmountScopeChildrenAndSlotsOnly(block, true);
3280
+ removeRange(
3281
+ block.startMarker !== null ? block.startMarker.nextSibling : block.parentNode.firstChild,
3282
+ block.endMarker
3283
+ );
3284
+ block.children = null;
3285
+ block.cleanups = preserved;
3286
+ block._slots = null;
3287
+ block.refFields = null;
3288
+ block.slots = [];
3289
+ block.deoptNode = null;
3290
+ });
3291
+ const hooks = block.hooks;
3292
+ if (hooks !== null) {
3293
+ for (const value of hooks.values()) {
3294
+ if (value !== null && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "deps")) {
3295
+ value.deps = void 0;
3296
+ }
3297
+ }
3298
+ }
3299
+ } finally {
3300
+ if (--TEARDOWN_DEPTH === 0) dispatchTeardownErrors();
3301
+ }
3302
+ }
3208
3303
  function resetScopeChildren(scope) {
3209
3304
  const block = scope.block;
3210
3305
  if (TEARDOWN_DEPTH === 0) {
@@ -10559,6 +10654,13 @@ function hmr(fn) {
10559
10654
  const nextFn = incomingMeta ? incomingMeta.fn : incoming;
10560
10655
  if (meta.fn.__octaneReturnedOutput !== nextFn.__octaneReturnedOutput)
10561
10656
  return false;
10657
+ for (const b of meta.liveBlocks) {
10658
+ if (b.disposed) {
10659
+ meta.liveBlocks.delete(b);
10660
+ continue;
10661
+ }
10662
+ if (!hasResettableHmrRange(b)) return false;
10663
+ }
10562
10664
  meta.fn = nextFn;
10563
10665
  if (typeof __OCTANE_PROFILE_ENABLED__ !== "undefined" && __OCTANE_PROFILE_ENABLED__)
10564
10666
  __profileComponentSource(wrapper, meta.fn);
@@ -10573,6 +10675,8 @@ function hmr(fn) {
10573
10675
  b.body = wrapper;
10574
10676
  if (typeof __OCTANE_PROFILE_ENABLED__ !== "undefined" && __OCTANE_PROFILE_ENABLED__)
10575
10677
  __profileSchedule(b, "hmr");
10678
+ promoteHmrBlockRange(b);
10679
+ resetHmrBlock(b);
10576
10680
  scheduleRender(b);
10577
10681
  }
10578
10682
  return true;
@@ -11684,7 +11788,7 @@ function useTransition(slot) {
11684
11788
  }
11685
11789
  };
11686
11790
  TRANSITION_LISTENERS.add(listener);
11687
- (scope.cleanups ??= []).push(() => TRANSITION_LISTENERS.delete(listener));
11791
+ registerHookCleanup(scope, () => TRANSITION_LISTENERS.delete(listener));
11688
11792
  }
11689
11793
  return [s.isPending, s.start];
11690
11794
  }
@@ -11786,7 +11890,7 @@ function useFormStatus(slot) {
11786
11890
  s = { form: null, listener: null };
11787
11891
  const slotRef = s;
11788
11892
  ensureHooks(scope).set(slot, slotRef);
11789
- (scope.cleanups ??= []).push(() => {
11893
+ registerHookCleanup(scope, () => {
11790
11894
  if (slotRef.form && slotRef.listener)
11791
11895
  FORM_STATUS_LISTENERS.get(slotRef.form)?.delete(slotRef.listener);
11792
11896
  });
@@ -11861,7 +11965,7 @@ function useOptimistic(passthrough, updateFnOrSlot, slot) {
11861
11965
  if (TRANSITION_PENDING_COUNT === 0 && slotRef.armed) clear();
11862
11966
  };
11863
11967
  TRANSITION_LISTENERS.add(listener);
11864
- (scope.cleanups ??= []).push(() => TRANSITION_LISTENERS.delete(listener));
11968
+ registerHookCleanup(scope, () => TRANSITION_LISTENERS.delete(listener));
11865
11969
  }
11866
11970
  s.updateFn = updateFn;
11867
11971
  let optimistic = passthrough;
@@ -611,6 +611,7 @@ export interface ObjectHostInstance {
611
611
  }
612
612
  interface ObjectDriverState {
613
613
  instances: Map<number, ObjectHostInstance>;
614
+ parents: Map<number, number | null>;
614
615
  events: Map<number, Map<string, UniversalEventListenerDescriptor>>;
615
616
  lifecycles: Map<number, Map<string, UniversalListenerDescriptor>>;
616
617
  localCallbacks: Map<number, Map<string, UniversalListenerDescriptor>>;
@@ -616,6 +616,7 @@ function createOwnerRecord(root, component, parent, identityPath, key) {
616
616
  root,
617
617
  renderer: root.renderer,
618
618
  component,
619
+ componentProps: null,
619
620
  parent,
620
621
  identityPath,
621
622
  key,
@@ -624,6 +625,7 @@ function createOwnerRecord(root, component, parent, identityPath, key) {
624
625
  hooks: /* @__PURE__ */ new Map(),
625
626
  effectOrder: [],
626
627
  children: [],
628
+ range: null,
627
629
  contextValues: null,
628
630
  updates: /* @__PURE__ */ new Map(),
629
631
  isBoundary: false,
@@ -639,6 +641,7 @@ function createOwnerRecord(root, component, parent, identityPath, key) {
639
641
  function draftOwner(record, parent, replayPath) {
640
642
  return {
641
643
  record,
644
+ componentProps: record.componentProps,
642
645
  parent,
643
646
  replayPath,
644
647
  hooks: new Map(record.hooks),
@@ -658,7 +661,7 @@ function draftOwner(record, parent, replayPath) {
658
661
  boundaryThenable: record.boundaryThenable,
659
662
  isBoundary: record.isBoundary,
660
663
  canHandleSuspense: record.canHandleSuspense,
661
- visibility: parent?.visibility ?? "visible"
664
+ visibility: parent?.visibility ?? record.visibility
662
665
  };
663
666
  }
664
667
  function childReplayPath(parent, component, identityPath, key) {
@@ -733,6 +736,15 @@ function readOwnerContext(owner, context, trackMemoRead = true) {
733
736
  return value;
734
737
  }
735
738
  }
739
+ if (currentAttempt().scope !== null) {
740
+ for (let current = owner?.record.parent ?? null; current !== null; current = current.parent) {
741
+ if (current.contextValues?.has(context)) {
742
+ value = current.contextValues.get(context);
743
+ if (trackMemoRead) CURRENT_MEMO_CONTEXT_READS?.set(context, value);
744
+ return value;
745
+ }
746
+ }
747
+ }
736
748
  value = currentAttempt().root.readBridgeContext(context);
737
749
  if (trackMemoRead) CURRENT_MEMO_CONTEXT_READS?.set(context, value);
738
750
  return value;
@@ -808,7 +820,7 @@ function renderLazyLeafItem(scope, render, item, index, key) {
808
820
  }
809
821
  }
810
822
  function ownerRange(owner, children) {
811
- return [{ kind: "range", key: owner.record.rangeKey, children }];
823
+ return [{ kind: "range", key: owner.record.rangeKey, owner: owner.record, children }];
812
824
  }
813
825
  function componentContext(renderer) {
814
826
  return {
@@ -836,6 +848,7 @@ function materializeComponentValue(value, expectedRenderer, path) {
836
848
  const normalized = normalizePropsValue(value.props);
837
849
  const owner = claimChildOwner(parent, value.component, path, value.hasKey ? value.key : null);
838
850
  const props = { ...normalized.props };
851
+ owner.componentProps = props;
839
852
  const nodes = executeOwner(owner, () => {
840
853
  const rendered = value.component(props, componentContext(expectedRenderer));
841
854
  return materializeValue(rendered, expectedRenderer, null, [...path, "output"]);
@@ -862,6 +875,8 @@ function disposeUncommittedDraft(owner) {
862
875
  for (const child of owner.children) disposeUncommittedDraft(child);
863
876
  if (!owner.record.mounted) {
864
877
  owner.record.disposed = true;
878
+ owner.record.componentProps = null;
879
+ owner.record.range = null;
865
880
  owner.record.updates.clear();
866
881
  for (const hook of owner.hooks.values()) {
867
882
  if (hook.kind === "effect-event") hook.cell.active = false;
@@ -907,6 +922,7 @@ function blueprintFromLogical(record) {
907
922
  return {
908
923
  kind: "range",
909
924
  key: record.key,
925
+ ...record.owner === null ? null : { owner: record.owner },
910
926
  children: record.children.map(blueprintFromLogical)
911
927
  };
912
928
  }
@@ -1777,6 +1793,61 @@ function walkDraftPostOrder(record, visit) {
1777
1793
  for (const child of record.children) walkDraftPostOrder(child, visit);
1778
1794
  visit(record);
1779
1795
  }
1796
+ function logicalTreeFeatures(record) {
1797
+ let features = 0;
1798
+ walkLogical(record, (current) => {
1799
+ if (current.kind === "portal") {
1800
+ features |= UNIVERSAL_TREE_PORTAL;
1801
+ return;
1802
+ }
1803
+ if (current.kind !== "host") return;
1804
+ if (current.events.size !== 0) features |= UNIVERSAL_TREE_EVENT;
1805
+ if (current.lifecycles.size !== 0) features |= UNIVERSAL_TREE_LIFECYCLE;
1806
+ if (current.localCallbacks.size !== 0) features |= UNIVERSAL_TREE_LOCAL_CALLBACK;
1807
+ if (current.ref != null) features |= UNIVERSAL_TREE_REF;
1808
+ if (current.visibility !== "visible") features |= UNIVERSAL_TREE_HIDDEN;
1809
+ for (const value of Object.values(current.props)) {
1810
+ if (isRendererRegion(value)) {
1811
+ features |= UNIVERSAL_TREE_REGION;
1812
+ break;
1813
+ }
1814
+ }
1815
+ });
1816
+ return features;
1817
+ }
1818
+ function ownerTreeHasWarmPlan(owner) {
1819
+ if (owner.component?.__warm !== void 0) return true;
1820
+ for (const child of owner.children) {
1821
+ if (ownerTreeHasWarmPlan(child)) return true;
1822
+ }
1823
+ return false;
1824
+ }
1825
+ function commonOwnerAncestor(left, right) {
1826
+ let leftDepth = 0;
1827
+ for (let current = left.parent; current !== null; current = current.parent) leftDepth++;
1828
+ let rightDepth = 0;
1829
+ for (let current = right.parent; current !== null; current = current.parent) rightDepth++;
1830
+ let a = left;
1831
+ let b = right;
1832
+ for (; leftDepth > rightDepth; leftDepth--) a = a.parent;
1833
+ for (; rightDepth > leftDepth; rightDepth--) b = b.parent;
1834
+ while (a !== null && a !== b) {
1835
+ a = a.parent;
1836
+ b = b.parent;
1837
+ }
1838
+ return a;
1839
+ }
1840
+ function stableLogicalChildren(records, blueprints) {
1841
+ if (records.length !== blueprints.length) return false;
1842
+ for (let index = 0; index < records.length; index++) {
1843
+ const record = records[index];
1844
+ const blueprint = blueprints[index];
1845
+ if (blueprint.kind === "range" && blueprint.compactLeafList !== void 0 || !sameRecordShape(record, blueprint) || !stableLogicalChildren(record.children, blueprint.children)) {
1846
+ return false;
1847
+ }
1848
+ }
1849
+ return true;
1850
+ }
1780
1851
  function collectRemovedPostOrder(record, output) {
1781
1852
  for (const child of record.children) collectRemovedPostOrder(child, output);
1782
1853
  if (record.kind === "host") output.push(record);
@@ -2090,7 +2161,7 @@ function scheduleOwner(owner, slot) {
2090
2161
  "state",
2091
2162
  typeof slot === "symbol" || typeof slot === "number" ? slot : void 0
2092
2163
  );
2093
- owner.root.schedule();
2164
+ owner.root.scheduleOwned(owner);
2094
2165
  }
2095
2166
  function currentDraftOwner() {
2096
2167
  currentAttempt();
@@ -3395,6 +3466,8 @@ class UniversalRootImpl {
3395
3466
  retryRenderInput = null;
3396
3467
  scheduled = false;
3397
3468
  scheduledUrgent = false;
3469
+ scheduledFullRoot = false;
3470
+ scheduledOwners = /* @__PURE__ */ new Set();
3398
3471
  scheduledPreparationDepth = 0;
3399
3472
  scheduledTransitionBatches = /* @__PURE__ */ new Set();
3400
3473
  unattemptedTransitionBatches = /* @__PURE__ */ new Set();
@@ -4041,7 +4114,7 @@ class UniversalRootImpl {
4041
4114
  }
4042
4115
  this.__scheduleMicrotask(() => this.flushScheduledWork());
4043
4116
  }
4044
- schedule() {
4117
+ markUrgentScheduled() {
4045
4118
  if (this.unmounted || this.owner?.disposed || this.lastComponent === null) return;
4046
4119
  this.scheduledUrgent = true;
4047
4120
  if (this.scheduled) return;
@@ -4049,6 +4122,15 @@ class UniversalRootImpl {
4049
4122
  SCHEDULED_UNIVERSAL_ROOTS.add(this);
4050
4123
  if (this.eventScopeDepth === 0 && UNIVERSAL_SYNC_DEPTH === 0) this.queueScheduledWork();
4051
4124
  }
4125
+ scheduleOwned(owner) {
4126
+ if (owner.root !== this || owner.disposed) return;
4127
+ this.scheduledOwners.add(owner);
4128
+ this.markUrgentScheduled();
4129
+ }
4130
+ schedule() {
4131
+ this.scheduledFullRoot = true;
4132
+ this.markUrgentScheduled();
4133
+ }
4052
4134
  /** @internal Associates promoted transition work with its accepting transaction. */
4053
4135
  scheduleTransition(batch) {
4054
4136
  if (batch.settled || this.unmounted || this.owner?.disposed || this.lastComponent === null) {
@@ -4391,6 +4473,8 @@ class UniversalRootImpl {
4391
4473
  for (const draft of owners) {
4392
4474
  if (committed.has(draft.record)) continue;
4393
4475
  draft.record.disposed = true;
4476
+ draft.record.componentProps = null;
4477
+ draft.record.range = null;
4394
4478
  draft.record.updates.clear();
4395
4479
  for (const hook of draft.hooks.values()) {
4396
4480
  if (hook.kind === "effect-event") hook.cell.active = false;
@@ -4439,9 +4523,184 @@ class UniversalRootImpl {
4439
4523
  projectedThenable: null
4440
4524
  };
4441
4525
  }
4526
+ // A local state update may replay only the component range that owns its hook.
4527
+ // The retained topology check keeps reconciliation atomic: features that need
4528
+ // root-wide coordination, or any structural change, fall back to the ordinary
4529
+ // full-root attempt before a host batch is prepared.
4530
+ prepareOwnedUpdate(target, component, props) {
4531
+ const range = target.range;
4532
+ if (this.transport !== null || this.bridge !== null || target.component === null || target.componentProps === null || ownerTreeHasWarmPlan(target) || range === null || range.kind !== "range" || range.owner !== target || target.visibility === "suspense-hidden") {
4533
+ return null;
4534
+ }
4535
+ for (let ancestor = target.parent; ancestor !== null; ancestor = ancestor.parent) {
4536
+ if (ancestor.isBoundary && (ancestor.hasBoundaryError || ancestor.boundaryThenable !== null)) {
4537
+ return null;
4538
+ }
4539
+ }
4540
+ const unsupported = UNIVERSAL_TREE_PORTAL | UNIVERSAL_TREE_REGION;
4541
+ const scopeFeatures = logicalTreeFeatures(range);
4542
+ if ((scopeFeatures & unsupported) !== 0) return null;
4543
+ this.flushPassivesBeforeRender();
4544
+ this.pending?.abort();
4545
+ const owner = draftOwner(target, null, [
4546
+ {
4547
+ component: target.component,
4548
+ identityPath: target.identityPath,
4549
+ key: target.key,
4550
+ ordinal: 0
4551
+ }
4552
+ ]);
4553
+ const previousAttempt = CURRENT_ATTEMPT;
4554
+ const previousOwner = CURRENT_OWNER;
4555
+ const previousWarm = CURRENT_UNIVERSAL_WARM;
4556
+ const previousWarmClaims = CURRENT_UNIVERSAL_WARM_CLAIMS;
4557
+ const previousWarmPlans = ACTIVE_UNIVERSAL_WARM_PLANS.slice();
4558
+ const attempt = {
4559
+ root: this,
4560
+ owner,
4561
+ scope: target,
4562
+ owners: [owner],
4563
+ treeFeatures: 0,
4564
+ replayEntries: [],
4565
+ retryThenables: /* @__PURE__ */ new Set(),
4566
+ nextUniversalId: this.nextUniversalId,
4567
+ implicitSlot: 0,
4568
+ transitionBatches: EMPTY_UNIVERSAL_TRANSITION_BATCHES,
4569
+ transitionRender: false,
4570
+ bridgeContextReads: null
4571
+ };
4572
+ ACTIVE_UNIVERSAL_WARM_PLANS.length = 0;
4573
+ CURRENT_UNIVERSAL_WARM = null;
4574
+ CURRENT_UNIVERSAL_WARM_CLAIMS = null;
4575
+ CURRENT_ATTEMPT = attempt;
4576
+ CURRENT_OWNER = owner;
4577
+ let nodes;
4578
+ try {
4579
+ nodes = executeOwner(owner, () => {
4580
+ const value = target.component(target.componentProps, componentContext(this.renderer));
4581
+ return materializeValue(value, this.renderer, null, [...target.identityPath, "output"]);
4582
+ });
4583
+ } catch (error) {
4584
+ this.discardDraftOwners(attempt.owners);
4585
+ if (error instanceof UniversalSuspense) {
4586
+ UNIVERSAL_WARM_CACHES.delete(this);
4587
+ return null;
4588
+ }
4589
+ for (let ancestor = target.parent; ancestor !== null; ancestor = ancestor.parent) {
4590
+ if (ancestor.isBoundary) {
4591
+ UNIVERSAL_WARM_CACHES.delete(this);
4592
+ return null;
4593
+ }
4594
+ }
4595
+ throw error;
4596
+ } finally {
4597
+ ACTIVE_UNIVERSAL_WARM_PLANS.length = 0;
4598
+ ACTIVE_UNIVERSAL_WARM_PLANS.push(...previousWarmPlans);
4599
+ CURRENT_UNIVERSAL_WARM = previousWarm;
4600
+ CURRENT_UNIVERSAL_WARM_CLAIMS = previousWarmClaims;
4601
+ CURRENT_ATTEMPT = previousAttempt;
4602
+ CURRENT_OWNER = previousOwner;
4603
+ }
4604
+ if (attempt.retryThenables.size !== 0 || (attempt.treeFeatures & unsupported) !== 0) {
4605
+ this.discardDraftOwners(attempt.owners);
4606
+ UNIVERSAL_WARM_CACHES.delete(this);
4607
+ return null;
4608
+ }
4609
+ const blueprint = {
4610
+ kind: "range",
4611
+ key: target.rangeKey,
4612
+ owner: target,
4613
+ children: nodes
4614
+ };
4615
+ this.expandCompactLeafLists(blueprint);
4616
+ let scopePlacement = null;
4617
+ if (!stableLogicalChildren(range.children, blueprint.children)) {
4618
+ scopePlacement = this.scopePhysicalPlacement(range);
4619
+ if (scopePlacement === null) {
4620
+ this.discardDraftOwners(attempt.owners);
4621
+ UNIVERSAL_WARM_CACHES.delete(this);
4622
+ return null;
4623
+ }
4624
+ }
4625
+ try {
4626
+ const transaction = this.createPreparedTransaction(
4627
+ blueprint,
4628
+ attempt,
4629
+ component,
4630
+ props,
4631
+ /* @__PURE__ */ new Set(),
4632
+ range,
4633
+ scopeFeatures,
4634
+ scopePlacement
4635
+ );
4636
+ this.pending = transaction;
4637
+ return transaction;
4638
+ } catch (error) {
4639
+ this.discardDraftOwners(attempt.owners);
4640
+ throw error;
4641
+ }
4642
+ }
4643
+ // The physical frame a scoped range commits into: its nearest host (or
4644
+ // container) ancestor plus the first physical host that follows the scope
4645
+ // inside that parent. Untouched siblings cannot move during a scoped commit,
4646
+ // so both stay valid anchors for inserted or reordered scope content. A
4647
+ // portal or detached ancestor owns a separate physical tree: no frame.
4648
+ scopePhysicalPlacement(scope) {
4649
+ let endAnchor = null;
4650
+ let current = scope;
4651
+ for (let parent = current.parent; parent !== null; current = parent, parent = current.parent) {
4652
+ if (parent.kind !== "host" && parent.kind !== "range") return null;
4653
+ if (endAnchor === null) {
4654
+ const siblings = parent.children;
4655
+ for (let index = siblings.indexOf(current) + 1; index < siblings.length; index++) {
4656
+ const found = physicalRecords([siblings[index]]);
4657
+ if (found.length !== 0) {
4658
+ endAnchor = found[0].id;
4659
+ break;
4660
+ }
4661
+ }
4662
+ }
4663
+ if (parent.kind === "host") return { parent: parent.id, endAnchor };
4664
+ if (parent === this.rootRecord) return { parent: null, endAnchor };
4665
+ }
4666
+ return null;
4667
+ }
4668
+ // A scheduled owner that cannot replay alone (an anonymous @for-item owner,
4669
+ // or several owners raised by one event scope) still has one smallest
4670
+ // ancestor whose retained props and range can replay every queued update:
4671
+ // the nearest enclosing component owner of their common ancestor.
4672
+ resolveScopedTarget() {
4673
+ let scope = null;
4674
+ for (const owner of this.scheduledOwners) {
4675
+ if (owner.disposed) continue;
4676
+ if (owner.visibility === "suspense-hidden") return void 0;
4677
+ scope = scope === null ? owner : commonOwnerAncestor(scope, owner);
4678
+ if (scope === null) return void 0;
4679
+ }
4680
+ let target = null;
4681
+ for (let current = scope; current !== null; current = current.parent) {
4682
+ if (current.component !== null && current.componentProps !== null && current.range !== null) {
4683
+ target = current;
4684
+ break;
4685
+ }
4686
+ }
4687
+ if (target === null) return void 0;
4688
+ for (const owner of this.scheduledOwners) {
4689
+ if (owner.disposed) continue;
4690
+ for (let current = owner; current !== target; current = current.parent) {
4691
+ if (current === null || current.isBoundary && (current.hasBoundaryError || current.boundaryThenable !== null)) {
4692
+ return void 0;
4693
+ }
4694
+ }
4695
+ }
4696
+ return target;
4697
+ }
4442
4698
  prepare(component, props) {
4699
+ const ownedTarget = this.scheduledPreparationDepth > 0 && this.scheduledUrgent && !this.scheduledFullRoot && this.scheduledOwners.size !== 0 ? this.resolveScopedTarget() : void 0;
4443
4700
  const scheduledUrgent = this.scheduledUrgent || this.scheduledPreparationDepth === 0;
4444
4701
  this.scheduledUrgent = false;
4702
+ this.scheduledFullRoot = false;
4703
+ this.scheduledOwners.clear();
4445
4704
  if (scheduledUrgent) this.urgentBoundarySuspension = null;
4446
4705
  const bridgeReplay = this.bridge === null ? null : this.queuedReplay;
4447
4706
  if (!scheduledUrgent && bridgeReplay !== null && bridgeReplay.component === component && bridgeReplay.active) {
@@ -4468,6 +4727,10 @@ class UniversalRootImpl {
4468
4727
  const scheduledTransitions = scheduledUrgent ? EMPTY_UNIVERSAL_TRANSITION_BATCHES : this.takeScheduledTransitionBatches();
4469
4728
  UNIVERSAL_WARM_CACHES.delete(this);
4470
4729
  try {
4730
+ if (ownedTarget !== void 0 && component === this.lastComponent && universalShallowEqual(props, this.lastProps)) {
4731
+ const ownedAttempt = this.prepareOwnedUpdate(ownedTarget, component, props);
4732
+ if (ownedAttempt !== null) return ownedAttempt;
4733
+ }
4471
4734
  const attempt = this.prepareWithReplay(
4472
4735
  component,
4473
4736
  props,
@@ -4505,6 +4768,7 @@ class UniversalRootImpl {
4505
4768
  { component, identityPath: ownerRecord.identityPath, key: ownerRecord.key, ordinal: 0 }
4506
4769
  ];
4507
4770
  const owner = draftOwner(ownerRecord, null, rootPath);
4771
+ owner.componentProps = props;
4508
4772
  const previousAttempt = CURRENT_ATTEMPT;
4509
4773
  const previousOwner = CURRENT_OWNER;
4510
4774
  const previousWarm = CURRENT_UNIVERSAL_WARM;
@@ -4513,6 +4777,7 @@ class UniversalRootImpl {
4513
4777
  const attempt = {
4514
4778
  root: this,
4515
4779
  owner,
4780
+ scope: null,
4516
4781
  owners: [owner],
4517
4782
  treeFeatures: 0,
4518
4783
  replayEntries,
@@ -4758,6 +5023,7 @@ class UniversalRootImpl {
4758
5023
  }
4759
5024
  for (const draft of attempt.owners) {
4760
5025
  const record = draft.record;
5026
+ record.componentProps = draft.componentProps;
4761
5027
  record.parent = draft.parent?.record ?? null;
4762
5028
  record.hooks = draft.hooks;
4763
5029
  record.effectOrder = [...draft.seenEffects];
@@ -4866,6 +5132,7 @@ class UniversalRootImpl {
4866
5132
  }
4867
5133
  for (const draft of attempt.owners) {
4868
5134
  const record = draft.record;
5135
+ record.componentProps = draft.componentProps;
4869
5136
  record.parent = draft.parent?.record ?? null;
4870
5137
  record.hooks = draft.hooks;
4871
5138
  record.effectOrder = [...draft.seenEffects];
@@ -4955,23 +5222,29 @@ class UniversalRootImpl {
4955
5222
  throw error;
4956
5223
  }
4957
5224
  }
4958
- createPreparedTransaction(blueprint, attempt, component, props, stagedPortalRegistrations) {
5225
+ createPreparedTransaction(blueprint, attempt, component, props, stagedPortalRegistrations, scopeRecord = this.rootRecord, scopeFeatures = 0, scopePlacement = null) {
4959
5226
  let nextId = this.nextId;
4960
- const treeFeatures = this.treeFeatures | attempt.treeFeatures;
4961
- const used = /* @__PURE__ */ new Set([this.rootRecord]);
5227
+ const scoped = scopeRecord !== this.rootRecord;
5228
+ const treeFeatures = (scoped ? scopeFeatures : this.treeFeatures) | attempt.treeFeatures;
5229
+ const used = /* @__PURE__ */ new Set([scopeRecord]);
5230
+ const changedRangeOwners = [];
4962
5231
  let topologyChanged = false;
4963
5232
  const reconcileChildren = (oldChildren, blueprints) => {
4964
5233
  if ((treeFeatures & UNIVERSAL_TREE_PORTAL) === 0 && oldChildren.length === blueprints.length && oldChildren.every((record, index) => sameRecordShape(record, blueprints[index]))) {
4965
5234
  return oldChildren.map((record, index) => {
4966
5235
  used.add(record);
4967
5236
  const blueprint2 = blueprints[index];
4968
- return {
5237
+ const draft = {
4969
5238
  record,
4970
5239
  blueprint: blueprint2,
4971
5240
  children: reconcileChildren(record.children, blueprint2.children),
4972
5241
  isNew: false,
4973
5242
  hostUpdate: null
4974
5243
  };
5244
+ if (record.kind === "range" && record.owner !== (blueprint2.owner ?? null)) {
5245
+ changedRangeOwners.push(draft);
5246
+ }
5247
+ return draft;
4975
5248
  });
4976
5249
  }
4977
5250
  const keyed = /* @__PURE__ */ new Map();
@@ -5012,13 +5285,17 @@ class UniversalRootImpl {
5012
5285
  record ??= createLogicalRecord(nextId++, child);
5013
5286
  claimed.add(record);
5014
5287
  used.add(record);
5015
- output.push({
5288
+ const draft = {
5016
5289
  record,
5017
5290
  blueprint: child,
5018
5291
  children: reconcileChildren(record.children, child.children),
5019
5292
  isNew,
5020
5293
  hostUpdate: null
5021
- });
5294
+ };
5295
+ if (record.kind === "range" && record.owner !== (child.owner ?? null)) {
5296
+ changedRangeOwners.push(draft);
5297
+ }
5298
+ output.push(draft);
5022
5299
  }
5023
5300
  if (oldChildren.length !== output.length || output.some((draft, index) => draft.record !== oldChildren[index])) {
5024
5301
  topologyChanged = true;
@@ -5026,9 +5303,9 @@ class UniversalRootImpl {
5026
5303
  return output;
5027
5304
  };
5028
5305
  const draftRoot = {
5029
- record: this.rootRecord,
5306
+ record: scopeRecord,
5030
5307
  blueprint,
5031
- children: reconcileChildren(this.rootRecord.children, blueprint.children),
5308
+ children: reconcileChildren(scopeRecord.children, blueprint.children),
5032
5309
  isNew: false,
5033
5310
  hostUpdate: null
5034
5311
  };
@@ -5039,13 +5316,13 @@ class UniversalRootImpl {
5039
5316
  else findRemoved(child);
5040
5317
  }
5041
5318
  };
5042
- if (topologyChanged) findRemoved(this.rootRecord);
5319
+ if (topologyChanged) findRemoved(scopeRecord);
5043
5320
  const previousPortalRegistrations = /* @__PURE__ */ new Set();
5044
5321
  const nextPortalRegistrations = /* @__PURE__ */ new Set();
5045
5322
  let reorderedPortalRecords = null;
5046
5323
  if ((treeFeatures & UNIVERSAL_TREE_PORTAL) !== 0) {
5047
5324
  const previousPortalsByTarget = topologyChanged ? /* @__PURE__ */ new Map() : null;
5048
- for (const child of this.rootRecord.children) {
5325
+ for (const child of scopeRecord.children) {
5049
5326
  walkLogical(child, (record) => {
5050
5327
  if (record.kind === "portal" && record.portalRegistration !== null) {
5051
5328
  previousPortalRegistrations.add(record.portalRegistration);
@@ -5092,7 +5369,7 @@ class UniversalRootImpl {
5092
5369
  const stagedRegionBridges = [];
5093
5370
  const nextRegionBridges = /* @__PURE__ */ new Set();
5094
5371
  if ((treeFeatures & UNIVERSAL_TREE_REGION) !== 0) {
5095
- for (const child of this.rootRecord.children) {
5372
+ for (const child of scopeRecord.children) {
5096
5373
  walkLogical(child, (record) => {
5097
5374
  if (record.kind !== "host") return;
5098
5375
  for (const value of Object.values(record.props)) {
@@ -5176,7 +5453,7 @@ class UniversalRootImpl {
5176
5453
  });
5177
5454
  const removes = [];
5178
5455
  const placements = [];
5179
- const planPlacements = (parentId, oldRecords, newDrafts, sourceParentId = parentId, forceMove = false) => {
5456
+ const planPlacements = (parentId, oldRecords, newDrafts, sourceParentId = parentId, forceMove = false, endAnchor = null) => {
5180
5457
  const oldPhysical = physicalRecords(oldRecords);
5181
5458
  const newPhysical = physicalDrafts(newDrafts);
5182
5459
  const desiredIds = new Set(newPhysical.map((entry) => entry.record.id));
@@ -5192,7 +5469,7 @@ class UniversalRootImpl {
5192
5469
  const id = draft.record.id;
5193
5470
  if (current[index] === id) continue;
5194
5471
  const currentIndex = current.indexOf(id);
5195
- const before = current[index] ?? null;
5472
+ const before = current[index] ?? endAnchor;
5196
5473
  if (currentIndex === -1) {
5197
5474
  placements.push({
5198
5475
  op: forceMove && previousIds.has(id) ? "move" : "insert",
@@ -5211,6 +5488,15 @@ class UniversalRootImpl {
5211
5488
  walkDraftPostOrder(draftRoot, (draft) => {
5212
5489
  if (draft.record === this.rootRecord) {
5213
5490
  planPlacements(null, this.rootRecord.children, draft.children);
5491
+ } else if (scoped && draft === draftRoot && scopePlacement !== null) {
5492
+ planPlacements(
5493
+ scopePlacement.parent,
5494
+ draft.record.children,
5495
+ draft.children,
5496
+ scopePlacement.parent,
5497
+ false,
5498
+ scopePlacement.endAnchor
5499
+ );
5214
5500
  } else if (draft.record.kind === "host") {
5215
5501
  planPlacements(draft.record.id, draft.record.children, draft.children);
5216
5502
  } else if (draft.record.kind === "portal") {
@@ -5441,7 +5727,7 @@ class UniversalRootImpl {
5441
5727
  committedOwnersParentFirst.push(owner);
5442
5728
  for (const child of owner.children) walkCommittedOwners(child);
5443
5729
  };
5444
- walkCommittedOwners(this.owner);
5730
+ walkCommittedOwners(scoped ? attempt.owner.record : this.owner);
5445
5731
  const removedOwners = committedOwnersParentFirst.filter((owner) => !draftedRecords.has(owner));
5446
5732
  const removedEffectEventCells = collectEffectEventCells(removedOwners);
5447
5733
  const orderedEffectCleanups = [];
@@ -5506,20 +5792,34 @@ class UniversalRootImpl {
5506
5792
  record.localCallbacks = localCallbackStage.staged.get(record) ?? record.localCallbacks;
5507
5793
  record.visibility = host.visibility;
5508
5794
  };
5795
+ const applyRangeOwner = (draft) => {
5796
+ const record = draft.record;
5797
+ const nextOwner = draft.blueprint.owner ?? null;
5798
+ if (record.owner !== nextOwner && record.owner?.range === record) {
5799
+ record.owner.range = null;
5800
+ }
5801
+ record.owner = nextOwner;
5802
+ if (nextOwner !== null) nextOwner.range = record;
5803
+ };
5509
5804
  const apply = (draft, parent) => {
5510
5805
  const record = draft.record;
5511
5806
  record.parent = parent;
5512
5807
  record.key = draft.blueprint.key;
5513
5808
  if (record.kind === "host") {
5514
5809
  applyHost(draft);
5810
+ } else if (record.kind === "range") {
5811
+ applyRangeOwner(draft);
5515
5812
  } else if (record.kind === "portal") {
5516
5813
  record.portalRegistration = draft.blueprint.registration;
5517
5814
  }
5518
5815
  record.children = draft.children.map((child) => child.record);
5519
5816
  for (const child of draft.children) apply(child, record);
5520
5817
  };
5521
- if (topologyChanged) apply(draftRoot, null);
5522
- else for (const draft of hostDrafts) applyHost(draft);
5818
+ if (topologyChanged) apply(draftRoot, scoped ? scopeRecord.parent : null);
5819
+ else {
5820
+ for (const draft of hostDrafts) applyHost(draft);
5821
+ for (const draft of changedRangeOwners) applyRangeOwner(draft);
5822
+ }
5523
5823
  stagedPortalRegistrations.clear();
5524
5824
  for (const registration of previousPortalRegistrations) {
5525
5825
  if (nextPortalRegistrations.has(registration)) continue;
@@ -5537,6 +5837,19 @@ class UniversalRootImpl {
5537
5837
  });
5538
5838
  }
5539
5839
  const hasPassiveWork = removedEffectEventCells.length !== 0 || orderedEffectCleanups.some((cleanup) => cleanup.phase === "passive") || effectChanges.some(({ next, changed }) => changed && next.phase === "passive");
5840
+ const previousScopedEventListeners = /* @__PURE__ */ new Set();
5841
+ const previousScopedLocalCallbacks = /* @__PURE__ */ new Set();
5842
+ if (scoped) {
5843
+ walkLogical(scopeRecord, (record) => {
5844
+ if (record.kind !== "host") return;
5845
+ for (const event of record.events.values()) {
5846
+ previousScopedEventListeners.add(event.listener);
5847
+ }
5848
+ for (const callback of record.localCallbacks.values()) {
5849
+ previousScopedLocalCallbacks.add(callback.listener);
5850
+ }
5851
+ });
5852
+ }
5540
5853
  const prepareHost = (value) => this.driver.prepareBatch(this.container, value, {
5541
5854
  invokeLocalCallback: (listener, args) => this.invokeLocalCallback(listener, args)
5542
5855
  });
@@ -5571,9 +5884,13 @@ class UniversalRootImpl {
5571
5884
  }
5572
5885
  }
5573
5886
  if ((treeFeatures & UNIVERSAL_TREE_EVENT) !== 0) {
5574
- for (const listener of this.publishedListeners) EVENT_DISPATCHERS.delete(listener);
5575
- this.publishedListeners.clear();
5576
- const handlers = /* @__PURE__ */ new Map();
5887
+ const handlers = scoped ? this.handlers : /* @__PURE__ */ new Map();
5888
+ const replacedListeners = scoped ? previousScopedEventListeners : this.publishedListeners;
5889
+ for (const listener of replacedListeners) {
5890
+ EVENT_DISPATCHERS.delete(listener);
5891
+ this.publishedListeners.delete(listener);
5892
+ handlers.delete(listener);
5893
+ }
5577
5894
  for (const [record, events] of stagedEvents) {
5578
5895
  if (!stagedVisibleEventRecords.has(record)) continue;
5579
5896
  for (const event of events.values()) {
@@ -5588,7 +5905,10 @@ class UniversalRootImpl {
5588
5905
  this.handlers = handlers;
5589
5906
  }
5590
5907
  if ((treeFeatures & UNIVERSAL_TREE_LOCAL_CALLBACK) !== 0) {
5591
- const localCallbacks = /* @__PURE__ */ new Map();
5908
+ const localCallbacks = scoped ? this.localCallbacks : /* @__PURE__ */ new Map();
5909
+ for (const listener of previousScopedLocalCallbacks) {
5910
+ localCallbacks.delete(listener);
5911
+ }
5592
5912
  for (const callbacks of localCallbackStage.staged.values()) {
5593
5913
  for (const callback of callbacks.values()) {
5594
5914
  localCallbacks.set(callback.listener, callback);
@@ -5599,11 +5919,16 @@ class UniversalRootImpl {
5599
5919
  for (const owner of removedOwners) {
5600
5920
  owner.disposed = true;
5601
5921
  owner.mounted = false;
5922
+ owner.componentProps = null;
5923
+ owner.range = null;
5602
5924
  owner.updates.clear();
5603
5925
  }
5604
5926
  for (const draft of draftOwnersParentFirst) {
5605
5927
  const record = draft.record;
5606
- record.parent = draft.parent?.record ?? null;
5928
+ record.componentProps = draft.componentProps;
5929
+ if (!scoped || draft !== attempt.owner) {
5930
+ record.parent = draft.parent?.record ?? null;
5931
+ }
5607
5932
  record.hooks = draft.hooks;
5608
5933
  record.effectOrder = [...draft.seenEffects];
5609
5934
  record.children = draft.children.map((child) => child.record);
@@ -5659,19 +5984,19 @@ class UniversalRootImpl {
5659
5984
  }
5660
5985
  }
5661
5986
  }
5662
- this.owner = attempt.owner.record;
5987
+ if (!scoped) this.owner = attempt.owner.record;
5663
5988
  this.lastComponent = component;
5664
5989
  this.lastProps = props;
5665
5990
  this.retryRenderInput = null;
5666
5991
  this.urgentBoundarySuspension = null;
5667
- this.bridgeContextReads = attempt.bridgeContextReads;
5992
+ if (!scoped) this.bridgeContextReads = attempt.bridgeContextReads;
5668
5993
  if (retryThenables.length > 0) {
5669
5994
  this.publishLocalReplay(retryThenables, retryMemos, component, props);
5670
5995
  }
5671
5996
  this.nextId = nextId;
5672
5997
  this.nextUniversalId = attempt.nextUniversalId;
5673
5998
  this.nextListener = nextListener;
5674
- this.treeFeatures = attempt.treeFeatures;
5999
+ this.treeFeatures = scoped ? this.treeFeatures | attempt.treeFeatures : attempt.treeFeatures;
5675
6000
  for (const context of changedContexts) context.$$version++;
5676
6001
  const retainedRegionCells = /* @__PURE__ */ new Set();
5677
6002
  for (const { next, previous } of stagedRegionBridges) {
@@ -6034,6 +6359,8 @@ class UniversalRootImpl {
6034
6359
  finalize: (acceptedHostError) => {
6035
6360
  this.scheduled = false;
6036
6361
  this.scheduledUrgent = false;
6362
+ this.scheduledFullRoot = false;
6363
+ this.scheduledOwners.clear();
6037
6364
  SCHEDULED_UNIVERSAL_ROOTS.delete(this);
6038
6365
  const transitionBatches = new Set(this.takeScheduledTransitionBatches());
6039
6366
  for (const transition of stagedTransitionBatches) transitionBatches.add(transition);
@@ -6042,6 +6369,8 @@ class UniversalRootImpl {
6042
6369
  this.cancelSuspendedReplays();
6043
6370
  this.scheduled = false;
6044
6371
  this.scheduledUrgent = false;
6372
+ this.scheduledFullRoot = false;
6373
+ this.scheduledOwners.clear();
6045
6374
  SCHEDULED_UNIVERSAL_ROOTS.delete(this);
6046
6375
  let attachmentUnsubscribeError = NO_PENDING_PASSIVE_ERROR;
6047
6376
  if (this.hostAttachments !== null) {
@@ -6068,6 +6397,8 @@ class UniversalRootImpl {
6068
6397
  for (const owner of owners) {
6069
6398
  owner.disposed = true;
6070
6399
  owner.mounted = false;
6400
+ owner.componentProps = null;
6401
+ owner.range = null;
6071
6402
  owner.updates.clear();
6072
6403
  }
6073
6404
  const deactivatedRegionCells = /* @__PURE__ */ new Set();
@@ -6450,6 +6781,7 @@ function createObjectContainer(renderer = "object") {
6450
6781
  assertRendererId(renderer, "Object container renderer");
6451
6782
  const state = {
6452
6783
  instances: /* @__PURE__ */ new Map(),
6784
+ parents: /* @__PURE__ */ new Map(),
6453
6785
  events: /* @__PURE__ */ new Map(),
6454
6786
  lifecycles: /* @__PURE__ */ new Map(),
6455
6787
  localCallbacks: /* @__PURE__ */ new Map(),
@@ -6517,8 +6849,14 @@ function createObjectDriver(renderer = "object") {
6517
6849
  }
6518
6850
  const state = container[OBJECT_DRIVER_STATE];
6519
6851
  const simulated = /* @__PURE__ */ new Map();
6520
- for (const [id, instance] of state.instances) {
6521
- simulated.set(id, {
6852
+ const destroyed = /* @__PURE__ */ new Set();
6853
+ const readSimulated = (id) => {
6854
+ if (destroyed.has(id)) return void 0;
6855
+ let value = simulated.get(id);
6856
+ if (value !== void 0) return value;
6857
+ const instance = state.instances.get(id);
6858
+ if (instance === void 0) return void 0;
6859
+ value = {
6522
6860
  type: instance.type,
6523
6861
  props: instance.props,
6524
6862
  visible: instance.visible,
@@ -6526,8 +6864,11 @@ function createObjectDriver(renderer = "object") {
6526
6864
  events: new Map(state.events.get(id)),
6527
6865
  lifecycles: new Map(state.lifecycles.get(id)),
6528
6866
  localCallbacks: new Map(state.localCallbacks.get(id))
6529
- });
6530
- }
6867
+ };
6868
+ simulated.set(id, value);
6869
+ return value;
6870
+ };
6871
+ const hasSimulated = (id) => !destroyed.has(id) && (simulated.has(id) || state.instances.has(id));
6531
6872
  const stagedInstances = /* @__PURE__ */ new Map();
6532
6873
  const cleanupKeys = /* @__PURE__ */ new Set();
6533
6874
  const invokeKeys = /* @__PURE__ */ new Set();
@@ -6536,17 +6877,35 @@ function createObjectDriver(renderer = "object") {
6536
6877
  const separator = key.indexOf(":");
6537
6878
  return [Number(key.slice(0, separator)), key.slice(separator + 1)];
6538
6879
  };
6539
- const rootChildren = container.children.map((child) => child.id);
6880
+ const DETACHED = /* @__PURE__ */ Symbol("octane.object-driver.detached");
6881
+ const parentChanges = /* @__PURE__ */ new Map();
6882
+ let rootChildren = null;
6883
+ const readParent = (id) => {
6884
+ if (parentChanges.has(id)) return parentChanges.get(id);
6885
+ return state.parents.has(id) ? state.parents.get(id) : DETACHED;
6886
+ };
6540
6887
  const simulatedChildren = (parent) => {
6541
- if (parent === null) return rootChildren;
6542
- const value = simulated.get(parent);
6888
+ if (parent === null) {
6889
+ return rootChildren ??= container.children.map((child) => child.id);
6890
+ }
6891
+ const value = readSimulated(parent);
6543
6892
  if (value === void 0) throw new Error(`Object driver: unknown parent ${parent}.`);
6544
6893
  return value.children;
6545
6894
  };
6895
+ const detachSimulated = (id) => {
6896
+ const parent = readParent(id);
6897
+ if (parent === DETACHED) return;
6898
+ const children = simulatedChildren(parent);
6899
+ const index = children.indexOf(id);
6900
+ if (index === -1) throw new Error(`Object driver: child ${id} is not attached.`);
6901
+ children.splice(index, 1);
6902
+ parentChanges.set(id, DETACHED);
6903
+ };
6546
6904
  for (const command of batch.commands) {
6547
6905
  if (command.op === "create") {
6548
- if (simulated.has(command.id))
6906
+ if (hasSimulated(command.id))
6549
6907
  throw new Error(`Object driver: duplicate id ${command.id}.`);
6908
+ destroyed.delete(command.id);
6550
6909
  simulated.set(command.id, {
6551
6910
  type: command.type,
6552
6911
  props: command.props,
@@ -6564,11 +6923,11 @@ function createObjectDriver(renderer = "object") {
6564
6923
  children: []
6565
6924
  });
6566
6925
  } else if (command.op === "update") {
6567
- const value = simulated.get(command.id);
6926
+ const value = readSimulated(command.id);
6568
6927
  if (value === void 0) throw new Error(`Object driver: unknown update ${command.id}.`);
6569
6928
  value.props = command.props;
6570
6929
  } else if (command.op === "recreate") {
6571
- const value = simulated.get(command.id);
6930
+ const value = readSimulated(command.id);
6572
6931
  const current = state.instances.get(command.id);
6573
6932
  if (value === void 0 || current === void 0) {
6574
6933
  throw new Error(`Object driver: unknown recreate ${command.id}.`);
@@ -6589,25 +6948,25 @@ function createObjectDriver(renderer = "object") {
6589
6948
  invokeKeys.add(keyFor(command.id, type));
6590
6949
  }
6591
6950
  } else if (command.op === "visibility") {
6592
- const value = simulated.get(command.id);
6951
+ const value = readSimulated(command.id);
6593
6952
  if (value === void 0) {
6594
6953
  throw new Error(`Object driver: unknown visibility target ${command.id}.`);
6595
6954
  }
6596
6955
  value.visible = command.state === "visible";
6597
6956
  } else if (command.op === "event") {
6598
- const value = simulated.get(command.id);
6957
+ const value = readSimulated(command.id);
6599
6958
  if (value === void 0)
6600
6959
  throw new Error(`Object driver: unknown event target ${command.id}.`);
6601
6960
  if (command.listener === null) value.events.delete(command.type);
6602
6961
  else value.events.set(command.type, command.listener);
6603
6962
  } else if (command.op === "lifecycle") {
6604
- const value = simulated.get(command.id);
6963
+ const value = readSimulated(command.id);
6605
6964
  if (value === void 0)
6606
6965
  throw new Error(`Object driver: unknown lifecycle target ${command.id}.`);
6607
6966
  if (command.listener === null) value.lifecycles.delete(command.type);
6608
6967
  else value.lifecycles.set(command.type, command.listener);
6609
6968
  } else if (command.op === "local-callback") {
6610
- const value = simulated.get(command.id);
6969
+ const value = readSimulated(command.id);
6611
6970
  if (value === void 0)
6612
6971
  throw new Error(`Object driver: unknown local callback target ${command.id}.`);
6613
6972
  const key = keyFor(command.id, command.type);
@@ -6621,21 +6980,16 @@ function createObjectDriver(renderer = "object") {
6621
6980
  if (command.parent !== null && typeof command.parent !== "number") {
6622
6981
  throw new Error("Object driver does not support portal target parents.");
6623
6982
  }
6624
- if (!simulated.has(command.id))
6983
+ if (!hasSimulated(command.id))
6625
6984
  throw new Error(`Object driver: unknown child ${command.id}.`);
6626
- for (const value of [
6627
- rootChildren,
6628
- ...[...simulated.values()].map((entry) => entry.children)
6629
- ]) {
6630
- const old = value.indexOf(command.id);
6631
- if (old !== -1) value.splice(old, 1);
6632
- }
6985
+ detachSimulated(command.id);
6633
6986
  const children = simulatedChildren(command.parent);
6634
6987
  const before = command.before === null ? children.length : children.indexOf(command.before);
6635
6988
  if (before === -1) throw new Error(`Object driver: unknown before id ${command.before}.`);
6636
6989
  children.splice(before, 0, command.id);
6990
+ parentChanges.set(command.id, command.parent);
6637
6991
  if (command.op === "move") {
6638
- for (const type of simulated.get(command.id).localCallbacks.keys()) {
6992
+ for (const type of readSimulated(command.id).localCallbacks.keys()) {
6639
6993
  const key = keyFor(command.id, type);
6640
6994
  cleanupKeys.add(key);
6641
6995
  invokeKeys.add(key);
@@ -6649,26 +7003,22 @@ function createObjectDriver(renderer = "object") {
6649
7003
  const index = children.indexOf(command.id);
6650
7004
  if (index === -1) throw new Error(`Object driver: child ${command.id} is not attached.`);
6651
7005
  children.splice(index, 1);
6652
- for (const type of simulated.get(command.id).localCallbacks.keys()) {
7006
+ parentChanges.set(command.id, DETACHED);
7007
+ for (const type of readSimulated(command.id).localCallbacks.keys()) {
6653
7008
  cleanupKeys.add(keyFor(command.id, type));
6654
7009
  }
6655
7010
  } else if (command.op === "destroy") {
6656
- const instance = simulated.get(command.id);
7011
+ const instance = readSimulated(command.id);
6657
7012
  if (instance === void 0)
6658
7013
  throw new Error(`Object driver: unknown destroy ${command.id}.`);
6659
- for (const value of [
6660
- rootChildren,
6661
- ...[...simulated.values()].map((entry) => entry.children)
6662
- ]) {
6663
- const attached = value.indexOf(command.id);
6664
- if (attached !== -1) value.splice(attached, 1);
6665
- }
7014
+ detachSimulated(command.id);
7015
+ for (const child of instance.children) parentChanges.set(child, DETACHED);
6666
7016
  instance.children.length = 0;
6667
- simulated.delete(command.id);
7017
+ destroyed.add(command.id);
6668
7018
  }
6669
7019
  }
6670
7020
  for (const [id, instance] of stagedInstances) {
6671
- const staged = simulated.get(id);
7021
+ const staged = readSimulated(id);
6672
7022
  if (staged === void 0) continue;
6673
7023
  instance.visible = staged.visible;
6674
7024
  instance.children.splice(
@@ -6681,6 +7031,16 @@ function createObjectDriver(renderer = "object") {
6681
7031
  }
6682
7032
  let status = "prepared";
6683
7033
  let acceptedCallbacksRan = false;
7034
+ const liveChildren = (parent) => objectChildren(container, parent, state.instances);
7035
+ const detachLive = (id) => {
7036
+ if (!state.parents.has(id)) return;
7037
+ const parent = state.parents.get(id);
7038
+ const instance = state.instances.get(id);
7039
+ const children = liveChildren(parent);
7040
+ const index = children.indexOf(instance);
7041
+ if (index !== -1) children.splice(index, 1);
7042
+ state.parents.delete(id);
7043
+ };
6684
7044
  return {
6685
7045
  apply() {
6686
7046
  if (status !== "prepared") return;
@@ -6697,7 +7057,11 @@ function createObjectDriver(renderer = "object") {
6697
7057
  tasks.push(() => {
6698
7058
  for (const command of batch.commands) {
6699
7059
  if (command.op === "create") {
6700
- state.instances.set(command.id, stagedInstances.get(command.id));
7060
+ const instance = stagedInstances.get(command.id);
7061
+ state.instances.set(command.id, instance);
7062
+ for (const child of instance.children) {
7063
+ state.parents.set(child.id, command.id);
7064
+ }
6701
7065
  state.events.set(command.id, /* @__PURE__ */ new Map());
6702
7066
  state.lifecycles.set(command.id, /* @__PURE__ */ new Map());
6703
7067
  state.localCallbacks.set(command.id, /* @__PURE__ */ new Map());
@@ -6707,10 +7071,8 @@ function createObjectDriver(renderer = "object") {
6707
7071
  } else if (command.op === "recreate") {
6708
7072
  const previous = state.instances.get(command.id);
6709
7073
  const replacement = stagedInstances.get(command.id);
6710
- for (const parent of [
6711
- container.children,
6712
- ...[...state.instances.values()].map((entry) => entry.children)
6713
- ]) {
7074
+ if (state.parents.has(command.id)) {
7075
+ const parent = liveChildren(state.parents.get(command.id));
6714
7076
  const index = parent.indexOf(previous);
6715
7077
  if (index !== -1) parent[index] = replacement;
6716
7078
  }
@@ -6735,33 +7097,25 @@ function createObjectDriver(renderer = "object") {
6735
7097
  throw new Error("Object driver does not support portal target parents.");
6736
7098
  }
6737
7099
  const instance = state.instances.get(command.id);
6738
- for (const parent of [
6739
- container.children,
6740
- ...[...state.instances.values()].map((entry) => entry.children)
6741
- ]) {
6742
- const old = parent.indexOf(instance);
6743
- if (old !== -1) parent.splice(old, 1);
6744
- }
6745
- const children = objectChildren(container, command.parent, state.instances);
7100
+ detachLive(command.id);
7101
+ const children = liveChildren(command.parent);
6746
7102
  const before = command.before === null ? children.length : children.indexOf(state.instances.get(command.before));
6747
7103
  children.splice(before, 0, instance);
7104
+ state.parents.set(command.id, command.parent);
6748
7105
  } else if (command.op === "remove") {
6749
7106
  if (command.parent !== null && typeof command.parent !== "number") {
6750
7107
  throw new Error("Object driver does not support portal target parents.");
6751
7108
  }
6752
- const children = objectChildren(container, command.parent, state.instances);
7109
+ const children = liveChildren(command.parent);
6753
7110
  children.splice(children.indexOf(state.instances.get(command.id)), 1);
7111
+ state.parents.delete(command.id);
6754
7112
  } else if (command.op === "destroy") {
6755
7113
  const instance = state.instances.get(command.id);
6756
- for (const parent of [
6757
- container.children,
6758
- ...[...state.instances.values()].map((entry) => entry.children)
6759
- ]) {
6760
- const attached = parent.indexOf(instance);
6761
- if (attached !== -1) parent.splice(attached, 1);
6762
- }
7114
+ detachLive(command.id);
7115
+ for (const child of instance.children) state.parents.delete(child.id);
6763
7116
  instance.children.length = 0;
6764
7117
  state.instances.delete(command.id);
7118
+ state.parents.delete(command.id);
6765
7119
  state.events.delete(command.id);
6766
7120
  state.lifecycles.delete(command.id);
6767
7121
  state.localCallbacks.delete(command.id);
@@ -6782,13 +7136,8 @@ function createObjectDriver(renderer = "object") {
6782
7136
  const listener = state.localCallbacks.get(id)?.get(type);
6783
7137
  if (instance === void 0 || listener === void 0) continue;
6784
7138
  tasks.push(() => {
6785
- let parent = null;
6786
- for (const candidate of state.instances.values()) {
6787
- if (candidate.children.includes(instance)) {
6788
- parent = candidate;
6789
- break;
6790
- }
6791
- }
7139
+ const parentId = state.parents.get(id);
7140
+ const parent = parentId == null ? null : state.instances.get(parentId) ?? null;
6792
7141
  const cleanup = context.invokeLocalCallback(listener.id, [parent, instance]);
6793
7142
  if (cleanup == null) return;
6794
7143
  if (typeof cleanup !== "function") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octane",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,