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
84 changes: 84 additions & 0 deletions src/core/scene/scene-process/fetch-rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* 基于 fetch 的 RPC 客户端
* 替代 ProcessRPC,将 module.method(args) 调用转为 HTTP POST 请求
*/
export class FetchRPC<TModules extends Record<string, any>> {
private baseURL: string;
private defaultTimeout: number;
private disposed = false;

constructor(baseURL: string, options?: { timeout?: number }) {
// 确保 baseURL 不以 / 结尾
this.baseURL = baseURL.replace(/\/+$/, '');
this.defaultTimeout = options?.timeout ?? 30000;
}

/**
* 远程方法调用(请求-响应)
* 签名与 ProcessRPC.request 保持一致
*/
async request<K extends keyof TModules, M extends keyof TModules[K]>(
module: K,
method: M,
args?: any[],
options?: { timeout?: number }
): Promise<any> {
if (this.disposed) {
throw new Error('[FetchRPC] Instance has been disposed');
}

const timeout = options?.timeout ?? this.defaultTimeout;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(`${this.baseURL}/scene/rpc`, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

所有rpc请求统一转换到 scene/rpc ? 路由不用修改?

method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
module: module as string,
method: method as string,
args: args || [],
}),
signal: controller.signal,
});

if (!response.ok) {
throw new Error(
`[FetchRPC] HTTP ${response.status}: ${module as string}.${method as string}`
);
}

const data = await response.json();

if (data.error) {
throw new Error(data.error);
}

return data.result;
} catch (err: any) {
if (err.name === 'AbortError') {
throw new Error(
`[FetchRPC] Request timeout: ${module as string}.${method as string}`
);
}
throw err;
} finally {
clearTimeout(timer);
}
}

/**
* 是否已连接(fetch 模式下始终返回 true,除非 disposed)
*/
isConnect(): boolean {
return !this.disposed;
}

/**
* 清理
*/
dispose(): void {
this.disposed = true;
}
}
29 changes: 23 additions & 6 deletions src/core/scene/scene-process/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ProcessRPC } from '../process-rpc';
import { FetchRPC } from './fetch-rpc';
import type { IMainModule } from '../main-process';

export class RpcProxy {
private rpcInstance: ProcessRPC<IMainModule> | null = null;
private rpcInstance: ProcessRPC<IMainModule> | FetchRPC<IMainModule> | null = null;

public getInstance() {
if (!this.rpcInstance) {
Expand All @@ -11,16 +12,32 @@ export class RpcProxy {
return this.rpcInstance;
}

async startup() {
async startup(serverURL?: string) {
// 在创建新实例前,先清理旧实例,防止内存泄漏
this.dispose();
this.rpcInstance = new ProcessRPC<IMainModule>();
this.rpcInstance.attach(process);
const { Service } = await import('./service/core/decorator');
this.rpcInstance.register(Service);
const baseURL = serverURL || this._inferBaseURL();
const isNode = typeof process !== 'undefined' && !!process.versions?.node;
if (isNode) {
this.rpcInstance = new FetchRPC<IMainModule>(baseURL);
}
else {
this.rpcInstance = new ProcessRPC<IMainModule>();
const { Service } = await import('./service/core/decorator');
this.rpcInstance.register(Service);
}
console.log('[Scene] Scene Process RPC ready');
}

/**
* 从当前页面 URL 推断 API 基地址
*/
private _inferBaseURL(): string {
if (typeof location !== 'undefined') {
return location.origin;
}
return 'http://localhost:3000';
}

/**
* 清理 RPC 实例
*/
Expand Down
82 changes: 68 additions & 14 deletions src/core/scene/scene-process/service/core/global-events.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,71 @@
import { EventEmitter } from 'events';
import { SceneProcessEventTag } from '../../../common';
/**
* 纯 JS 实现的轻量 EventEmitter,替代 Node.js 的 EventEmitter
* 在浏览器/Worker 环境中无需依赖 Node 'events' 模块
*/
class SimpleEventEmitter {
private _listeners = new Map<string, Set<(...args: any[]) => void>>();

on(event: string, listener: (...args: any[]) => void): void {
if (!this._listeners.has(event)) {
this._listeners.set(event, new Set());
}
this._listeners.get(event)!.add(listener);
}

once(event: string, listener: (...args: any[]) => void): void {
const wrapper = (...args: any[]) => {
this.off(event, wrapper);
listener(...args);
};
(wrapper as any)._original = listener;
this.on(event, wrapper);
}

off(event: string, listener: (...args: any[]) => void): void {
const set = this._listeners.get(event);
if (!set) return;
// 直接移除
if (set.delete(listener)) return;
// 尝试移除 once 包装
for (const fn of set) {
if ((fn as any)._original === listener) {
set.delete(fn);
return;
}
}
}

emit(event: string, ...args: any[]): void {
const set = this._listeners.get(event);
if (!set) return;
for (const listener of set) {
listener(...args);
}
}

removeAllListeners(event?: string): void {
if (event) {
this._listeners.delete(event);
} else {
this._listeners.clear();
}
}
}

// 全局共享的 EventEmitter 实例(内部使用,不对外暴露)
const globalEventEmitter = new EventEmitter();
const globalEventEmitter = new SimpleEventEmitter();

/**
* 全局事件管理器
* 统一管理所有服务的事件监听,支持类型安全的事件订阅
*
* Fetch 方案改造:
* - EventEmitter 替换为纯 JS 实现的 SimpleEventEmitter
* - 移除 IMessageTransport 跨线程广播
* - broadcast 退化为与 emit 相同的本地事件触发
*/
class GlobalEventManager {

/**
* 监听指定类型的事件(类型安全版本)
* @param event 事件名称
Expand Down Expand Up @@ -92,25 +149,22 @@ class GlobalEventManager {
}

/**
* 跨进程广播,传的参数需要能被序列化
* @param event 事件名称
* @param args 事件参数
* 广播事件
*
* Fetch 方案改造:
* - 移除 process.send()(原实现:发送到 Node 父进程)
* - 移除 IMessageTransport(Worker 方案的产物,Fetch 方案不需要)
* - broadcast 退化为与 emit 相同的本地事件触发
*
* 如需向服务端推送事件,应由调用方主动 fetch 接口
*/
broadcast<TEvents extends Record<string, any>>(
event: keyof TEvents,
...args: TEvents[keyof TEvents]
): void;
broadcast(event: string, ...args: any[]): void;
broadcast(event: any, ...args: any[]): void {
const message = {
type: SceneProcessEventTag,
event: event as string,
args: [...args]
};
globalEventEmitter.emit(event, ...args);
if ('connected' in process) {
process.send?.(message);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个修改破坏 mcp模式的兼容性

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是的, 还需要做下兼容。昨天只是先把做的web先提交了。今天还需要处理。

}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/scene/scene-process/service/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const tickTime = 1000 / 60;
*/
@register('Engine')
export class EngineService extends BaseService<IEngineEvents> implements IEngineService {
private _setTimeoutId: NodeJS.Timeout | null = null;
private _setTimeoutId: ReturnType<typeof setTimeout> | null = null;
private _rafId: number | null = null;
private _maxDeltaTimeInEM = 1 / 30;
private _stateRecord = 0; // 记录当前状态
Expand Down
11 changes: 8 additions & 3 deletions src/core/scene/scene-process/service/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,14 @@
importExceptionHandler: (...args) => this._handleImportException(...args),
cceModuleMap,
});
// eslint-disable-next-line no-undef
globalThis.self = window;
this._executor.addPolyfillFile(require.resolve('@cocos/build-polyfills/prebuilt/editor/bundle'));
// Web 改造:
// - 移除 globalThis.self = window(Web 环境中 self 已自动指向 window 或 WorkerGlobalScope)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

移除self = window.
mcp 模式下能正常工作?

// - require.resolve 替换为构建时注入的常量或动态路径
// 若使用 bundler(如 vite/webpack),可通过 define 插件注入 __POLYFILL_BUNDLE_URL__
const polyfillUrl = typeof __POLYFILL_BUNDLE_URL__ !== 'undefined'

Check failure on line 173 in src/core/scene/scene-process/service/script.ts

View workflow job for this annotation

GitHub Actions / pr-test (macos-latest)

Cannot find name '__POLYFILL_BUNDLE_URL__'.

Check failure on line 173 in src/core/scene/scene-process/service/script.ts

View workflow job for this annotation

GitHub Actions / pr-test (windows-latest)

Cannot find name '__POLYFILL_BUNDLE_URL__'.
? __POLYFILL_BUNDLE_URL__

Check failure on line 174 in src/core/scene/scene-process/service/script.ts

View workflow job for this annotation

GitHub Actions / pr-test (macos-latest)

Cannot find name '__POLYFILL_BUNDLE_URL__'.

Check failure on line 174 in src/core/scene/scene-process/service/script.ts

View workflow job for this annotation

GitHub Actions / pr-test (windows-latest)

Cannot find name '__POLYFILL_BUNDLE_URL__'.
: '@cocos/build-polyfills/prebuilt/editor/bundle';
this._executor.addPolyfillFile(polyfillUrl);
// 同步插件脚本列表
await this._syncPluginScripts.nextIteration();
// 重载项目与插件脚本
Expand Down
Loading