diff --git a/packages/renderer-core/src/main.ts b/packages/renderer-core/src/createRenderer.ts similarity index 98% rename from packages/renderer-core/src/main.ts rename to packages/renderer-core/src/createRenderer.ts index fcc0f7f2c..18d344f6f 100644 --- a/packages/renderer-core/src/main.ts +++ b/packages/renderer-core/src/createRenderer.ts @@ -83,6 +83,7 @@ export function createRenderer( }, destroy: () => { lifeCycleService.setPhase(LifecyclePhase.Destroying); + instantiationService.dispose(); }, }; diff --git a/packages/renderer-core/src/index.ts b/packages/renderer-core/src/index.ts index 1d37db614..58b9135c4 100644 --- a/packages/renderer-core/src/index.ts +++ b/packages/renderer-core/src/index.ts @@ -1,5 +1,5 @@ /* --------------- api -------------------- */ -export { createRenderer } from './main'; +export { createRenderer } from './createRenderer'; export { IExtensionHostService } from './services/extension'; export { definePackageLoader, IPackageManagementService } from './services/package'; export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService'; @@ -9,7 +9,6 @@ export { IRuntimeIntlService } from './services/runtimeIntlService'; export { IRuntimeUtilService } from './services/runtimeUtilService'; export { ISchemaService } from './services/schema'; export { Widget } from './widget'; -export * from './utils/value'; /* --------------- types ---------------- */ export type * from './types'; diff --git a/packages/renderer-core/src/services/code-runtime/codeRuntime.ts b/packages/renderer-core/src/services/code-runtime/codeRuntime.ts index 2584e81f7..12c75581b 100644 --- a/packages/renderer-core/src/services/code-runtime/codeRuntime.ts +++ b/packages/renderer-core/src/services/code-runtime/codeRuntime.ts @@ -1,31 +1,34 @@ import { type StringDictionary, type JSNode, - type EventDisposable, + type IDisposable, type JSExpression, type JSFunction, specTypes, + isNode, + toDisposable, + Disposable, } from '@alilc/lowcode-shared'; import { type ICodeScope, CodeScope } from './codeScope'; -import { isNode } from '../../../../shared/src/utils/node'; -import { mapValue } from '../../utils/value'; +import { mapValue } from './value'; import { evaluate } from './evaluate'; export interface CodeRuntimeOptions { initScopeValue?: Partial; + parentScope?: ICodeScope; evalCodeFunction?: EvalCodeFunction; } -export interface ICodeRuntime { +export interface ICodeRuntime extends IDisposable { getScope(): ICodeScope; run(code: string, scope?: ICodeScope): R | undefined; resolve(value: StringDictionary): any; - onResolve(handler: NodeResolverHandler): EventDisposable; + onResolve(handler: NodeResolverHandler): IDisposable; createChild( options: Omit, 'parentScope'>, @@ -34,49 +37,55 @@ export interface ICodeRuntime { export type NodeResolverHandler = (node: JSNode) => JSNode | false | undefined; -let onResolveHandlers: NodeResolverHandler[] = []; - export type EvalCodeFunction = (code: string, scope: any) => any; -export class CodeRuntime implements ICodeRuntime { - private codeScope: ICodeScope; +export class CodeRuntime + extends Disposable + implements ICodeRuntime +{ + private _codeScope: ICodeScope; + + private _evalCodeFunction: EvalCodeFunction = evaluate; - private evalCodeFunction: EvalCodeFunction = evaluate; + private _resolveHandlers: NodeResolverHandler[] = []; constructor(options: CodeRuntimeOptions = {}) { - if (options.evalCodeFunction) this.evalCodeFunction = options.evalCodeFunction; + super(); - if (options.parentScope) { - this.codeScope = options.parentScope.createChild(options.initScopeValue ?? {}); - } else { - this.codeScope = new CodeScope(options.initScopeValue ?? {}); - } + if (options.evalCodeFunction) this._evalCodeFunction = options.evalCodeFunction; + this._codeScope = this.addDispose( + options.parentScope + ? options.parentScope.createChild(options.initScopeValue ?? {}) + : new CodeScope(options.initScopeValue ?? {}), + ); } getScope() { - return this.codeScope; + return this._codeScope; } run(code: string): R | undefined { + this._throwIfDisposed(`this code runtime has been disposed`); + if (!code) return undefined; try { - const result = this.evalCodeFunction(code, this.codeScope.value); + const result = this._evalCodeFunction(code, this._codeScope.value); return result as R; } catch (err) { // todo replace logger - console.error('eval error', code, this.codeScope.value, err); + console.error('eval error', code, this._codeScope.value, err); return undefined; } } resolve(data: StringDictionary): any { - if (onResolveHandlers.length > 0) { + if (this._resolveHandlers.length > 0) { data = mapValue(data, isNode, (node: JSNode) => { let newNode: JSNode | false | undefined = node; - for (const handler of onResolveHandlers) { + for (const handler of this._resolveHandlers) { newNode = handler(newNode as JSNode); if (newNode === false || typeof newNode === 'undefined') { break; @@ -110,20 +119,25 @@ export class CodeRuntime implemen /** * 顺序执行 handler */ - onResolve(handler: NodeResolverHandler): EventDisposable { - onResolveHandlers.push(handler); - return () => { - onResolveHandlers = onResolveHandlers.filter((h) => h !== handler); - }; + onResolve(handler: NodeResolverHandler): IDisposable { + this._resolveHandlers.push(handler); + + return this.addDispose( + toDisposable(() => { + this._resolveHandlers = this._resolveHandlers.filter((h) => h !== handler); + }), + ); } createChild( options?: Omit, 'parentScope'>, ): ICodeRuntime { - return new CodeRuntime({ - initScopeValue: options?.initScopeValue, - parentScope: this.codeScope, - evalCodeFunction: options?.evalCodeFunction ?? this.evalCodeFunction, - }); + return this.addDispose( + new CodeRuntime({ + initScopeValue: options?.initScopeValue, + parentScope: this._codeScope, + evalCodeFunction: options?.evalCodeFunction ?? this._evalCodeFunction, + }), + ); } } diff --git a/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts b/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts index d405eacfa..d329638e8 100644 --- a/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts +++ b/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts @@ -1,13 +1,13 @@ import { createDecorator, - invariant, Disposable, type StringDictionary, + type IDisposable, } from '@alilc/lowcode-shared'; import { type ICodeRuntime, type CodeRuntimeOptions, CodeRuntime } from './codeRuntime'; import { ISchemaService } from '../schema'; -export interface ICodeRuntimeService { +export interface ICodeRuntimeService extends IDisposable { readonly rootRuntime: ICodeRuntime; createCodeRuntime( @@ -18,15 +18,18 @@ export interface ICodeRuntimeService { export const ICodeRuntimeService = createDecorator('codeRuntimeService'); export class CodeRuntimeService extends Disposable implements ICodeRuntimeService { - rootRuntime: ICodeRuntime; + private _rootRuntime: ICodeRuntime; + get rootRuntime() { + return this._rootRuntime; + } constructor( options: CodeRuntimeOptions = {}, @ISchemaService private schemaService: ISchemaService, ) { super(); - this.rootRuntime = new CodeRuntime(options); + this._rootRuntime = this.addDispose(new CodeRuntime(options)); this.addDispose( this.schemaService.onSchemaUpdate(({ key, data }) => { if (key === 'constants') { @@ -39,10 +42,10 @@ export class CodeRuntimeService extends Disposable implements ICodeRuntimeServic createCodeRuntime( options: CodeRuntimeOptions = {}, ): ICodeRuntime { - invariant(this.rootRuntime, `please initialize codeRuntimeService on renderer starting!`); + this._throwIfDisposed(); - return options.parentScope - ? new CodeRuntime(options) - : this.rootRuntime.createChild(options); + return this.addDispose( + options.parentScope ? new CodeRuntime(options) : this.rootRuntime.createChild(options), + ); } } diff --git a/packages/renderer-core/src/services/code-runtime/codeScope.ts b/packages/renderer-core/src/services/code-runtime/codeScope.ts index 393bc618d..86537ace9 100644 --- a/packages/renderer-core/src/services/code-runtime/codeScope.ts +++ b/packages/renderer-core/src/services/code-runtime/codeScope.ts @@ -1,4 +1,9 @@ -import { type StringDictionary, LinkedListNode } from '@alilc/lowcode-shared'; +import { + type StringDictionary, + Disposable, + type IDisposable, + LinkedListNode, +} from '@alilc/lowcode-shared'; import { trustedGlobals } from './globals-es2015'; /* @@ -9,33 +14,50 @@ const unscopables = trustedGlobals.reduce((acc, key) => ({ ...acc, [key]: true } __proto__: null, }); -export interface ICodeScope { +export interface ICodeScope extends IDisposable { readonly value: T; set(name: keyof T, value: any): void; + setValue(value: Partial, replace?: boolean): void; + createChild(initValue: Partial): ICodeScope; + + dispose(): void; } -export class CodeScope implements ICodeScope { +export class CodeScope + extends Disposable + implements ICodeScope +{ node = LinkedListNode.Undefined; - private proxyValue: T; + private _proxyValue?: T; constructor(initValue: Partial) { + super(); + this.node.current = initValue; - this.proxyValue = this.createProxy(); } get value(): T { - return this.proxyValue; + this._throwIfDisposed('code scope has been disposed'); + + if (!this._proxyValue) { + this._proxyValue = this._createProxy(); + } + return this._proxyValue; } set(name: keyof T, value: any): void { + this._throwIfDisposed('code scope has been disposed'); + this.node.current[name] = value; } setValue(value: Partial, replace = false) { + this._throwIfDisposed('code scope has been disposed'); + if (replace) { this.node.current = { ...value }; } else { @@ -44,24 +66,30 @@ export class CodeScope implements } createChild(initValue: Partial): ICodeScope { - const childScope = new CodeScope(initValue); + const childScope = this.addDispose(new CodeScope(initValue)); childScope.node.prev = this.node; return childScope; } - private createProxy(): T { + dispose(): void { + super.dispose(); + this.node = LinkedListNode.Undefined; + this._proxyValue = undefined; + } + + private _createProxy(): T { return new Proxy(Object.create(null) as T, { set: (target, p, newValue) => { this.set(p as string, newValue); return true; }, - get: (_, p) => this.findValue(p) ?? undefined, - has: (_, p) => this.hasProperty(p), + get: (_, p) => this._findValue(p) ?? undefined, + has: (_, p) => this._hasProperty(p), }); } - private findValue(prop: PropertyKey) { + private _findValue(prop: PropertyKey) { if (prop === Symbol.unscopables) return unscopables; let node = this.node; @@ -73,7 +101,7 @@ export class CodeScope implements } } - private hasProperty(prop: PropertyKey): boolean { + private _hasProperty(prop: PropertyKey): boolean { if (prop in unscopables) return true; let node = this.node; diff --git a/packages/renderer-core/src/services/code-runtime/index.ts b/packages/renderer-core/src/services/code-runtime/index.ts index abebe49af..3527cd472 100644 --- a/packages/renderer-core/src/services/code-runtime/index.ts +++ b/packages/renderer-core/src/services/code-runtime/index.ts @@ -1,3 +1,4 @@ export * from './codeScope'; export * from './codeRuntimeService'; export * from './codeRuntime'; +export * from './value'; diff --git a/packages/renderer-core/src/utils/value.ts b/packages/renderer-core/src/services/code-runtime/value.ts similarity index 100% rename from packages/renderer-core/src/utils/value.ts rename to packages/renderer-core/src/services/code-runtime/value.ts diff --git a/packages/renderer-core/src/services/model/componentTreeModel.ts b/packages/renderer-core/src/services/model/componentTreeModel.ts index fb4b8aa9c..89ff757b6 100644 --- a/packages/renderer-core/src/services/model/componentTreeModel.ts +++ b/packages/renderer-core/src/services/model/componentTreeModel.ts @@ -10,8 +10,9 @@ import { type InstanceDataSourceApi, type ComponentTree, specTypes, - invariant, uniqueId, + type IDisposable, + Disposable, } from '@alilc/lowcode-shared'; import { type ICodeRuntime } from '../code-runtime'; import { IWidget, Widget } from '../../widget'; @@ -60,7 +61,7 @@ export type ModelDataSourceCreator = ( codeRuntime: ICodeRuntime, ) => InstanceDataSourceApi; -export interface ComponentTreeModelOptions { +export interface ComponentTreeModelOptions extends IDisposable { id?: string; metadata?: StringDictionary; @@ -69,37 +70,53 @@ export interface ComponentTreeModelOptions { } export class ComponentTreeModel + extends Disposable implements IComponentTreeModel { - private instanceMap = new Map(); + private _instanceMap = new Map(); - public id: string; + private _id: string; - public widgets: IWidget[] = []; + private _widgets?: IWidget[]; - public metadata: StringDictionary = {}; + private _metadata: StringDictionary; constructor( - public componentsTree: ComponentTree, - public codeRuntime: ICodeRuntime, + private _componentsTree: ComponentTree, + private _codeRuntime: ICodeRuntime, options: ComponentTreeModelOptions, ) { - invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel'); + super(); - this.id = options?.id ?? `model_${uniqueId()}`; - if (options?.metadata) { - this.metadata = options.metadata; - } + this._id = options?.id ?? `model_${uniqueId()}`; + this._metadata = options?.metadata ?? {}; + this.initialize(options); + this.addDispose(_codeRuntime); + } - if (componentsTree.children) { - this.widgets = this.buildWidgets(componentsTree.children); - } + get id() { + return this._id; + } - this.initialize(options); + get codeRuntime() { + return this._codeRuntime; + } + + get metadata() { + return this._metadata; + } + + get widgets() { + if (!this._componentsTree.children) return []; + + if (!this._widgets) { + this._widgets = this.buildWidgets(this._componentsTree.children); + } + return this._widgets; } private initialize({ stateCreator, dataSourceCreator }: ComponentTreeModelOptions) { - const { state = {}, defaultProps, props = {}, dataSource, methods = {} } = this.componentsTree; + const { state = {}, defaultProps, props = {}, dataSource, methods = {} } = this._componentsTree; const codeScope = this.codeRuntime.getScope(); const initalProps = this.codeRuntime.resolve(props); @@ -119,12 +136,12 @@ export class ComponentTreeModel Object.assign( { $: (ref: string) => { - const insArr = this.instanceMap.get(ref); + const insArr = this._instanceMap.get(ref); if (!insArr) return undefined; return insArr[0]; }, $$: (ref: string) => { - return this.instanceMap.get(ref) ?? []; + return this._instanceMap.get(ref) ?? []; }, }, dataSourceApi, @@ -140,19 +157,19 @@ export class ComponentTreeModel } getCssText(): string | undefined { - return this.componentsTree.css; + return this._componentsTree.css; } triggerLifeCycle(lifeCycleName: ComponentLifeCycle, ...args: any[]) { // keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象 if ( - !this.componentsTree.lifeCycles || - !Object.keys(this.componentsTree.lifeCycles).includes(lifeCycleName) + !this._componentsTree.lifeCycles || + !Object.keys(this._componentsTree.lifeCycles).includes(lifeCycleName) ) { return; } - const lifeCycleSchema = this.componentsTree.lifeCycles[lifeCycleName]; + const lifeCycleSchema = this._componentsTree.lifeCycles[lifeCycleName]; const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema); if (typeof lifeCycleFn === 'function') { @@ -161,22 +178,22 @@ export class ComponentTreeModel } setComponentRef(ref: string, ins: ComponentInstance) { - let insArr = this.instanceMap.get(ref); + let insArr = this._instanceMap.get(ref); if (!insArr) { insArr = []; - this.instanceMap.set(ref, insArr); + this._instanceMap.set(ref, insArr); } insArr!.push(ins); } removeComponentRef(ref: string, ins?: ComponentInstance) { - const insArr = this.instanceMap.get(ref); + const insArr = this._instanceMap.get(ref); if (insArr) { if (ins) { const idx = insArr.indexOf(ins); if (idx > 0) insArr.splice(idx, 1); } else { - this.instanceMap.delete(ref); + this._instanceMap.delete(ref); } } } @@ -197,6 +214,11 @@ export class ComponentTreeModel } }); } + + dispose(): void { + super.dispose(); + this._instanceMap.clear(); + } } export function normalizeComponentNode(node: ComponentNode): NormalizedComponentNode { diff --git a/packages/renderer-core/src/services/model/componentTreeModelService.ts b/packages/renderer-core/src/services/model/componentTreeModelService.ts index 2401ea5d0..cd09a7898 100644 --- a/packages/renderer-core/src/services/model/componentTreeModelService.ts +++ b/packages/renderer-core/src/services/model/componentTreeModelService.ts @@ -1,5 +1,7 @@ import { createDecorator, + type IDisposable, + Disposable, invariant, type ComponentTree, type StringDictionary, @@ -16,7 +18,7 @@ export interface CreateComponentTreeModelOptions extends ComponentTreeModelOptio codeScopeValue?: StringDictionary; } -export interface IComponentTreeModelService { +export interface IComponentTreeModelService extends IDisposable { create( componentsTree: ComponentTree, options?: CreateComponentTreeModelOptions, @@ -32,23 +34,28 @@ export const IComponentTreeModelService = createDecorator( componentsTree: ComponentTree, options: CreateComponentTreeModelOptions, ): IComponentTreeModel { - return new ComponentTreeModel( - componentsTree, - // @ts-expect-error: preset scope value - this.codeRuntimeService.createCodeRuntime({ - initScopeValue: options?.codeScopeValue, - }), - options, + this._throwIfDisposed(`ComponentTreeModelService has been disposed.`); + + return this.addDispose( + new ComponentTreeModel( + componentsTree, + this.codeRuntimeService.createCodeRuntime({ + initScopeValue: options?.codeScopeValue as any, + }), + options, + ), ); } @@ -56,18 +63,11 @@ export class ComponentTreeModelService implements IComponentTreeModelService { id: string, options: CreateComponentTreeModelOptions, ): IComponentTreeModel { - const componentsTrees = this.schemaService.get('componentsTree'); + const componentsTrees = this.schemaService.get('componentsTree', []); const componentsTree = componentsTrees.find((item) => item.id === id); invariant(componentsTree, 'componentsTree not found'); - return new ComponentTreeModel( - componentsTree, - // @ts-expect-error: preset scope value - this.codeRuntimeService.createCodeRuntime({ - initScopeValue: options?.codeScopeValue, - }), - options, - ); + return this.create(componentsTree, options); } } diff --git a/packages/renderer-core/src/services/runtimeIntlService.ts b/packages/renderer-core/src/services/runtimeIntlService.ts index 551c89bc0..205a45e9f 100644 --- a/packages/renderer-core/src/services/runtimeIntlService.ts +++ b/packages/renderer-core/src/services/runtimeIntlService.ts @@ -5,6 +5,7 @@ import { type Locale, type Translations, type LocaleTranslationsMap, + Disposable, } from '@alilc/lowcode-shared'; import { ICodeRuntimeService } from './code-runtime'; @@ -26,27 +27,25 @@ export interface IRuntimeIntlService { export const IRuntimeIntlService = createDecorator('IRuntimeIntlService'); -export class RuntimeIntlService implements IRuntimeIntlService { - private intl: Intl = new Intl(); +export class RuntimeIntlService extends Disposable implements IRuntimeIntlService { + private _intl: Intl; constructor( defaultLocale: string | undefined, i18nTranslations: LocaleTranslationsMap, @ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService, ) { - if (defaultLocale) this.setLocale(defaultLocale); + super(); + this._intl = this.addDispose(new Intl(defaultLocale)); for (const key of Object.keys(i18nTranslations)) { - this.addTranslations(key, i18nTranslations[key]); + this._intl.addTranslations(key, i18nTranslations[key]); } - this._injectScope(); } localize(descriptor: MessageDescriptor): string { - const formatter = this.intl.getFormatter(); - - return formatter.$t( + return this._intl.getFormatter().$t( { id: descriptor.key, defaultMessage: descriptor.fallback, @@ -56,15 +55,15 @@ export class RuntimeIntlService implements IRuntimeIntlService { } setLocale(locale: string): void { - this.intl.setLocale(locale); + this._intl.setLocale(locale); } getLocale(): string { - return this.intl.getLocale(); + return this._intl.getLocale(); } addTranslations(locale: Locale, translations: Translations) { - this.intl.addTranslations(locale, translations); + this._intl.addTranslations(locale, translations); } private _injectScope(): void { diff --git a/packages/renderer-core/src/services/runtimeUtilService.ts b/packages/renderer-core/src/services/runtimeUtilService.ts index 73e36bcf0..9d8d2c88c 100644 --- a/packages/renderer-core/src/services/runtimeUtilService.ts +++ b/packages/renderer-core/src/services/runtimeUtilService.ts @@ -18,7 +18,7 @@ export interface IRuntimeUtilService { export const IRuntimeUtilService = createDecorator('rendererUtilService'); export class RuntimeUtilService implements IRuntimeUtilService { - private utilsMap: Map = new Map(); + private _utilsMap: Map = new Map(); constructor( utils: UtilDescription[] = [], @@ -28,7 +28,7 @@ export class RuntimeUtilService implements IRuntimeUtilService { for (const util of utils) { this.add(util); } - this.injectScope(); + this._injectScope(); } add(utilItem: UtilDescription, force?: boolean): void; @@ -54,39 +54,39 @@ export class RuntimeUtilService implements IRuntimeUtilService { force = fn as boolean; } - this.addUtilByName(name, utilObj, force); + this._addUtilByName(name, utilObj, force); } - private addUtilByName( + private _addUtilByName( name: string, fn: AnyFunction | StringDictionary | UtilDescription, force?: boolean, ): void { - if (this.utilsMap.has(name) && !force) return; + if (this._utilsMap.has(name) && !force) return; if (isPlainObject(fn)) { if ((fn as UtilDescription).type === 'function' || (fn as UtilDescription).type === 'npm') { - const utilFn = this.parseUtil(fn as UtilDescription); + const utilFn = this._parseUtil(fn as UtilDescription); if (utilFn) { - this.addUtilByName(name, utilFn, force); + this._addUtilByName(name, utilFn, force); } } else if ((fn as StringDictionary).destructuring) { for (const key of Object.keys(fn)) { - this.addUtilByName(key, (fn as StringDictionary)[key], force); + this._addUtilByName(key, (fn as StringDictionary)[key], force); } } else { - this.utilsMap.set(name, fn); + this._utilsMap.set(name, fn); } } else if (typeof fn === 'function') { - this.utilsMap.set(name, fn); + this._utilsMap.set(name, fn); } } remove(name: string): void { - this.utilsMap.delete(name); + this._utilsMap.delete(name); } - private parseUtil(utilItem: UtilDescription) { + private _parseUtil(utilItem: UtilDescription) { if (utilItem.type === 'function') { return this.codeRuntimeService.rootRuntime.run(utilItem.content.value); } else { @@ -94,16 +94,16 @@ export class RuntimeUtilService implements IRuntimeUtilService { } } - private injectScope(): void { + private _injectScope(): void { const exposed = new Proxy(Object.create(null), { get: (_, p: string) => { - return this.utilsMap.get(p); + return this._utilsMap.get(p); }, set() { return false; }, has: (_, p: string) => { - return this.utilsMap.has(p); + return this._utilsMap.has(p); }, }); diff --git a/packages/renderer-core/src/services/schema/schemaService.ts b/packages/renderer-core/src/services/schema/schemaService.ts index 6f4ba0daa..b736ea989 100644 --- a/packages/renderer-core/src/services/schema/schemaService.ts +++ b/packages/renderer-core/src/services/schema/schemaService.ts @@ -22,9 +22,9 @@ export const ISchemaService = createDecorator('schemaService'); export class SchemaService extends Disposable implements ISchemaService { private store: NormalizedSchema; - private _observer = this.addDispose(new Events.Observable()); + private _observer = this.addDispose(new Events.Emitter()); - readonly onSchemaUpdate = this._observer.subscribe; + readonly onSchemaUpdate = this._observer.event; constructor(schema: unknown) { super(); diff --git a/packages/renderer-core/src/widget/widget.ts b/packages/renderer-core/src/widget/widget.ts index 3f64251f5..fc897929e 100644 --- a/packages/renderer-core/src/widget/widget.ts +++ b/packages/renderer-core/src/widget/widget.ts @@ -6,7 +6,7 @@ export interface IWidget { readonly rawNode: NodeType; - model: IComponentTreeModel; + readonly model: IComponentTreeModel; children?: IWidget[]; } @@ -14,17 +14,26 @@ export interface IWidget { export class Widget implements IWidget { - public rawNode: NodeType; + private _key: string; - public key: string; - - public children?: IWidget[] | undefined; + children?: IWidget[] | undefined; constructor( - node: NodeType, - public model: IComponentTreeModel, + private _node: NodeType, + private _model: IComponentTreeModel, ) { - this.rawNode = node; - this.key = (node as ComponentNode)?.id ?? uniqueId(); + this._key = (_node as ComponentNode)?.id ?? uniqueId(); + } + + get rawNode(): NodeType { + return this._node; + } + + get key(): string { + return this._key; + } + + get model(): IComponentTreeModel { + return this._model; } } diff --git a/packages/shared/src/common/disposable.ts b/packages/shared/src/common/disposable.ts index 88e71fca1..ae62db7a8 100644 --- a/packages/shared/src/common/disposable.ts +++ b/packages/shared/src/common/disposable.ts @@ -32,6 +32,14 @@ export abstract class Disposable implements IDisposable { private readonly _store = new DisposableStore(); + protected _isDisposed = false; + + protected _throwIfDisposed(msg: string = 'this disposable has been disposed'): void { + if (this._isDisposed) { + throw new Error(msg); + } + } + dispose(): void { this._store.dispose(); } @@ -40,6 +48,7 @@ export abstract class Disposable implements IDisposable { * Adds `o` to the collection of disposables managed by this object. */ protected addDispose(o: T): T { + this._throwIfDisposed(); if ((o as unknown as Disposable) === this) { throw new Error('Cannot register a disposable on itself!'); } diff --git a/packages/shared/src/common/event.ts b/packages/shared/src/common/event.ts index 1e8c58324..79cbc69aa 100644 --- a/packages/shared/src/common/event.ts +++ b/packages/shared/src/common/event.ts @@ -1,18 +1,35 @@ -import { Disposable, IDisposable, toDisposable } from './disposable'; +import { AnyFunction } from '../types'; +import { combinedDisposable, Disposable, IDisposable, toDisposable } from './disposable'; -export type Event = (listener: (arg: T, thisArg?: any) => any) => IDisposable; +export type Event = (listener: (arg: T) => R, thisArg?: any) => IDisposable; -export class Observable { +export interface EmitterOptions { + /** + * Optional function that's called *before* the very first listener is added + */ + onWillAddFirstListener: AnyFunction; + /** + * Optional function that's called *after* remove the very last listener + */ + onDidRemoveLastListener: AnyFunction; +} + +export class Emitter { private _isDisposed = false; - private _event?: Event; + private _event?: Event; private _listeners?: Set<(arg: T) => void>; + constructor(private _options?: EmitterOptions) {} + dispose(): void { if (this._isDisposed) return; - this._listeners?.clear(); - this._listeners = undefined; + if (this._listeners?.size !== 0) { + this._listeners?.clear(); + this._listeners = undefined; + this._options?.onDidRemoveLastListener(); + } this._event = undefined; this._isDisposed = true; } @@ -24,9 +41,9 @@ export class Observable { } /** - * For the public to allow to subscribe to events from this Observable + * For the public to allow to subscribe to events from this Emitter */ - get subscribe(): Event { + get event(): Event { if (!this._event) { this._event = (listener: (arg: T) => void, thisArg?: any) => { if (this._isDisposed) { @@ -37,7 +54,10 @@ export class Observable { listener = listener.bind(thisArg); } - if (!this._listeners) this._listeners = new Set(); + if (!this._listeners) { + this._listeners = new Set(); + this._options?.onWillAddFirstListener(); + } this._listeners.add(listener); return toDisposable(() => { @@ -54,6 +74,143 @@ export class Observable { if (this._listeners?.has(listener)) { this._listeners.delete(listener); + + if (this._listeners.size === 0) { + this._listeners = undefined; + this._options?.onDidRemoveLastListener?.(this); + } } } } + +function snapshot(event: Event): Event { + let listener: IDisposable | undefined; + + const options: EmitterOptions | undefined = { + onWillAddFirstListener() { + listener = event(emitter.notify, emitter); + }, + onDidRemoveLastListener() { + listener?.dispose(); + }, + }; + + const emitter = new Emitter(options); + + return emitter.event; +} + +/** + * Given an event, returns another event which only fires once. + * + * @param event The event source for the new event. + */ +export function once(event: Event): Event { + return (listener, thisArgs = null) => { + // we need this, in case the event fires during the listener call + let didFire = false; + let result: IDisposable | undefined = undefined; + result = event((e) => { + if (didFire) { + return; + } else if (result) { + result.dispose(); + } else { + didFire = true; + } + + return listener.call(thisArgs, e); + }, null); + + if (didFire) { + result.dispose(); + } + + return result; + }; +} + +export function forEach(event: Event, each: (i: I) => void): Event { + return snapshot((listener, thisArgs = null) => + event((i) => { + each(i); + listener.call(thisArgs, i); + }, null), + ); +} + +/** + * Maps an event of one type into an event of another type using a mapping function, similar to how + * `Array.prototype.map` works. + * + * @param event The event source for the new event. + * @param map The mapping function. + */ +export function map(event: Event, map: (i: I) => O): Event { + return snapshot((listener, thisArgs = null) => + event((i) => listener.call(thisArgs, map(i)), null), + ); +} + +export function reduce( + event: Event, + merge: (last: O | undefined, event: I) => O, + initial?: O, +): Event { + let output: O | undefined = initial; + + return map(event, (e) => { + output = merge(output, e); + return output; + }); +} + +export function filter(event: Event, filter: (e: T | U) => e is T): Event; +export function filter(event: Event, filter: (e: T) => boolean): Event; +export function filter(event: Event, filter: (e: T | R) => e is R): Event; +export function filter(event: Event, filter: (e: T) => boolean): Event { + return snapshot((listener, thisArgs = null) => + event((e) => filter(e) && listener.call(thisArgs, e), null), + ); +} + +/** + * Given a collection of events, returns a single event which emits whenever any of the provided events emit. + */ +export function any(...events: Event[]): Event; +export function any(...events: Event[]): Event; +export function any(...events: Event[]): Event { + return (listener, thisArgs = null) => { + return combinedDisposable(...events.map((event) => event((e) => listener.call(thisArgs, e)))); + }; +} + +/** + * Creates a promise out of an event, using the {@link Event.once} helper. + */ +export function toPromise(event: Event): Promise { + return new Promise((resolve) => once(event)(resolve)); +} + +/** + * Creates an event out of a promise that fires once when the promise is + * resolved with the result of the promise or `undefined`. + */ +export function fromPromise(promise: Promise): Event { + const result = new Emitter(); + + promise + .then( + (res) => { + result.notify(res); + }, + () => { + result.notify(undefined); + }, + ) + .finally(() => { + result.dispose(); + }); + + return result.event; +} diff --git a/packages/shared/src/common/intl.ts b/packages/shared/src/common/intl.ts index 31ef55aa3..d9f034cd4 100644 --- a/packages/shared/src/common/intl.ts +++ b/packages/shared/src/common/intl.ts @@ -2,6 +2,7 @@ import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@f import { mapKeys } from 'lodash-es'; import { signal, computed, effect, type Signal, type ComputedSignal } from './signals'; import { platformLocale } from './platform'; +import { Disposable, toDisposable } from './disposable'; export { IntlFormatter }; @@ -9,59 +10,65 @@ export type Locale = string; export type Translations = Record; export type LocaleTranslationsRecord = Record; -export class Intl { - private locale: Signal; - private messageStore: Signal; - private currentMessage: ComputedSignal; - private intlShape: IntlFormatter; +export class Intl extends Disposable { + private _locale: Signal; + private _messageStore: Signal; + private _currentMessage: ComputedSignal; + private _intlShape: IntlFormatter; constructor(defaultLocale: string = platformLocale, messages: LocaleTranslationsRecord = {}) { - if (defaultLocale) { - defaultLocale = nomarlizeLocale(defaultLocale); - } else { - defaultLocale = 'zh-CN'; - } - - const messageStore = mapKeys(messages, (_, key) => { - return nomarlizeLocale(key); + super(); + + this._locale = signal(defaultLocale ? nomarlizeLocale(defaultLocale) : 'zh-CN'); + this._messageStore = signal( + mapKeys(messages, (_, key) => { + return nomarlizeLocale(key); + }), + ); + this._currentMessage = computed(() => { + return this._messageStore.value[this._locale.value] ?? {}; }); - this.locale = signal(defaultLocale); - this.messageStore = signal(messageStore); - this.currentMessage = computed(() => { - return this.messageStore.value[this.locale.value] ?? {}; - }); - - effect(() => { - const cache = createIntlCache(); - this.intlShape = createIntl( - { - locale: this.locale.value, - messages: this.currentMessage.value, - }, - cache, - ); - }); + this.addDispose( + toDisposable( + effect(() => { + const cache = createIntlCache(); + this._intlShape = createIntl( + { + locale: this._locale.value, + messages: this._currentMessage.value, + }, + cache, + ); + }), + ), + ); } getLocale() { - return this.locale.value; + return this._locale.value; } setLocale(locale: Locale) { + this._throwIfDisposed(`this intl has been disposed`); + const nomarlizedLocale = nomarlizeLocale(locale); - this.locale.value = nomarlizedLocale; + this._locale.value = nomarlizedLocale; } addTranslations(locale: Locale, messages: Translations) { + this._throwIfDisposed(`this intl has been disposed`); + locale = nomarlizeLocale(locale); - const original = this.messageStore.value[locale]; + const original = this._messageStore.value[locale]; - this.messageStore.value[locale] = Object.assign(original, messages); + this._messageStore.value[locale] = Object.assign(original, messages); } getFormatter(): IntlFormatter { - return this.intlShape; + this._throwIfDisposed(`this intl has been disposed`); + + return this._intlShape; } }