From d86dc83d656a26806f640e1dd3083c61ed70b963 Mon Sep 17 00:00:00 2001 From: "chendaxin.tk" Date: Sun, 27 Sep 2026 23:29:04 +0800 Subject: [PATCH 1/2] fix(core): share generated ids across bundles Co-authored-by: TRAE CLI --- .../__tests__/unit/common/generator.test.ts | 32 +++++++++++++++++++ packages/vrender-core/src/common/generator.ts | 21 ++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/vrender-core/__tests__/unit/common/generator.test.ts b/packages/vrender-core/__tests__/unit/common/generator.test.ts index c00bc5cc4..ae29c647a 100644 --- a/packages/vrender-core/__tests__/unit/common/generator.test.ts +++ b/packages/vrender-core/__tests__/unit/common/generator.test.ts @@ -1,5 +1,11 @@ +/** + * @jest-environment node + */ + import { Generator } from '../../../src/common/generator'; +declare const require: any; + describe('Generator', () => { test('GenAutoIncrementId returns a finite number', () => { const id = Generator.GenAutoIncrementId(); @@ -20,4 +26,30 @@ describe('Generator', () => { expect(ids[i + 1]).toBe(ids[i] + 1); } }); + + test('GenAutoIncrementId shares one sequence across isolated module instances', () => { + jest.resetModules(); + const { Generator: FirstGenerator } = require('../../../src/common/generator'); + const firstId = FirstGenerator.GenAutoIncrementId(); + + jest.resetModules(); + const { Generator: SecondGenerator } = require('../../../src/common/generator'); + const secondId = SecondGenerator.GenAutoIncrementId(); + + expect(secondId).toBe(firstId + 1); + }); + + test('keeps node ids unique across isolated module instances', () => { + jest.resetModules(); + const { Node: ParentNode } = require('../../../src/graphic/node-tree'); + const parent = new ParentNode(); + + jest.resetModules(); + const { Node: ChildNode } = require('../../../src/graphic/node-tree'); + const child = new ChildNode(); + + expect(child._uid).not.toBe(parent._uid); + expect(parent.appendChild(child)).toBe(child); + expect(parent.firstChild).toBe(child); + }); }); diff --git a/packages/vrender-core/src/common/generator.ts b/packages/vrender-core/src/common/generator.ts index 5a252a963..81e6897d1 100644 --- a/packages/vrender-core/src/common/generator.ts +++ b/packages/vrender-core/src/common/generator.ts @@ -1,7 +1,22 @@ -export class Generator { - private static auto_increment_id: number = 0; +interface IGeneratorState { + autoIncrementId: number; +} +const GENERATOR_STATE_SYMBOL = Symbol.for('@visactor/vrender-core/generator-state'); + +function getGeneratorState(): IGeneratorState { + const scope = globalThis as typeof globalThis & { + [GENERATOR_STATE_SYMBOL]?: IGeneratorState; + }; + return scope[GENERATOR_STATE_SYMBOL] || (scope[GENERATOR_STATE_SYMBOL] = { autoIncrementId: 0 }); +} + +// Application and legacy runtime state are shared across bundles. Keep node ids in the same +// global scope so a shared Stage cannot receive a Layer with a colliding id from another bundle. +const generatorState = getGeneratorState(); + +export class Generator { static GenAutoIncrementId(): number { - return Generator.auto_increment_id++; + return generatorState.autoIncrementId++; } } From 1c20e7bf3d7df491c23e0ae997f053d01c60dcff Mon Sep 17 00:00:00 2001 From: "chendaxin.tk" Date: Tue, 13 Oct 2026 14:57:53 +0800 Subject: [PATCH 2/2] test(core): cover cross-bundle node identity and lifecycle --- docs/agent/VRENDER_RUNTIME_AND_ENTRIES.md | 11 +++ .../__tests__/unit/common/generator.test.ts | 57 +++++++++------- .../graphic/node-tree-cross-bundle.test.ts | 67 +++++++++++++++++++ 3 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 packages/vrender-core/__tests__/unit/graphic/node-tree-cross-bundle.test.ts diff --git a/docs/agent/VRENDER_RUNTIME_AND_ENTRIES.md b/docs/agent/VRENDER_RUNTIME_AND_ENTRIES.md index dc9e64902..cad2fd9f8 100644 --- a/docs/agent/VRENDER_RUNTIME_AND_ENTRIES.md +++ b/docs/agent/VRENDER_RUNTIME_AND_ENTRIES.md @@ -65,6 +65,17 @@ Stage 创建属于 app: - handle 的 `release()` 减少引用计数;最后一个 handle 释放后 app 释放。 - 直接调用 shared app 的 `app.release()` 会走同一 record release 逻辑。 +### 跨 bundle 的内部 ID + +`vrender-core/src/common/generator.ts` 在同一 JavaScript realm 内通过 +`Symbol.for('@visactor/vrender-core/generator-state')` 共享数字 ID 计数器。 +多个 bundle 的节点可能经 shared app / runtime service 进入同一棵树, +因此 ID 不能按模块副本或 app 独立计数,也不能随 app release 重置。 +模块初始化时缓存计数器对象,分配 ID 的热路径只做一次递增。 + +旧版本的模块独立计数器不会自动接入此机制;发布时仍需覆盖共存的依赖版本。 +回归入口:core 的 `common/generator.test.ts` 和 `graphic/node-tree-cross-bundle.test.ts`。 + ## `shared` / `shared-browser` / `shared-lynx` / `shared-browser-lite` - `./entries/shared`:通用 env shared entry,支持 `browser`、`node`、`taro`、`feishu`、`tt`、`wx`、`lynx`、`harmony`。package exports 中 `browser` condition 路由到 `shared-browser.js`,`lynx` condition 路由到 `shared-lynx.js`。 diff --git a/packages/vrender-core/__tests__/unit/common/generator.test.ts b/packages/vrender-core/__tests__/unit/common/generator.test.ts index ae29c647a..56b99b209 100644 --- a/packages/vrender-core/__tests__/unit/common/generator.test.ts +++ b/packages/vrender-core/__tests__/unit/common/generator.test.ts @@ -1,12 +1,16 @@ -/** - * @jest-environment node - */ +import type { Generator as GeneratorClass } from '../../../src/common/generator'; -import { Generator } from '../../../src/common/generator'; - -declare const require: any; +function loadGeneratorCopy(): typeof GeneratorClass { + let copy: typeof GeneratorClass; + jest.isolateModules(() => { + copy = jest.requireActual<{ Generator: typeof GeneratorClass }>('../../../src/common/generator').Generator; + }); + return copy; +} describe('Generator', () => { + const Generator = loadGeneratorCopy(); + test('GenAutoIncrementId returns a finite number', () => { const id = Generator.GenAutoIncrementId(); expect(typeof id).toBe('number'); @@ -27,29 +31,34 @@ describe('Generator', () => { } }); - test('GenAutoIncrementId shares one sequence across isolated module instances', () => { - jest.resetModules(); - const { Generator: FirstGenerator } = require('../../../src/common/generator'); - const firstId = FirstGenerator.GenAutoIncrementId(); + test('keeps IDs unique when independently loaded copies allocate alternately', () => { + const first = loadGeneratorCopy(); + const second = loadGeneratorCopy(); + expect(first).not.toBe(second); - jest.resetModules(); - const { Generator: SecondGenerator } = require('../../../src/common/generator'); - const secondId = SecondGenerator.GenAutoIncrementId(); + const ids = Array.from({ length: 100 }, (_, index) => (index % 2 === 0 ? first : second).GenAutoIncrementId()); - expect(secondId).toBe(firstId + 1); + expect(new Set(ids).size).toBe(ids.length); + ids.slice(1).forEach((id, index) => expect(id).toBe(ids[index] + 1)); }); - test('keeps node ids unique across isolated module instances', () => { - jest.resetModules(); - const { Node: ParentNode } = require('../../../src/graphic/node-tree'); - const parent = new ParentNode(); + test('continues the sequence when another bundle loads after allocations', () => { + const first = loadGeneratorCopy(); + const ids = Array.from({ length: 100 }, () => first.GenAutoIncrementId()); + const second = loadGeneratorCopy(); + const next = second.GenAutoIncrementId(); + + expect(next).toBe(ids[ids.length - 1] + 1); + expect(first.GenAutoIncrementId()).toBe(next + 1); + }); - jest.resetModules(); - const { Node: ChildNode } = require('../../../src/graphic/node-tree'); - const child = new ChildNode(); + test('does not restart the sequence when a module is evaluated again', () => { + const first = loadGeneratorCopy(); + const beforeReload = first.GenAutoIncrementId(); + const reloaded = loadGeneratorCopy(); - expect(child._uid).not.toBe(parent._uid); - expect(parent.appendChild(child)).toBe(child); - expect(parent.firstChild).toBe(child); + expect(reloaded).not.toBe(first); + expect(reloaded.GenAutoIncrementId()).toBe(beforeReload + 1); + expect(first.GenAutoIncrementId()).toBe(beforeReload + 2); }); }); diff --git a/packages/vrender-core/__tests__/unit/graphic/node-tree-cross-bundle.test.ts b/packages/vrender-core/__tests__/unit/graphic/node-tree-cross-bundle.test.ts new file mode 100644 index 000000000..e164237ba --- /dev/null +++ b/packages/vrender-core/__tests__/unit/graphic/node-tree-cross-bundle.test.ts @@ -0,0 +1,67 @@ +import type { Node } from '../../../src/graphic/node-tree'; + +function loadNodeCopy(): typeof Node { + let copy: typeof Node; + jest.isolateModules(() => { + copy = jest.requireActual<{ Node: typeof Node }>('../../../src/graphic/node-tree').Node; + }); + return copy; +} + +describe('node trees across independently loaded bundles', () => { + test('attaches a child from another bundle instead of treating it as the parent itself', () => { + const FirstNode = loadNodeCopy(); + const SecondNode = loadNodeCopy(); + expect(FirstNode).not.toBe(SecondNode); + + const parent = new FirstNode(); + const child = new SecondNode(); + + expect(parent.appendChild(child)).toBe(child); + expect(parent.firstChild).toBe(child); + expect(parent.childrenCount).toBe(1); + expect(child.parent).toBe(parent); + expect(parent.appendChild(parent)).toBeNull(); + }); + + test('preserves child lookup and removal when reparenting nodes from another bundle', () => { + const FirstNode = loadNodeCopy(); + const SecondNode = loadNodeCopy(); + const firstParent = new FirstNode(); + const firstChild = new FirstNode(); + const secondParent = new SecondNode(); + const secondChild = new SecondNode(); + firstParent.appendChild(firstChild); + secondParent.appendChild(secondChild); + + firstParent.appendChild(secondChild); + + expect(secondParent.childrenCount).toBe(0); + expect(firstParent.childrenCount).toBe(2); + expect(firstParent.findChildByUid(firstChild._uid)).toBe(firstChild); + expect(firstParent.findChildByUid(secondChild._uid)).toBe(secondChild); + expect(firstParent.removeChild(firstChild)).toBe(firstChild); + expect(firstChild.parent).toBeNull(); + expect(firstParent.firstChild).toBe(secondChild); + expect(firstParent.findChildByUid(secondChild._uid)).toBe(secondChild); + expect(firstParent.removeChild(secondChild)).toBe(secondChild); + expect(firstParent.childrenCount).toBe(0); + }); + + test('distinguishes ancestry in separate trees created by different bundles', () => { + const FirstNode = loadNodeCopy(); + const SecondNode = loadNodeCopy(); + const firstParent = new FirstNode(); + const firstChild = new FirstNode(); + const secondParent = new SecondNode(); + const secondChild = new SecondNode(); + firstParent.appendChild(firstChild); + secondParent.appendChild(secondChild); + + expect(firstChild.isChildOf(firstParent)).toBe(true); + expect(firstChild.isChildOf(secondParent)).toBe(false); + expect(firstChild.isDescendantsOf(secondParent)).toBe(false); + expect(firstParent.containNode(secondChild)).toBe(false); + expect(secondParent.containNode(firstChild)).toBe(false); + }); +});