Skip to content
Open
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
17 changes: 14 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,24 @@ jobs:
ls -alh
- uses: actions/setup-node@v3
with:
node-version: 18
# - name: Verify Change Logs
# run: node common/scripts/install-run-rush.js change --verify
node-version: 18.20.3
- name: Debug Info
run: |
echo "Node.js version: $(node --version)"
echo "npm version: $(npm --version)"
echo "pnpm version: $(pnpm --version)"
- name: Rush Install
run: node common/scripts/install-run-rush.js install
- name: Rush build
run: node common/scripts/install-run-rush.js build
- name: Print build failures
if: failure()
run: |
echo "=== Failed projects ==="
find . -name "rush-build-error.log" -exec echo "--- {} ---" \; -exec cat {} \; 2>/dev/null || true
echo ""
echo "=== Looking for build output files with errors ==="
find common/temp -name "*.log" -path "*/build/*" -mmin -10 2>/dev/null | head -20 || true
- name: Check Lint
run: node common/scripts/install-run-rush.js lint --verbose
- name: Check TS
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: "https://registry.npmjs.org/"

- name: Upgrade npm for OIDC support
run: |
npm install -g npm@latest
npm install -g npm@11
npm --version

- name: Determine version and publish parameters
Expand Down
3 changes: 3 additions & 0 deletions common/config/rush/.npmrc-publish
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@
#
# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}
#

registry=https://registry.npmjs.org/
provenance=true
1,182 changes: 593 additions & 589 deletions common/config/rush/command-line.json

Large diffs are not rendered by default.

45 changes: 36 additions & 9 deletions packages/canvas-engine/core/__tests__/pipeline.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import React from 'react';

import { interfaces } from 'inversify';
import { cleanup, render } from '@testing-library/react';
import { Message } from '@phosphor/messaging';
import { Message, MessageLoop } from '@phosphor/messaging';

import { PlaygroundReactProvider, PlaygroundReactRenderer } from '../src/react';
import { createPlaygroundContainer } from '../src/playground-container';
import { createLayerReactAutorun } from '../src/core/pipeline/pipline-react-utils';
import { PipelineEntitiesImpl, PipelineMessage } from '../src/core';
import { PipelineEntitiesImpl, PipelineMessage, PipelineRenderer } from '../src/core';
import { ConfigEntity } from '../src/common';
import { createPlayground } from '../__mocks__/playground-container.mock';
import {
Expand Down Expand Up @@ -48,7 +48,7 @@ describe('pipeline render', () => {
<PlaygroundReactRenderer>
<div></div>
</PlaygroundReactRenderer>
</PlaygroundReactProvider>,
</PlaygroundReactProvider>
);

expect(playgroundRender.asFragment()).toMatchSnapshot();
Expand All @@ -62,7 +62,7 @@ describe('pipeline render', () => {
playgroundContainer={createPlaygroundContainer()}
>
<PlaygroundReactRenderer />
</PlaygroundReactProvider>,
</PlaygroundReactProvider>
);

expect(playgroundRender.asFragment()).toMatchSnapshot();
Expand All @@ -73,7 +73,7 @@ describe('pipeline render', () => {
const playgroundRender = render(
<PlaygroundReactProvider containerModules={modules} playgroundContext={{}}>
<PlaygroundReactRenderer />
</PlaygroundReactProvider>,
</PlaygroundReactProvider>
);

expect(playgroundRender.asFragment()).toMatchSnapshot();
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('pipeline render', () => {
testLayer,
mockOriginRender,
renderCb,
renderer,
renderer
);
render(<Portal1 />);
autorun();
Expand All @@ -114,7 +114,7 @@ describe('pipeline render', () => {
testLayer,
mockOriginRender,
renderCb,
renderer,
renderer
);
render(<Portal2 />);
// 2. 渲染完成调用 renderCb
Expand All @@ -123,7 +123,7 @@ describe('pipeline render', () => {
testLayer,
mockOriginRender,
renderCb,
renderer,
renderer
);
// 3, 使用 undefined 触发 catchError 分支
render(<Portal3 />);
Expand All @@ -134,7 +134,7 @@ describe('pipeline render', () => {
testLayer,
mockOriginRenderReturnNull,
renderCb,
renderer,
renderer
);
const { container } = render(<Portal4 />);
expect(container).toMatchSnapshot();
Expand Down Expand Up @@ -189,6 +189,33 @@ describe('pipeline render', () => {
expect(render(<Comp />)).toMatchSnapshot();
});

it('pipeline renderer batches layer updates into one message', () => {
const playground = createPlayground();
playground.ready();
playground.registerLayer(TestRenderLayer1);
playground.registerLayer(TestRenderLayer2);
const renderer = playground.pipelineRegistry.renderer as PipelineRenderer;
const layer1 = playground.getLayer(TestRenderLayer1)!;
const layer2 = playground.getLayer(TestRenderLayer2)!;
const flushSpy = vi.spyOn(renderer as any, 'flushPendingLayers');
const postMessageSpy = vi.spyOn(MessageLoop, 'postMessage').mockImplementation(() => undefined);

vi.stubEnv('NODE_ENV', 'production');
renderer.updateLayer(layer1);
renderer.updateLayer(layer1);
renderer.updateLayer(layer2, true);

expect(postMessageSpy).toHaveBeenCalledTimes(1);
renderer.processMessage((renderer as any).flushMessage);

expect(flushSpy).toHaveBeenCalledTimes(1);
expect((renderer as any).pendingLayers.size).toBe(0);
expect((renderer as any).flushMessagePosted).toBe(false);

vi.unstubAllEnvs();
postMessageSpy.mockRestore();
});

it('pipeline-entities', () => {
const playground = createPlayground();
playground.ready();
Expand Down
45 changes: 31 additions & 14 deletions packages/canvas-engine/core/src/common/entity-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import type {
export function bindConfigEntity(bind: interfaces.Bind, entityRegistry: EntityRegistry): void {
bind(entityRegistry)
.toDynamicValue(
ctx =>
ctx.container.get<EntityManager>(EntityManager)!.createEntity(entityRegistry)! as never,
(ctx) =>
ctx.container.get<EntityManager>(EntityManager)!.createEntity(entityRegistry)! as never
)
.inSingletonScope();
}
Expand All @@ -56,6 +56,8 @@ export class EntityManager implements Disposable {

protected onEntityChangeEmitter = new Emitter<string>();

protected onEntityInstanceChangeEmitter = new Emitter<{ entityType: string; entityId: string }>();

protected onEntityLifeCycleEmitter = new Emitter<{
type: 'add' | 'update' | 'delete';
entity: Entity;
Expand Down Expand Up @@ -113,6 +115,11 @@ export class EntityManager implements Disposable {
*/
readonly onEntityChange = this.onEntityChangeEmitter.event;

/**
* Fine-grained: fires when a specific entity instance changes (includes entityId).
*/
readonly onEntityInstanceChange = this.onEntityInstanceChangeEmitter.event;

/**
* entity data 数据变化
*/
Expand All @@ -139,20 +146,24 @@ export class EntityManager implements Disposable {
changeEntityLocked = false;

constructor() {
this.toDispose.pushAll([this.onEntityChangeEmitter, this.schedule]);
this.toDispose.pushAll([
this.onEntityChangeEmitter,
this.onEntityInstanceChangeEmitter,
this.schedule,
]);
}

@postConstruct()
init() {
this.contributions.forEach(contrib => contrib.registerEntityManager?.(this));
this.contributions.forEach((contrib) => contrib.registerEntityManager?.(this));
}

/**
* 创建实体
*/
createEntity<T extends Entity>(
Registry: EntityRegistry,
opts?: Omit<T['__opts_type__'], 'entityManager'>,
opts?: Omit<T['__opts_type__'], 'entityManager'>
): T {
if (!Registry.type) {
throw new Error(`[EntityManager] createEntity need a type: ${Registry}`);
Expand Down Expand Up @@ -202,7 +213,7 @@ export class EntityManager implements Disposable {
*/
resetEntities(registry: EntityRegistry): void {
const entities = this.getEntities(registry);
entities.forEach(entity => {
entities.forEach((entity) => {
entity.reset();
});
}
Expand All @@ -214,7 +225,7 @@ export class EntityManager implements Disposable {

updateConfigEntity<E extends ConfigEntity>(
registry: EntityRegistry,
config: Partial<E['config']>,
config: Partial<E['config']>
): void {
const entity = this.configEntities.get(registry.type);
if (entity) {
Expand Down Expand Up @@ -296,11 +307,11 @@ export class EntityManager implements Disposable {

getEntityDatas<T extends EntityData>(
entityRegistry: EntityRegistry,
dataRegistry: EntityDataRegistry<T>,
dataRegistry: EntityDataRegistry<T>
): T[] {
return this.getEntities<any>(entityRegistry)
.map((e: Entity) => e.getData<T>(dataRegistry))
.filter(d => !!d) as T[];
.filter((d) => !!d) as T[];
}

hasEntity(registry: EntityRegistry): boolean {
Expand Down Expand Up @@ -362,22 +373,22 @@ export class EntityManager implements Disposable {
this.configEntities.set(entity.type, entity);
}
entities.push(entity);
entity.onEntityChange(entity => {
entity.onEntityChange((entity) => {
this.fireEntityChanged(entity);
this.fireEntityLifeCycleChanged({ type: 'update', entity });
});
entity.onDataChange(e => {
entity.onDataChange((e) => {
this.fireEntityDataChanged(entity.type, e.data.type);
});
entity.toDispose.push(
Disposable.create(() => {
this.removeEntity(entity);
this.fireEntityLifeCycleChanged({ type: 'delete', entity });
}),
})
);
entity
.getDefaultDataRegistries()
.forEach(registry => this.fireEntityDataChanged(entity.type, registry.type));
.forEach((registry) => this.fireEntityDataChanged(entity.type, registry.type));
this.fireEntityChanged(entity);
this.fireEntityLifeCycleChanged({ type: 'add', entity });
}
Expand All @@ -389,7 +400,7 @@ export class EntityManager implements Disposable {
if (index !== -1) {
this.entityInstanceMapByType.set(
entity.type,
entities.filter(e => e !== entity),
entities.filter((e) => e !== entity)
);
this.entityInstanceMap.delete(entity.id);

Expand All @@ -415,6 +426,7 @@ export class EntityManager implements Disposable {

fireEntityChanged = (entity: Entity | string) => {
const entityType = typeof entity === 'string' ? entity : entity.type;
const entityId = typeof entity === 'string' ? undefined : entity.id;
let version = this.entityVersionMap.get(entityType) || 0;
/* istanbul ignore next */
if (version === Number.MAX_SAFE_INTEGER) {
Expand All @@ -425,6 +437,11 @@ export class EntityManager implements Disposable {
this.schedule.push(entityType, () => {
this.onEntityChangeEmitter.fire(entityType);
});
if (entityId) {
this.schedule.push(`instance/${entityId}`, () => {
this.onEntityInstanceChangeEmitter.fire({ entityType, entityId });
});
}
};

fireEntityDataChanged = (entityType: string, entityDataType: string) => {
Expand Down
53 changes: 43 additions & 10 deletions packages/canvas-engine/core/src/common/playground-schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,61 @@
* SPDX-License-Identifier: MIT
*/

import { throttle } from 'lodash-es';
import { type Disposable } from '@flowgram.ai/utils';

// TODO 先用 throttle 替代
export class PlaygroundSchedule implements Disposable {
protected execMap: Map<any, () => void> = new Map();
protected pendingExecMap: Map<any, () => void> = new Map();

private animationFrame: number | undefined;

private microtaskScheduled = false;

push(key: any, fn: () => void): void {
const { execMap } = this;
if (process.env.NODE_ENV === 'test') {
fn();
return;
}
let schedule = execMap.get(key);
if (!schedule) {
schedule = throttle(fn, 0) as () => void;
execMap.set(key, schedule);
this.pendingExecMap.set(key, fn);
this.scheduleFlush();
}

private scheduleFlush(): void {
if (typeof requestAnimationFrame === 'function') {
if (this.animationFrame !== undefined) {
return;
}
this.animationFrame = requestAnimationFrame(() => {
this.animationFrame = undefined;
this.flush();
});
return;
}
schedule();

if (this.microtaskScheduled) {
return;
}
this.microtaskScheduled = true;
Promise.resolve().then(() => {
this.microtaskScheduled = false;
this.flush();
});
}

private flush(): void {
if (!this.pendingExecMap.size) {
return;
}
const execList = Array.from(this.pendingExecMap.values());
this.pendingExecMap.clear();
execList.forEach((fn) => fn());
}

dispose(): void {
this.execMap.clear();
this.pendingExecMap.clear();
if (this.animationFrame !== undefined && typeof cancelAnimationFrame === 'function') {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = undefined;
}
this.microtaskScheduled = false;
}
}
Loading
Loading