Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/agent/VRENDER_RUNTIME_AND_ENTRIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`。
Expand Down
43 changes: 42 additions & 1 deletion packages/vrender-core/__tests__/unit/common/generator.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { Generator } from '../../../src/common/generator';
import type { Generator as GeneratorClass } from '../../../src/common/generator';

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');
Expand All @@ -20,4 +30,35 @@ describe('Generator', () => {
expect(ids[i + 1]).toBe(ids[i] + 1);
}
});

test('keeps IDs unique when independently loaded copies allocate alternately', () => {
const first = loadGeneratorCopy();
const second = loadGeneratorCopy();
expect(first).not.toBe(second);

const ids = Array.from({ length: 100 }, (_, index) => (index % 2 === 0 ? first : second).GenAutoIncrementId());

expect(new Set(ids).size).toBe(ids.length);
ids.slice(1).forEach((id, index) => expect(id).toBe(ids[index] + 1));
});

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);
});

test('does not restart the sequence when a module is evaluated again', () => {
const first = loadGeneratorCopy();
const beforeReload = first.GenAutoIncrementId();
const reloaded = loadGeneratorCopy();

expect(reloaded).not.toBe(first);
expect(reloaded.GenAutoIncrementId()).toBe(beforeReload + 1);
expect(first.GenAutoIncrementId()).toBe(beforeReload + 2);
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
21 changes: 18 additions & 3 deletions packages/vrender-core/src/common/generator.ts
Original file line number Diff line number Diff line change
@@ -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++;
}
}
Loading