octane 0.1.50 → 0.1.51

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.
@@ -4238,29 +4238,53 @@ function __useStateWithGetter(initial, slot) {
4238
4238
  let HIDDEN_REVEAL_ACTIONS = null;
4239
4239
  function deferHiddenRevealAction(boundary, action) {
4240
4240
  const deferred = HIDDEN_REVEAL_ACTIONS ??= /* @__PURE__ */ new WeakMap();
4241
- const actions = deferred.get(boundary);
4242
- if (actions === void 0) deferred.set(boundary, [action]);
4243
- else actions.push(action);
4241
+ let queue = deferred.get(boundary);
4242
+ if (queue === void 0) {
4243
+ queue = { entries: [], live: 0 };
4244
+ deferred.set(boundary, queue);
4245
+ }
4246
+ const entry = { action, active: true, queue };
4247
+ queue.entries.push(entry);
4248
+ queue.live++;
4249
+ return entry;
4250
+ }
4251
+ function claimHiddenRevealAction(boundary, entry) {
4252
+ const queue = entry.queue;
4253
+ if (HIDDEN_REVEAL_ACTIONS?.get(boundary) !== queue) return null;
4254
+ if (!entry.active) return null;
4255
+ entry.active = false;
4256
+ if (--queue.live === 0) HIDDEN_REVEAL_ACTIONS.delete(boundary);
4257
+ return entry.action;
4258
+ }
4259
+ function cancelHiddenRevealAction(boundary, entry) {
4260
+ const queue = entry.queue;
4261
+ if (claimHiddenRevealAction(boundary, entry) === null || queue.live === 0) return;
4262
+ if (queue.entries.length > queue.live * 2) {
4263
+ queue.entries = queue.entries.filter((candidate) => candidate.active);
4264
+ }
4244
4265
  }
4245
4266
  function publishHiddenRevealActions(boundary) {
4246
- const actions = HIDDEN_REVEAL_ACTIONS?.get(boundary);
4247
- if (actions === void 0) return;
4267
+ const queue = HIDDEN_REVEAL_ACTIONS?.get(boundary);
4268
+ if (queue === void 0) return;
4269
+ const entries = queue.entries;
4248
4270
  if (boundary.__kind === "activityBlockSlot") {
4249
- for (let i = 0; i < actions.length; i++) {
4250
- const action = actions[i];
4271
+ const end = entries.length;
4272
+ for (let i = 0; i < end; i++) {
4273
+ const entry = entries[i];
4274
+ if (!entry.active) continue;
4251
4275
  enqueueEffectEventCommitAction(() => {
4252
- if (HIDDEN_REVEAL_ACTIONS?.get(boundary) !== actions) return;
4253
- const index = actions.indexOf(action);
4254
- if (index === -1) return;
4255
- actions.splice(index, 1);
4256
- if (actions.length === 0) HIDDEN_REVEAL_ACTIONS.delete(boundary);
4257
- return action();
4276
+ const claimed = claimHiddenRevealAction(boundary, entry);
4277
+ if (claimed === null) return;
4278
+ return claimed();
4258
4279
  });
4259
4280
  }
4260
4281
  return;
4261
4282
  }
4262
4283
  HIDDEN_REVEAL_ACTIONS.delete(boundary);
4263
- for (let i = 0; i < actions.length; i++) enqueueEffectEventCommitAction(actions[i]);
4284
+ for (let i = 0; i < entries.length; i++) {
4285
+ const entry = entries[i];
4286
+ if (entry.active) enqueueEffectEventCommitAction(entry.action);
4287
+ }
4264
4288
  }
4265
4289
  function useLinkedState(source, reconcile, options, slot) {
4266
4290
  if (typeof options === "symbol") {
@@ -5334,10 +5358,10 @@ function beginHydratePreload(state) {
5334
5358
  return promise;
5335
5359
  }
5336
5360
  function resolveHydrateWaiters(state, reason) {
5337
- if (state.hydrationWaiters.size === 0) return;
5338
- const waiters = [...state.hydrationWaiters];
5339
- state.hydrationWaiters.clear();
5340
- for (let i = 0; i < waiters.length; i++) waiters[i](reason);
5361
+ const waiters = state.hydrationWaiters;
5362
+ if (waiters === null) return;
5363
+ state.hydrationWaiters = null;
5364
+ for (const waiter of waiters) waiter(reason);
5341
5365
  }
5342
5366
  function waitForHydratePrefetchStrategy(state, strategy) {
5343
5367
  const controller = state.prefetchAbort;
@@ -5351,13 +5375,13 @@ function waitForHydratePrefetchStrategy(state, strategy) {
5351
5375
  if (settled) return;
5352
5376
  settled = true;
5353
5377
  cleanup?.();
5354
- state.hydrationWaiters.delete(onHydrate);
5378
+ state.hydrationWaiters?.delete(onHydrate);
5355
5379
  signal.removeEventListener("abort", onAbort);
5356
5380
  resolve(reason);
5357
5381
  };
5358
5382
  const onHydrate = () => finish("hydrate");
5359
5383
  const onAbort = () => finish("abort");
5360
- state.hydrationWaiters.add(onHydrate);
5384
+ (state.hydrationWaiters ??= /* @__PURE__ */ new Set()).add(onHydrate);
5361
5385
  signal.addEventListener("abort", onAbort, { once: true });
5362
5386
  cleanup = strategy._s?.({
5363
5387
  element: state.wrapper,
@@ -5757,7 +5781,7 @@ function createHydrateSlot(props, scope, boundaryId) {
5757
5781
  prefetchPromise: null,
5758
5782
  preloadPromise: null,
5759
5783
  loadedBody: null,
5760
- hydrationWaiters: /* @__PURE__ */ new Set(),
5784
+ hydrationWaiters: null,
5761
5785
  activationGeneration: 0,
5762
5786
  activationRequested: !serverPreserved,
5763
5787
  activationReady: !serverPreserved,
@@ -19475,28 +19499,23 @@ function enqueueInlineCaughtError(state) {
19475
19499
  const handler = rootErrorHandlersFor(block)?.onCaughtError;
19476
19500
  if (handler === void 0) return;
19477
19501
  const error = state.err;
19478
- let parkedOwner = null;
19502
+ let parked = null;
19479
19503
  let cleanupRegistered = false;
19480
19504
  const action = (owner2) => {
19481
- parkedOwner = null;
19505
+ parked = null;
19482
19506
  if (block.disposed || state.block !== block) return;
19483
19507
  const hidden = owner2 ?? findScheduledVisibilityOwner(block, true);
19484
19508
  if (hidden !== null) {
19485
- parkedOwner = hidden;
19486
19509
  if (!cleanupRegistered) {
19487
19510
  cleanupRegistered = true;
19488
19511
  (block.cleanups ??= []).push(() => {
19489
- const owner3 = parkedOwner;
19490
- parkedOwner = null;
19491
- if (owner3 === null) return;
19492
- const actions = HIDDEN_REVEAL_ACTIONS?.get(owner3);
19493
- if (actions === void 0) return;
19494
- const index = actions.indexOf(action);
19495
- if (index !== -1) actions.splice(index, 1);
19496
- if (actions.length === 0) HIDDEN_REVEAL_ACTIONS.delete(owner3);
19512
+ const pending = parked;
19513
+ parked = null;
19514
+ if (pending === null) return;
19515
+ cancelHiddenRevealAction(pending.owner, pending.entry);
19497
19516
  });
19498
19517
  }
19499
- deferHiddenRevealAction(hidden, action);
19518
+ parked = { owner: hidden, entry: deferHiddenRevealAction(hidden, action) };
19500
19519
  return;
19501
19520
  }
19502
19521
  return { state, block, error, handler, resume: action };
@@ -333,10 +333,14 @@ class ScopeImpl {
333
333
  type,
334
334
  ...node ? { key: node.key, revision: node.revision } : {}
335
335
  };
336
- if (this.events.length === this.traceLimit) this.events.shift();
337
- this.events.push(event);
336
+ if (this.events.length === this.traceLimit) {
337
+ this.events[(event.sequence - 1) % this.traceLimit] = event;
338
+ } else {
339
+ this.events.push(event);
340
+ }
338
341
  }
339
342
  inspect() {
343
+ const traceStart = this.traceLimit && this.events.length === this.traceLimit ? this.sequence % this.traceLimit : 0;
340
344
  return {
341
345
  scopeKey: this.scopeKey,
342
346
  epoch: this.epoch,
@@ -365,7 +369,9 @@ class ScopeImpl {
365
369
  dependencies
366
370
  };
367
371
  }),
368
- trace: this.events.map((event) => ({ ...event }))
372
+ trace: this.events.map((event, index) => ({
373
+ ...traceStart ? this.events[(traceStart + index) % this.events.length] : event
374
+ }))
369
375
  };
370
376
  }
371
377
  dispose() {
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  version: () => version
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const version = "0.1.50";
24
+ const version = "0.1.51";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  version
@@ -75,6 +75,21 @@ export const OCTANE_RUNTIME_REQUESTS = Object.freeze({
75
75
  server: 'octane/server',
76
76
  });
77
77
 
78
+ // Vite can classify descriptor exports from its transform-local preflight AST,
79
+ // but the neutral compiler must never trust caller-supplied facts or a naked
80
+ // authored AST. Only opaque objects registered here can bypass the established
81
+ // string classifier, and each proof is consumed once for its exact source/id.
82
+ const descriptorChildrenExportProofs = new WeakMap();
83
+ const descriptorChildrenExportAuthorities = new WeakMap();
84
+
85
+ function consumeDescriptorChildrenExportProof(proof, source, id) {
86
+ if (proof === null || typeof proof !== 'object') return null;
87
+ const prepared = descriptorChildrenExportProofs.get(proof);
88
+ descriptorChildrenExportProofs.delete(proof);
89
+ if (prepared?.source !== source || prepared.id !== id) return null;
90
+ return prepared.exports;
91
+ }
92
+
78
93
  /** Strip bundler query/hash suffixes without changing the underlying path. */
79
94
  export function cleanModuleId(id) {
80
95
  const query = id.indexOf('?');
@@ -259,6 +274,7 @@ export function findVoidComponentExports(source, id) {
259
274
  }
260
275
 
261
276
  const voidBindings = new Set();
277
+ const memoDependents = new Map();
262
278
  const hasLowerableJsxReturnBranches = createJsxReturnBranchClassifier(ast.body || []);
263
279
  // Mirrors the compile-time lowering decisions exactly (nullish-guard @{}
264
280
  // bodies AND React-style conditional JSX returns), so cross-module call-site
@@ -273,41 +289,40 @@ export function findVoidComponentExports(source, id) {
273
289
  continue;
274
290
  }
275
291
  if (declaration.type !== 'VariableDeclaration' || declaration.kind !== 'const') continue;
292
+ // Resolve only the exact, immutable `const Export = memo(Local)` form. The
293
+ // imported memo identity is lexical proof; method calls, comparators, and
294
+ // arbitrary wrappers stay unknown. Index the reverse edges once so a chain
295
+ // declared outermost-first does not rescan every declaration per link.
276
296
  for (const item of declaration.declarations || []) {
297
+ const init = item.init;
277
298
  if (
278
299
  item.id?.type === 'Identifier' &&
279
- (item.init?.type === 'FunctionExpression' ||
280
- item.init?.type === 'ArrowFunctionExpression') &&
281
- isVoidFunction(item.init)
300
+ (init?.type === 'FunctionExpression' || init?.type === 'ArrowFunctionExpression') &&
301
+ isVoidFunction(init)
282
302
  ) {
283
303
  voidBindings.add(item.id.name);
284
304
  }
305
+ if (
306
+ item.id?.type !== 'Identifier' ||
307
+ init?.type !== 'CallExpression' ||
308
+ init.callee?.type !== 'Identifier' ||
309
+ !memoLocals.has(init.callee.name) ||
310
+ init.arguments?.length !== 1 ||
311
+ init.arguments[0]?.type !== 'Identifier'
312
+ )
313
+ continue;
314
+ const target = init.arguments[0].name;
315
+ let dependents = memoDependents.get(target);
316
+ if (dependents === undefined) memoDependents.set(target, (dependents = []));
317
+ dependents.push(item.id.name);
285
318
  }
286
319
  }
287
- // Resolve only the exact, immutable `const Export = memo(Local)` form. The
288
- // imported memo identity is lexical proof; method calls, comparators, and
289
- // arbitrary wrappers stay unknown.
290
- let changed = true;
291
- while (changed) {
292
- changed = false;
293
- for (const declaration of declarations) {
294
- if (declaration.type !== 'VariableDeclaration' || declaration.kind !== 'const') continue;
295
- for (const item of declaration.declarations || []) {
296
- const init = item.init;
297
- if (
298
- item.id?.type !== 'Identifier' ||
299
- voidBindings.has(item.id.name) ||
300
- init?.type !== 'CallExpression' ||
301
- init.callee?.type !== 'Identifier' ||
302
- !memoLocals.has(init.callee.name) ||
303
- init.arguments?.length !== 1 ||
304
- init.arguments[0]?.type !== 'Identifier' ||
305
- !voidBindings.has(init.arguments[0].name)
306
- )
307
- continue;
308
- voidBindings.add(item.id.name);
309
- changed = true;
310
- }
320
+ const pending = [...voidBindings];
321
+ for (let index = 0; index < pending.length; index++) {
322
+ for (const dependent of memoDependents.get(pending[index]) ?? []) {
323
+ if (voidBindings.has(dependent)) continue;
324
+ voidBindings.add(dependent);
325
+ pending.push(dependent);
311
326
  }
312
327
  }
313
328
 
@@ -518,6 +533,7 @@ class OctaneBundlerCompiler {
518
533
  if (renderer.intrinsics !== undefined) this.pragmaOwnedModules.add(renderer.intrinsics);
519
534
  }
520
535
  this.warn = typeof options.warn === 'function' ? options.warn : null;
536
+ descriptorChildrenExportAuthorities.set(this, options._descriptorPreflightAuthority ?? null);
521
537
  this.warnedOwnership = new Set();
522
538
  this.warnedCompileDiagnostics = new Set();
523
539
  // Deliberately instance-scoped: separate projects/build environments must
@@ -933,8 +949,30 @@ class OctaneBundlerCompiler {
933
949
  return findStaticRuntimeImportRequests(code, this._canonicalModuleId(id));
934
950
  }
935
951
 
952
+ /** @internal Create a one-transform descriptor-export proof from a read-only AST. */
953
+ _prepareDescriptorChildrenExports(authority, source, id, ast) {
954
+ if (
955
+ authority === null ||
956
+ authority !== descriptorChildrenExportAuthorities.get(this) ||
957
+ typeof source !== 'string' ||
958
+ typeof id !== 'string' ||
959
+ ast === null ||
960
+ typeof ast !== 'object' ||
961
+ ast.type !== 'Program'
962
+ ) {
963
+ throw new TypeError('Invalid descriptor-children preflight input.');
964
+ }
965
+ const proof = Object.freeze({});
966
+ descriptorChildrenExportProofs.set(proof, {
967
+ source,
968
+ id,
969
+ exports: Object.freeze(findDescriptorChildrenExports(ast, id)),
970
+ });
971
+ return proof;
972
+ }
973
+
936
974
  /** CSS proof discovery uses the same ownership gate as the eventual compile. */
937
- findCssModuleImportRequests(code, id, environment = 'client') {
975
+ findCssModuleImportRequests(code, id, environment = 'client', parsedAst = null) {
938
976
  if (typeof code !== 'string' || !code.includes('.module.')) return [];
939
977
  // The live-read witness currently follows DOM host ownership only. Even
940
978
  // a DOM-owned module can delegate a JSX-valued prop to another renderer.
@@ -956,7 +994,7 @@ class OctaneBundlerCompiler {
956
994
  ) {
957
995
  return [];
958
996
  }
959
- return findCssModuleImportRequests(code, filename);
997
+ return findCssModuleImportRequests(parsedAst ?? code, filename);
960
998
  }
961
999
 
962
1000
  /**
@@ -1014,6 +1052,11 @@ class OctaneBundlerCompiler {
1014
1052
 
1015
1053
  transform(code, id, options = {}) {
1016
1054
  assertNativeReadOptions(options);
1055
+ const preparedDescriptorChildrenExports = consumeDescriptorChildrenExportProof(
1056
+ options._descriptorChildrenExportsProof,
1057
+ code,
1058
+ id,
1059
+ );
1017
1060
  const file = cleanModuleId(id);
1018
1061
  const hydrateBoundaryPath = hydrateBoundaryPathFromId(id);
1019
1062
  const collected = {
@@ -1199,7 +1242,10 @@ class OctaneBundlerCompiler {
1199
1242
  voidComponentExports: findVoidComponentExports(voidComponentAst, filename),
1200
1243
  }),
1201
1244
  ...(cssModuleConstantImports === undefined ? null : { cssModuleConstantImports }),
1202
- descriptorChildrenExports: findDescriptorChildrenExports(code, filename),
1245
+ descriptorChildrenExports:
1246
+ preparedDescriptorChildrenExports === null
1247
+ ? findDescriptorChildrenExports(code, filename)
1248
+ : [...preparedDescriptorChildrenExports],
1203
1249
  ...finishMetadata(collected),
1204
1250
  };
1205
1251
  }
@@ -151,12 +151,16 @@ export function createClientReferenceManifest(entries) {
151
151
  /** Return the runtime import/re-export requests that a bundler should resolve. */
152
152
  export function findStaticRuntimeImportRequests(source, filename = 'unknown') {
153
153
  let ast;
154
- try {
155
- ast = parseModule(source, filename);
156
- } catch {
157
- // The owning compiler/parser will report the useful syntax diagnostic. Import
158
- // classification is an adapter prepass and must not replace it.
159
- return [];
154
+ if (source && typeof source === 'object' && source.type === 'Program') {
155
+ ast = source;
156
+ } else {
157
+ try {
158
+ ast = parseModule(source, filename);
159
+ } catch {
160
+ // The owning compiler/parser will report the useful syntax diagnostic. Import
161
+ // classification is an adapter prepass and must not replace it.
162
+ return [];
163
+ }
160
164
  }
161
165
  const requests = new Set();
162
166
  for (const statement of ast.body ?? []) {
@@ -499,7 +499,7 @@ function owningComponentForReference(components, node) {
499
499
  }
500
500
 
501
501
  function ownerReachableComponentsAst(ast, localSpecializations) {
502
- const localNames = new Set(localSpecializations.components.keys());
502
+ const localNames = localSpecializations.components;
503
503
  const dependencies = new Map();
504
504
  const reachable = new Set(
505
505
  [...localSpecializations.exported].filter((name) => localNames.has(name)),
@@ -702,7 +702,7 @@ function collectSpecializationAstReplacements(node, cloneNames, runtime, aliases
702
702
  }
703
703
 
704
704
  function specializeLocalComponentsAst(region, index, state) {
705
- const localNames = new Set(state.localSpecializations.components.keys());
705
+ const localNames = state.localSpecializations.components;
706
706
  const selected = new Set([
707
707
  ...jsxComponentReferences(region, localNames),
708
708
  ...localCallReferences(region, localNames),
@@ -5340,38 +5340,70 @@ function classifyStableHookfulChildCalls(moduleBody, ctx) {
5340
5340
  });
5341
5341
  }
5342
5342
 
5343
- let changed = true;
5344
- while (changed) {
5345
- changed = false;
5346
- for (const [name, candidate] of candidates) {
5347
- for (const edge of candidate.dependencies) {
5348
- if (!candidates.has(tagBindingName(edge))) {
5349
- candidates.delete(name);
5350
- changed = true;
5351
- break;
5352
- }
5343
+ const dependents = new Map();
5344
+ const removalQueue = [];
5345
+ for (const [name, candidate] of candidates) {
5346
+ let invalid = false;
5347
+ for (const edge of candidate.dependencies) {
5348
+ const childName = tagBindingName(edge);
5349
+ if (!candidates.has(childName)) {
5350
+ invalid = true;
5351
+ continue;
5353
5352
  }
5353
+ let childDependents = dependents.get(childName);
5354
+ if (childDependents === undefined) {
5355
+ dependents.set(childName, (childDependents = []));
5356
+ }
5357
+ childDependents.push(name);
5354
5358
  }
5359
+ if (invalid) removalQueue.push(name);
5355
5360
  }
5356
- changed = true;
5357
- while (changed) {
5358
- changed = false;
5359
- for (const candidate of candidates.values()) {
5360
- for (const edge of candidate.dependencies) {
5361
- const child = candidates.get(tagBindingName(edge));
5362
- for (const capture of child.captures) {
5363
- if (!candidate.captures.has(capture)) {
5364
- candidate.captures.add(capture);
5365
- changed = true;
5366
- }
5367
- }
5368
- for (const publication of child.publications) {
5369
- if (!candidate.publications.has(publication)) {
5370
- candidate.publications.add(publication);
5371
- changed = true;
5372
- }
5373
- }
5374
- }
5361
+ for (let index = 0; index < removalQueue.length; index++) {
5362
+ const name = removalQueue[index];
5363
+ if (!candidates.delete(name)) continue;
5364
+ const parentNames = dependents.get(name);
5365
+ if (parentNames === undefined) continue;
5366
+ for (const parentName of parentNames) {
5367
+ if (candidates.has(parentName)) removalQueue.push(parentName);
5368
+ }
5369
+ }
5370
+
5371
+ const captureOwners = [];
5372
+ const captureValues = [];
5373
+ const publicationOwners = [];
5374
+ const publicationValues = [];
5375
+ for (const [name, candidate] of candidates) {
5376
+ for (const capture of candidate.captures) {
5377
+ captureOwners.push(name);
5378
+ captureValues.push(capture);
5379
+ }
5380
+ for (const publication of candidate.publications) {
5381
+ publicationOwners.push(name);
5382
+ publicationValues.push(publication);
5383
+ }
5384
+ }
5385
+ for (let index = 0; index < captureOwners.length; index++) {
5386
+ const parentNames = dependents.get(captureOwners[index]);
5387
+ if (parentNames === undefined) continue;
5388
+ const capture = captureValues[index];
5389
+ for (const parentName of parentNames) {
5390
+ const parent = candidates.get(parentName);
5391
+ if (parent === undefined || parent.captures.has(capture)) continue;
5392
+ parent.captures.add(capture);
5393
+ captureOwners.push(parentName);
5394
+ captureValues.push(capture);
5395
+ }
5396
+ }
5397
+ for (let index = 0; index < publicationOwners.length; index++) {
5398
+ const parentNames = dependents.get(publicationOwners[index]);
5399
+ if (parentNames === undefined) continue;
5400
+ const publication = publicationValues[index];
5401
+ for (const parentName of parentNames) {
5402
+ const parent = candidates.get(parentName);
5403
+ if (parent === undefined || parent.publications.has(publication)) continue;
5404
+ parent.publications.add(publication);
5405
+ publicationOwners.push(parentName);
5406
+ publicationValues.push(publication);
5375
5407
  }
5376
5408
  }
5377
5409
  for (const [name, candidate] of candidates) {
@@ -10,12 +10,16 @@ export function isPlainCssModuleId(id) {
10
10
 
11
11
  /** Only authored CSS-module imports can ask the host for this narrow proof. */
12
12
  export function findCssModuleImportRequests(source, id) {
13
- if (typeof source !== 'string' || !source.includes('.module.')) return [];
14
13
  let ast;
15
- try {
16
- ast = parseModule(source, id);
17
- } catch {
18
- return [];
14
+ if (source && typeof source === 'object' && source.type === 'Program') {
15
+ ast = source;
16
+ } else {
17
+ if (typeof source !== 'string' || !source.includes('.module.')) return [];
18
+ try {
19
+ ast = parseModule(source, id);
20
+ } catch {
21
+ return [];
22
+ }
19
23
  }
20
24
  const requests = new Set();
21
25
  for (const statement of ast.body ?? []) {
@@ -693,17 +693,38 @@ function visitJsxTag(name, visitIdentifier) {
693
693
  }
694
694
 
695
695
  /** Collect names whose values must cross from the parent module into a queried child. */
696
- function collectCaptures(nodes, importBindings, shadowedImports = new Set()) {
696
+ function collectCaptures(
697
+ nodes,
698
+ importBindings,
699
+ shadowedImports = new Set(),
700
+ additionalBindings = null,
701
+ ) {
697
702
  const captures = new Set();
698
703
  const scopes = [];
699
704
  const seen = new WeakSet();
700
- const isBound = (name) => {
701
- if (importBindings.has(name) && !shadowedImports.has(name)) return true;
702
- for (let index = scopes.length - 1; index >= 0; index--) {
703
- if (scopes[index].has(name)) return true;
704
- }
705
- return false;
706
- };
705
+ // Module slicing can consult a second binding set without materializing its
706
+ // union for every boundary. Preserve the single-set path for all other walks.
707
+ const isBound =
708
+ additionalBindings === null
709
+ ? (name) => {
710
+ if (importBindings.has(name) && !shadowedImports.has(name)) return true;
711
+ for (let index = scopes.length - 1; index >= 0; index--) {
712
+ if (scopes[index].has(name)) return true;
713
+ }
714
+ return false;
715
+ }
716
+ : (name) => {
717
+ if (
718
+ (importBindings.has(name) || additionalBindings.has(name)) &&
719
+ !shadowedImports.has(name)
720
+ ) {
721
+ return true;
722
+ }
723
+ for (let index = scopes.length - 1; index >= 0; index--) {
724
+ if (scopes[index].has(name)) return true;
725
+ }
726
+ return false;
727
+ };
707
728
  const reference = (node) => {
708
729
  if (node?.name && !isBound(node.name)) captures.add(node.name);
709
730
  };
@@ -976,8 +997,31 @@ function privateModuleDeclarationGraph(ast, importBindings) {
976
997
  return { byBinding, records };
977
998
  }
978
999
 
1000
+ function sortedBoundaryRanges(boundaries) {
1001
+ return boundaries
1002
+ .map((boundary) => ({ end: boundary.node.end, start: boundary.node.start }))
1003
+ .sort((left, right) => left.start - right.start || left.end - right.end);
1004
+ }
1005
+
1006
+ function declarationContainsBoundary(node, boundaryRanges) {
1007
+ let low = 0;
1008
+ let high = boundaryRanges.length;
1009
+ while (low < high) {
1010
+ const middle = (low + high) >>> 1;
1011
+ if (boundaryRanges[middle].start < node.start) low = middle + 1;
1012
+ else high = middle;
1013
+ }
1014
+ for (let index = low; index < boundaryRanges.length; index++) {
1015
+ const boundary = boundaryRanges[index];
1016
+ if (boundary.start > node.end) break;
1017
+ if (boundary.end <= node.end) return true;
1018
+ }
1019
+ return false;
1020
+ }
1021
+
979
1022
  function movableModuleDeclarations(analysis) {
980
1023
  const records = [];
1024
+ let boundaryRanges = null;
981
1025
  const moduleBindings = topLevelBindingNames(analysis.ast);
982
1026
  const exportedBindings = localExportBindings(analysis.ast);
983
1027
  for (const node of analysis.ast.body ?? []) {
@@ -994,11 +1038,8 @@ function movableModuleDeclarations(analysis) {
994
1038
  // A declaration containing its own Hydrate site needs another extraction
995
1039
  // pass after it moves. Keep that uncommon declaration in the parent until
996
1040
  // recursive declaration slicing has an explicit protocol of its own.
997
- if (
998
- analysis.boundaries.some(
999
- (boundary) => boundary.node.start >= node.start && boundary.node.end <= node.end,
1000
- )
1001
- ) {
1041
+ boundaryRanges ??= sortedBoundaryRanges(analysis.boundaries);
1042
+ if (declarationContainsBoundary(node, boundaryRanges)) {
1002
1043
  continue;
1003
1044
  }
1004
1045
  const bindings = declarationBindingSet(node);
@@ -1007,6 +1048,7 @@ function movableModuleDeclarations(analysis) {
1007
1048
  records.push({
1008
1049
  bindings,
1009
1050
  dependencies: new Set(),
1051
+ order: records.length,
1010
1052
  retainedDependencies: new Set(),
1011
1053
  node,
1012
1054
  });
@@ -1123,8 +1165,12 @@ function hydrateBoundaryElement(
1123
1165
  if (boundary.disabled) return node;
1124
1166
  assertDirectChildren(boundary, boundary.filename);
1125
1167
  validateBoundary(boundary, boundary.filename, boundary.hookNames);
1126
- const availableBindings = new Set([...importBindings, ...additionalModuleBindings]);
1127
- const captures = collectCaptures(node.children, availableBindings, boundary.shadowedImports);
1168
+ const captures = collectCaptures(
1169
+ node.children,
1170
+ importBindings,
1171
+ boundary.shadowedImports,
1172
+ additionalModuleBindings.size === 0 ? null : additionalModuleBindings,
1173
+ );
1128
1174
  const attributes = [
1129
1175
  ...(opening.attributes ?? []),
1130
1176
  jsxExpressionAttribute('__load', hydrateLoaderExpression(boundary, request), opening),
@@ -1222,8 +1268,9 @@ function extractedModuleAst(
1222
1268
  const moduleBindings = moduleBindingsByPath.get(boundary.path) ?? new Set();
1223
1269
  const captures = collectCaptures(
1224
1270
  boundary.node.children,
1225
- new Set([...analysis.imports.importBindings, ...moduleBindings]),
1271
+ analysis.imports.importBindings,
1226
1272
  boundary.shadowedImports,
1273
+ moduleBindings.size === 0 ? null : moduleBindings,
1227
1274
  );
1228
1275
  const pathName = boundary.path.replace(/[^A-Za-z0-9_$]/g, '_');
1229
1276
  const componentName = uniqueGeneratedName(source, `__OctaneHydrateBoundary_${pathName}`);
@@ -1276,7 +1323,35 @@ function extractedModuleAst(
1276
1323
  return pruneUnusedImportsAst(program, false);
1277
1324
  }
1278
1325
 
1279
- function createModuleMovePlanAst(source, analysis, request) {
1326
+ function moduleReferencesForBoundary(analysis, boundary, request, moduleBindingsByPath) {
1327
+ assertDirectChildren(boundary, boundary.filename);
1328
+ validateBoundary(boundary, boundary.filename, analysis.imports.hookNames);
1329
+ const collectModuleReferences = (nodes) =>
1330
+ collectCaptures(nodes, analysis.imports.importBindings).filter(
1331
+ (name) => !boundary.shadowedImports.has(name),
1332
+ );
1333
+ if (boundary.children.length === 0) {
1334
+ return collectModuleReferences(boundary.node.children);
1335
+ }
1336
+ const nestedAnalysis = {
1337
+ ...analysis,
1338
+ roots: boundary.children,
1339
+ };
1340
+ const fragment = transformHydrateAst(
1341
+ {
1342
+ type: 'Program',
1343
+ sourceType: 'module',
1344
+ body: boundary.node.children,
1345
+ metadata: { path: [] },
1346
+ },
1347
+ nestedAnalysis,
1348
+ request,
1349
+ moduleBindingsByPath,
1350
+ );
1351
+ return collectModuleReferences(fragment.body);
1352
+ }
1353
+
1354
+ function createModuleMovePlanAst(analysis, request) {
1280
1355
  const candidates = movableModuleDeclarations(analysis);
1281
1356
  if (candidates.records.length === 0) {
1282
1357
  return {
@@ -1340,13 +1415,9 @@ function createModuleMovePlanAst(source, analysis, request) {
1340
1415
  const referencesByPath = new Map();
1341
1416
  for (const boundary of analysis.boundaries) {
1342
1417
  if (boundary.disabled || hasPermanentStaticAncestor(boundary)) continue;
1343
- const childAst = extractedModuleAst(source, analysis, boundary, request, allBindingsByPath);
1344
1418
  referencesByPath.set(
1345
1419
  boundary.path,
1346
- collectCaptures(
1347
- (childAst.body ?? []).filter((node) => node.type !== 'ImportDeclaration'),
1348
- analysis.imports.importBindings,
1349
- ),
1420
+ moduleReferencesForBoundary(analysis, boundary, request, allBindingsByPath),
1350
1421
  );
1351
1422
  }
1352
1423
  const recordsForReferences = (childReferences) => {
@@ -1371,8 +1442,11 @@ function createModuleMovePlanAst(source, analysis, request) {
1371
1442
  return records;
1372
1443
  };
1373
1444
  const preliminaryCounts = new Map();
1374
- for (const references of referencesByPath.values()) {
1375
- for (const record of recordsForReferences(references)) {
1445
+ const preliminaryRecordsByPath = new Map();
1446
+ for (const [path, references] of referencesByPath) {
1447
+ const records = recordsForReferences(references);
1448
+ preliminaryRecordsByPath.set(path, records);
1449
+ for (const record of records) {
1376
1450
  preliminaryCounts.set(record, (preliminaryCounts.get(record) ?? 0) + 1);
1377
1451
  }
1378
1452
  }
@@ -1383,9 +1457,12 @@ function createModuleMovePlanAst(source, analysis, request) {
1383
1457
  const movedRecords = new Set();
1384
1458
  for (const boundary of analysis.boundaries) {
1385
1459
  if (boundary.disabled || hasPermanentStaticAncestor(boundary)) continue;
1386
- const records = recordsForReferences(referencesByPath.get(boundary.path) ?? []);
1387
- if (records.size === 0) continue;
1388
- const ordered = candidates.records.filter((record) => records.has(record));
1460
+ const records = preliminaryRecordsByPath.get(boundary.path);
1461
+ if (records === undefined) continue;
1462
+ const ordered = [...records]
1463
+ .filter((record) => !eagerRecords.has(record))
1464
+ .sort((left, right) => left.order - right.order);
1465
+ if (ordered.length === 0) continue;
1389
1466
  bindingsByPath.set(boundary.path, new Set(ordered.flatMap((record) => [...record.bindings])));
1390
1467
  declarationsByPath.set(boundary.path, ordered);
1391
1468
  for (const record of ordered) movedRecords.add(record);
@@ -1532,7 +1609,7 @@ export function prepareHydrateBoundaries(source, filename, boundaryPath = null,
1532
1609
  boundary.hookNames = analysis.imports.hookNames;
1533
1610
  }
1534
1611
  const request = sameSourceRequest(filename);
1535
- const moduleMovePlan = createModuleMovePlanAst(source, analysis, request);
1612
+ const moduleMovePlan = createModuleMovePlanAst(analysis, request);
1536
1613
  const permanentStaticRemoved =
1537
1614
  boundaryPath === null
1538
1615
  ? createPermanentStaticRemovalPlanAst(analysis, request, moduleMovePlan)
@@ -232,10 +232,14 @@ function collectVoidJsxImportCandidates(ast) {
232
232
  */
233
233
  export function findVoidComponentImports(source, id) {
234
234
  let ast;
235
- try {
236
- ast = parseModule(source, id);
237
- } catch {
238
- return [];
235
+ if (source && typeof source === 'object' && source.type === 'Program') {
236
+ ast = source;
237
+ } else {
238
+ try {
239
+ ast = parseModule(source, id);
240
+ } catch {
241
+ return [];
242
+ }
239
243
  }
240
244
  const unique = new Map();
241
245
  for (const candidate of [
@@ -16,6 +16,7 @@
16
16
  import * as nodeCrypto from 'node:crypto';
17
17
  import * as nodeFs from 'node:fs';
18
18
  import * as nodePath from 'node:path';
19
+ import { parseModule } from '@tsrx/core';
19
20
  import {
20
21
  CLIENT_REFERENCE_MANIFEST_FILENAME,
21
22
  cleanModuleId,
@@ -38,6 +39,7 @@ const PROFILE_DEFINE = '__OCTANE_PROFILE_ENABLED__';
38
39
  const VOID_EXPORTS_META = 'octane:void-component-exports';
39
40
  const DESCRIPTOR_CHILDREN_EXPORTS_META = 'octane:descriptor-children-exports';
40
41
  const CLIENT_REFERENCE_META = 'octane:client-reference';
42
+ const DESCRIPTOR_PREFLIGHT_AUTHORITY = Symbol('octane.descriptor-preflight');
41
43
 
42
44
  function realRoot(path) {
43
45
  try {
@@ -491,9 +493,8 @@ async function loadDescriptorChildrenImports(
491
493
  return proven;
492
494
  }
493
495
 
494
- async function loadClientOnlyImports(context, compiler, code, importer) {
496
+ async function loadClientOnlyImports(context, compiler, requests, importer) {
495
497
  if (typeof context.resolve !== 'function') return [];
496
- const requests = compiler.findServerImportRequests(code, importer);
497
498
  const classified = [];
498
499
  await Promise.all(
499
500
  requests.map(async (request) => {
@@ -611,6 +612,7 @@ export function octane(options = {}) {
611
612
  // virtual module until its own transform publishes metadata.
612
613
  let allowDescriptorGraphLoad = true;
613
614
  let compiler = createOctaneCompiler({
615
+ _descriptorPreflightAuthority: DESCRIPTOR_PREFLIGHT_AUTHORITY,
614
616
  root: projectRoot,
615
617
  exclude: options.exclude,
616
618
  profile: profileEnabled,
@@ -630,6 +632,7 @@ export function octane(options = {}) {
630
632
  cssModuleProofStates.clear();
631
633
  projectRoot = nodePath.resolve(root);
632
634
  compiler = createOctaneCompiler({
635
+ _descriptorPreflightAuthority: DESCRIPTOR_PREFLIGHT_AUTHORITY,
633
636
  root: projectRoot,
634
637
  exclude: options.exclude,
635
638
  profile: profileEnabled,
@@ -752,9 +755,47 @@ export function octane(options = {}) {
752
755
  ? forceSsr
753
756
  : transformOptions?.ssr === true || this.environment?.config?.consumer === 'server';
754
757
  const environment = server ? 'server' : 'client';
755
- const cssRequests = specializeCssModuleConstants
756
- ? compiler.findCssModuleImportRequests(code, id, environment)
757
- : [];
758
+ // Parse authored source once, synchronously classify every applicable
759
+ // adapter concern, then let the AST fall out of scope before any graph
760
+ // loading begins. Compilation keeps its independent authoritative parser.
761
+ const preflight = (() => {
762
+ const authoredSource = code;
763
+ let ast;
764
+ try {
765
+ ast = parseModule(authoredSource, id);
766
+ } catch {
767
+ // Every adapter classifier uses this exact parser/source/id and would
768
+ // return an empty result after repeating the same failed parse. Preserve
769
+ // those facts without the retries. The compiler receives no descriptor
770
+ // proof, so its canonical-id fallback and authoritative diagnostics stay
771
+ // unchanged.
772
+ return {
773
+ cssRequests: [],
774
+ descriptorExportsProof: null,
775
+ descriptorImports: [],
776
+ serverImportRequests: [],
777
+ voidImports: [],
778
+ };
779
+ }
780
+ return {
781
+ cssRequests: specializeCssModuleConstants
782
+ ? compiler.findCssModuleImportRequests(code, id, environment, ast)
783
+ : [],
784
+ descriptorExportsProof: compiler._prepareDescriptorChildrenExports(
785
+ DESCRIPTOR_PREFLIGHT_AUTHORITY,
786
+ code,
787
+ id,
788
+ ast,
789
+ ),
790
+ descriptorImports: findDescriptorChildrenImports(ast, id),
791
+ serverImportRequests: server ? compiler.findServerImportRequests(ast, id) : [],
792
+ voidImports:
793
+ specializeProductionRoots && !server && !hmrEnabled && !profileEnabled
794
+ ? findVoidComponentImports(ast, id)
795
+ : [],
796
+ };
797
+ })();
798
+ const cssRequests = preflight.cssRequests;
758
799
  const loadCssImports = () =>
759
800
  loadCssModuleImports(
760
801
  this,
@@ -774,6 +815,9 @@ export function octane(options = {}) {
774
815
  .filter((key) => key.startsWith('export\0'))
775
816
  .map((key) => key.slice('export\0'.length));
776
817
  const result = compiler.transform(code, id, {
818
+ ...(preflight.descriptorExportsProof === null
819
+ ? null
820
+ : { _descriptorChildrenExportsProof: preflight.descriptorExportsProof }),
777
821
  environment,
778
822
  hmr: !server && hmrEnabled ? 'vite' : false,
779
823
  // DEV server transforms also carry SSR-only diagnostics. HMR itself
@@ -849,11 +893,11 @@ export function octane(options = {}) {
849
893
  };
850
894
 
851
895
  if (server) {
852
- const descriptorImports = findDescriptorChildrenImports(code, id).filter(
896
+ const descriptorImports = preflight.descriptorImports.filter(
853
897
  (candidate) => candidate.local !== undefined || !nodeFs.existsSync(cleanModuleId(id)),
854
898
  );
855
899
  return Promise.all([
856
- loadClientOnlyImports(this, compiler, code, id),
900
+ loadClientOnlyImports(this, compiler, preflight.serverImportRequests, id),
857
901
  loadDescriptorChildrenImports(
858
902
  this,
859
903
  descriptorImports,
@@ -871,9 +915,9 @@ export function octane(options = {}) {
871
915
 
872
916
  const voidImports =
873
917
  specializeProductionRoots && !server && !hmrEnabled && !profileEnabled
874
- ? findVoidComponentImports(code, id)
918
+ ? preflight.voidImports
875
919
  : [];
876
- const descriptorImports = findDescriptorChildrenImports(code, id).filter(
920
+ const descriptorImports = preflight.descriptorImports.filter(
877
921
  (candidate) => candidate.local !== undefined || !nodeFs.existsSync(cleanModuleId(id)),
878
922
  );
879
923
  if (voidImports.length === 0 && descriptorImports.length === 0 && cssRequests.length === 0) {
package/dist/runtime.js CHANGED
@@ -4071,29 +4071,53 @@ function __useStateWithGetter(initial, slot) {
4071
4071
  let HIDDEN_REVEAL_ACTIONS = null;
4072
4072
  function deferHiddenRevealAction(boundary, action) {
4073
4073
  const deferred = HIDDEN_REVEAL_ACTIONS ??= /* @__PURE__ */ new WeakMap();
4074
- const actions = deferred.get(boundary);
4075
- if (actions === void 0) deferred.set(boundary, [action]);
4076
- else actions.push(action);
4074
+ let queue = deferred.get(boundary);
4075
+ if (queue === void 0) {
4076
+ queue = { entries: [], live: 0 };
4077
+ deferred.set(boundary, queue);
4078
+ }
4079
+ const entry = { action, active: true, queue };
4080
+ queue.entries.push(entry);
4081
+ queue.live++;
4082
+ return entry;
4083
+ }
4084
+ function claimHiddenRevealAction(boundary, entry) {
4085
+ const queue = entry.queue;
4086
+ if (HIDDEN_REVEAL_ACTIONS?.get(boundary) !== queue) return null;
4087
+ if (!entry.active) return null;
4088
+ entry.active = false;
4089
+ if (--queue.live === 0) HIDDEN_REVEAL_ACTIONS.delete(boundary);
4090
+ return entry.action;
4091
+ }
4092
+ function cancelHiddenRevealAction(boundary, entry) {
4093
+ const queue = entry.queue;
4094
+ if (claimHiddenRevealAction(boundary, entry) === null || queue.live === 0) return;
4095
+ if (queue.entries.length > queue.live * 2) {
4096
+ queue.entries = queue.entries.filter((candidate) => candidate.active);
4097
+ }
4077
4098
  }
4078
4099
  function publishHiddenRevealActions(boundary) {
4079
- const actions = HIDDEN_REVEAL_ACTIONS?.get(boundary);
4080
- if (actions === void 0) return;
4100
+ const queue = HIDDEN_REVEAL_ACTIONS?.get(boundary);
4101
+ if (queue === void 0) return;
4102
+ const entries = queue.entries;
4081
4103
  if (boundary.__kind === "activityBlockSlot") {
4082
- for (let i = 0; i < actions.length; i++) {
4083
- const action = actions[i];
4104
+ const end = entries.length;
4105
+ for (let i = 0; i < end; i++) {
4106
+ const entry = entries[i];
4107
+ if (!entry.active) continue;
4084
4108
  enqueueEffectEventCommitAction(() => {
4085
- if (HIDDEN_REVEAL_ACTIONS?.get(boundary) !== actions) return;
4086
- const index = actions.indexOf(action);
4087
- if (index === -1) return;
4088
- actions.splice(index, 1);
4089
- if (actions.length === 0) HIDDEN_REVEAL_ACTIONS.delete(boundary);
4090
- return action();
4109
+ const claimed = claimHiddenRevealAction(boundary, entry);
4110
+ if (claimed === null) return;
4111
+ return claimed();
4091
4112
  });
4092
4113
  }
4093
4114
  return;
4094
4115
  }
4095
4116
  HIDDEN_REVEAL_ACTIONS.delete(boundary);
4096
- for (let i = 0; i < actions.length; i++) enqueueEffectEventCommitAction(actions[i]);
4117
+ for (let i = 0; i < entries.length; i++) {
4118
+ const entry = entries[i];
4119
+ if (entry.active) enqueueEffectEventCommitAction(entry.action);
4120
+ }
4097
4121
  }
4098
4122
  function useLinkedState(source, reconcile, options, slot) {
4099
4123
  if (typeof options === "symbol") {
@@ -5167,10 +5191,10 @@ function beginHydratePreload(state) {
5167
5191
  return promise;
5168
5192
  }
5169
5193
  function resolveHydrateWaiters(state, reason) {
5170
- if (state.hydrationWaiters.size === 0) return;
5171
- const waiters = [...state.hydrationWaiters];
5172
- state.hydrationWaiters.clear();
5173
- for (let i = 0; i < waiters.length; i++) waiters[i](reason);
5194
+ const waiters = state.hydrationWaiters;
5195
+ if (waiters === null) return;
5196
+ state.hydrationWaiters = null;
5197
+ for (const waiter of waiters) waiter(reason);
5174
5198
  }
5175
5199
  function waitForHydratePrefetchStrategy(state, strategy) {
5176
5200
  const controller = state.prefetchAbort;
@@ -5184,13 +5208,13 @@ function waitForHydratePrefetchStrategy(state, strategy) {
5184
5208
  if (settled) return;
5185
5209
  settled = true;
5186
5210
  cleanup?.();
5187
- state.hydrationWaiters.delete(onHydrate);
5211
+ state.hydrationWaiters?.delete(onHydrate);
5188
5212
  signal.removeEventListener("abort", onAbort);
5189
5213
  resolve(reason);
5190
5214
  };
5191
5215
  const onHydrate = () => finish("hydrate");
5192
5216
  const onAbort = () => finish("abort");
5193
- state.hydrationWaiters.add(onHydrate);
5217
+ (state.hydrationWaiters ??= /* @__PURE__ */ new Set()).add(onHydrate);
5194
5218
  signal.addEventListener("abort", onAbort, { once: true });
5195
5219
  cleanup = strategy._s?.({
5196
5220
  element: state.wrapper,
@@ -5590,7 +5614,7 @@ function createHydrateSlot(props, scope, boundaryId) {
5590
5614
  prefetchPromise: null,
5591
5615
  preloadPromise: null,
5592
5616
  loadedBody: null,
5593
- hydrationWaiters: /* @__PURE__ */ new Set(),
5617
+ hydrationWaiters: null,
5594
5618
  activationGeneration: 0,
5595
5619
  activationRequested: !serverPreserved,
5596
5620
  activationReady: !serverPreserved,
@@ -19309,28 +19333,23 @@ function enqueueInlineCaughtError(state) {
19309
19333
  const handler = rootErrorHandlersFor(block)?.onCaughtError;
19310
19334
  if (handler === void 0) return;
19311
19335
  const error = state.err;
19312
- let parkedOwner = null;
19336
+ let parked = null;
19313
19337
  let cleanupRegistered = false;
19314
19338
  const action = (owner2) => {
19315
- parkedOwner = null;
19339
+ parked = null;
19316
19340
  if (block.disposed || state.block !== block) return;
19317
19341
  const hidden = owner2 ?? findScheduledVisibilityOwner(block, true);
19318
19342
  if (hidden !== null) {
19319
- parkedOwner = hidden;
19320
19343
  if (!cleanupRegistered) {
19321
19344
  cleanupRegistered = true;
19322
19345
  (block.cleanups ??= []).push(() => {
19323
- const owner3 = parkedOwner;
19324
- parkedOwner = null;
19325
- if (owner3 === null) return;
19326
- const actions = HIDDEN_REVEAL_ACTIONS?.get(owner3);
19327
- if (actions === void 0) return;
19328
- const index = actions.indexOf(action);
19329
- if (index !== -1) actions.splice(index, 1);
19330
- if (actions.length === 0) HIDDEN_REVEAL_ACTIONS.delete(owner3);
19346
+ const pending = parked;
19347
+ parked = null;
19348
+ if (pending === null) return;
19349
+ cancelHiddenRevealAction(pending.owner, pending.entry);
19331
19350
  });
19332
19351
  }
19333
- deferHiddenRevealAction(hidden, action);
19352
+ parked = { owner: hidden, entry: deferHiddenRevealAction(hidden, action) };
19334
19353
  return;
19335
19354
  }
19336
19355
  return { state, block, error, handler, resume: action };
@@ -332,10 +332,14 @@ class ScopeImpl {
332
332
  type,
333
333
  ...node ? { key: node.key, revision: node.revision } : {}
334
334
  };
335
- if (this.events.length === this.traceLimit) this.events.shift();
336
- this.events.push(event);
335
+ if (this.events.length === this.traceLimit) {
336
+ this.events[(event.sequence - 1) % this.traceLimit] = event;
337
+ } else {
338
+ this.events.push(event);
339
+ }
337
340
  }
338
341
  inspect() {
342
+ const traceStart = this.traceLimit && this.events.length === this.traceLimit ? this.sequence % this.traceLimit : 0;
339
343
  return {
340
344
  scopeKey: this.scopeKey,
341
345
  epoch: this.epoch,
@@ -364,7 +368,9 @@ class ScopeImpl {
364
368
  dependencies
365
369
  };
366
370
  }),
367
- trace: this.events.map((event) => ({ ...event }))
371
+ trace: this.events.map((event, index) => ({
372
+ ...traceStart ? this.events[(traceStart + index) % this.events.length] : event
373
+ }))
368
374
  };
369
375
  }
370
376
  dispose() {
@@ -8914,6 +8914,7 @@ function createObjectDriver(renderer = "object") {
8914
8914
  );
8915
8915
  }
8916
8916
  const state = container[OBJECT_DRIVER_STATE];
8917
+ const useBatchedDetaches = batch.commands.length >= 4096;
8917
8918
  const simulated = /* @__PURE__ */ new Map();
8918
8919
  const destroyed = /* @__PURE__ */ new Set();
8919
8920
  const readSimulated = (id) => {
@@ -8946,27 +8947,66 @@ function createObjectDriver(renderer = "object") {
8946
8947
  const DETACHED = /* @__PURE__ */ Symbol("octane.object-driver.detached");
8947
8948
  const parentChanges = /* @__PURE__ */ new Map();
8948
8949
  let rootChildren = null;
8950
+ let pendingDetaches = null;
8949
8951
  const readParent = (id) => {
8950
8952
  if (parentChanges.has(id)) return parentChanges.get(id);
8951
8953
  return state.parents.has(id) ? state.parents.get(id) : DETACHED;
8952
8954
  };
8955
+ const flushPendingDetaches = (parent, children) => {
8956
+ const detached = pendingDetaches?.get(parent);
8957
+ if (detached === void 0) return;
8958
+ let write = 0;
8959
+ for (let read = 0; read < children.length; read++) {
8960
+ const child = children[read];
8961
+ if (!detached.delete(child)) children[write++] = child;
8962
+ }
8963
+ if (detached.size !== 0) {
8964
+ const missing = detached.values().next().value;
8965
+ throw new Error(`Object driver: child ${missing} is not attached.`);
8966
+ }
8967
+ children.length = write;
8968
+ pendingDetaches.delete(parent);
8969
+ if (pendingDetaches.size === 0) pendingDetaches = null;
8970
+ };
8953
8971
  const simulatedChildren = (parent) => {
8972
+ let children;
8954
8973
  if (parent === null) {
8955
- return rootChildren ??= container.children.map((child) => child.id);
8974
+ children = rootChildren ??= container.children.map((child) => child.id);
8975
+ } else {
8976
+ const value = readSimulated(parent);
8977
+ if (value === void 0) throw new Error(`Object driver: unknown parent ${parent}.`);
8978
+ children = value.children;
8956
8979
  }
8957
- const value = readSimulated(parent);
8958
- if (value === void 0) throw new Error(`Object driver: unknown parent ${parent}.`);
8959
- return value.children;
8980
+ flushPendingDetaches(parent, children);
8981
+ return children;
8960
8982
  };
8961
- const detachSimulated = (id) => {
8983
+ const detachSimulated = (id, defer) => {
8962
8984
  const parent = readParent(id);
8963
8985
  if (parent === DETACHED) return;
8964
- const children = simulatedChildren(parent);
8965
- const index = children.indexOf(id);
8966
- if (index === -1) throw new Error(`Object driver: child ${id} is not attached.`);
8967
- children.splice(index, 1);
8986
+ if (useBatchedDetaches && defer) {
8987
+ const detaches = pendingDetaches ??= /* @__PURE__ */ new Map();
8988
+ const detached = detaches.get(parent);
8989
+ if (detached === void 0) detaches.set(parent, /* @__PURE__ */ new Set([id]));
8990
+ else detached.add(id);
8991
+ } else {
8992
+ const children = simulatedChildren(parent);
8993
+ const index = children.indexOf(id);
8994
+ if (index === -1) throw new Error(`Object driver: child ${id} is not attached.`);
8995
+ children.splice(index, 1);
8996
+ }
8968
8997
  parentChanges.set(id, DETACHED);
8969
8998
  };
8999
+ const forgetPendingDetachParent = (parent) => {
9000
+ if (pendingDetaches === null) return;
9001
+ pendingDetaches.delete(parent);
9002
+ if (pendingDetaches.size === 0) pendingDetaches = null;
9003
+ };
9004
+ const flushAllPendingDetaches = () => {
9005
+ while (pendingDetaches !== null) {
9006
+ const parent = pendingDetaches.keys().next().value;
9007
+ simulatedChildren(parent);
9008
+ }
9009
+ };
8970
9010
  for (const command of batch.commands) {
8971
9011
  if (command.op === "create") {
8972
9012
  if (hasSimulated(command.id))
@@ -9048,7 +9088,7 @@ function createObjectDriver(renderer = "object") {
9048
9088
  }
9049
9089
  if (!hasSimulated(command.id))
9050
9090
  throw new Error(`Object driver: unknown child ${command.id}.`);
9051
- detachSimulated(command.id);
9091
+ detachSimulated(command.id, false);
9052
9092
  const children = simulatedChildren(command.parent);
9053
9093
  const before = command.before === null ? children.length : children.indexOf(command.before);
9054
9094
  if (before === -1) throw new Error(`Object driver: unknown before id ${command.before}.`);
@@ -9065,11 +9105,10 @@ function createObjectDriver(renderer = "object") {
9065
9105
  if (command.parent !== null && typeof command.parent !== "number") {
9066
9106
  throw new Error("Object driver does not support portal target parents.");
9067
9107
  }
9068
- const children = simulatedChildren(command.parent);
9069
- const index = children.indexOf(command.id);
9070
- if (index === -1) throw new Error(`Object driver: child ${command.id} is not attached.`);
9071
- children.splice(index, 1);
9072
- parentChanges.set(command.id, DETACHED);
9108
+ if (readParent(command.id) !== command.parent) {
9109
+ throw new Error(`Object driver: child ${command.id} is not attached.`);
9110
+ }
9111
+ detachSimulated(command.id, true);
9073
9112
  for (const type of readSimulated(command.id).localCallbacks.keys()) {
9074
9113
  cleanupKeys.add(keyFor(command.id, type));
9075
9114
  }
@@ -9077,12 +9116,14 @@ function createObjectDriver(renderer = "object") {
9077
9116
  const instance = readSimulated(command.id);
9078
9117
  if (instance === void 0)
9079
9118
  throw new Error(`Object driver: unknown destroy ${command.id}.`);
9080
- detachSimulated(command.id);
9119
+ detachSimulated(command.id, true);
9081
9120
  for (const child of instance.children) parentChanges.set(child, DETACHED);
9082
9121
  instance.children.length = 0;
9122
+ forgetPendingDetachParent(command.id);
9083
9123
  destroyed.add(command.id);
9084
9124
  }
9085
9125
  }
9126
+ flushAllPendingDetaches();
9086
9127
  for (const [id, instance] of stagedInstances) {
9087
9128
  const staged = readSimulated(id);
9088
9129
  if (staged === void 0) continue;
@@ -9097,16 +9138,48 @@ function createObjectDriver(renderer = "object") {
9097
9138
  }
9098
9139
  let status = "prepared";
9099
9140
  let acceptedCallbacksRan = false;
9100
- const liveChildren = (parent) => objectChildren(container, parent, state.instances);
9101
- const detachLive = (id) => {
9141
+ let pendingLiveDetaches = null;
9142
+ const liveChildren = (parent) => {
9143
+ const children = objectChildren(container, parent, state.instances);
9144
+ const detached = pendingLiveDetaches?.get(parent);
9145
+ if (detached === void 0) return children;
9146
+ let write = 0;
9147
+ for (let read = 0; read < children.length; read++) {
9148
+ const child = children[read];
9149
+ if (!detached.has(child.id)) children[write++] = child;
9150
+ }
9151
+ children.length = write;
9152
+ pendingLiveDetaches.delete(parent);
9153
+ if (pendingLiveDetaches.size === 0) pendingLiveDetaches = null;
9154
+ return children;
9155
+ };
9156
+ const detachLive = (id, defer) => {
9102
9157
  if (!state.parents.has(id)) return;
9103
9158
  const parent = state.parents.get(id);
9104
9159
  const instance = state.instances.get(id);
9105
- const children = liveChildren(parent);
9106
- const index = children.indexOf(instance);
9107
- if (index !== -1) children.splice(index, 1);
9160
+ if (useBatchedDetaches && defer) {
9161
+ const detaches = pendingLiveDetaches ??= /* @__PURE__ */ new Map();
9162
+ const detached = detaches.get(parent);
9163
+ if (detached === void 0) detaches.set(parent, /* @__PURE__ */ new Set([id]));
9164
+ else detached.add(id);
9165
+ } else {
9166
+ const children = liveChildren(parent);
9167
+ const index = children.indexOf(instance);
9168
+ if (index !== -1) children.splice(index, 1);
9169
+ }
9108
9170
  state.parents.delete(id);
9109
9171
  };
9172
+ const forgetPendingLiveParent = (parent) => {
9173
+ if (pendingLiveDetaches === null) return;
9174
+ pendingLiveDetaches.delete(parent);
9175
+ if (pendingLiveDetaches.size === 0) pendingLiveDetaches = null;
9176
+ };
9177
+ const flushAllPendingLiveDetaches = () => {
9178
+ while (pendingLiveDetaches !== null) {
9179
+ const parent = pendingLiveDetaches.keys().next().value;
9180
+ liveChildren(parent);
9181
+ }
9182
+ };
9110
9183
  return {
9111
9184
  apply() {
9112
9185
  if (status !== "prepared") return;
@@ -9163,7 +9236,7 @@ function createObjectDriver(renderer = "object") {
9163
9236
  throw new Error("Object driver does not support portal target parents.");
9164
9237
  }
9165
9238
  const instance = state.instances.get(command.id);
9166
- detachLive(command.id);
9239
+ detachLive(command.id, false);
9167
9240
  const children = liveChildren(command.parent);
9168
9241
  const before = command.before === null ? children.length : children.indexOf(state.instances.get(command.before));
9169
9242
  children.splice(before, 0, instance);
@@ -9172,14 +9245,13 @@ function createObjectDriver(renderer = "object") {
9172
9245
  if (command.parent !== null && typeof command.parent !== "number") {
9173
9246
  throw new Error("Object driver does not support portal target parents.");
9174
9247
  }
9175
- const children = liveChildren(command.parent);
9176
- children.splice(children.indexOf(state.instances.get(command.id)), 1);
9177
- state.parents.delete(command.id);
9248
+ detachLive(command.id, true);
9178
9249
  } else if (command.op === "destroy") {
9179
9250
  const instance = state.instances.get(command.id);
9180
- detachLive(command.id);
9251
+ detachLive(command.id, true);
9181
9252
  for (const child of instance.children) state.parents.delete(child.id);
9182
9253
  instance.children.length = 0;
9254
+ forgetPendingLiveParent(command.id);
9183
9255
  state.instances.delete(command.id);
9184
9256
  state.parents.delete(command.id);
9185
9257
  state.events.delete(command.id);
@@ -9188,6 +9260,7 @@ function createObjectDriver(renderer = "object") {
9188
9260
  state.localCleanups.delete(command.id);
9189
9261
  }
9190
9262
  }
9263
+ flushAllPendingLiveDetaches();
9191
9264
  container.commits.push(batch);
9192
9265
  });
9193
9266
  runCommitTasks(tasks);
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "0.1.50";
1
+ const version = "0.1.51";
2
2
  export {
3
3
  version
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octane",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": [