feat: add event methods, like rxjs

This commit is contained in:
1ncounter 2024-07-31 17:36:52 +08:00
parent a02b19e6e3
commit d4bdf14a1d
16 changed files with 430 additions and 181 deletions

View File

@ -83,6 +83,7 @@ export function createRenderer<RenderObject = IRenderObject>(
}, },
destroy: () => { destroy: () => {
lifeCycleService.setPhase(LifecyclePhase.Destroying); lifeCycleService.setPhase(LifecyclePhase.Destroying);
instantiationService.dispose();
}, },
}; };

View File

@ -1,5 +1,5 @@
/* --------------- api -------------------- */ /* --------------- api -------------------- */
export { createRenderer } from './main'; export { createRenderer } from './createRenderer';
export { IExtensionHostService } from './services/extension'; export { IExtensionHostService } from './services/extension';
export { definePackageLoader, IPackageManagementService } from './services/package'; export { definePackageLoader, IPackageManagementService } from './services/package';
export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService'; export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService';
@ -9,7 +9,6 @@ export { IRuntimeIntlService } from './services/runtimeIntlService';
export { IRuntimeUtilService } from './services/runtimeUtilService'; export { IRuntimeUtilService } from './services/runtimeUtilService';
export { ISchemaService } from './services/schema'; export { ISchemaService } from './services/schema';
export { Widget } from './widget'; export { Widget } from './widget';
export * from './utils/value';
/* --------------- types ---------------- */ /* --------------- types ---------------- */
export type * from './types'; export type * from './types';

View File

@ -1,31 +1,34 @@
import { import {
type StringDictionary, type StringDictionary,
type JSNode, type JSNode,
type EventDisposable, type IDisposable,
type JSExpression, type JSExpression,
type JSFunction, type JSFunction,
specTypes, specTypes,
isNode,
toDisposable,
Disposable,
} from '@alilc/lowcode-shared'; } from '@alilc/lowcode-shared';
import { type ICodeScope, CodeScope } from './codeScope'; import { type ICodeScope, CodeScope } from './codeScope';
import { isNode } from '../../../../shared/src/utils/node'; import { mapValue } from './value';
import { mapValue } from '../../utils/value';
import { evaluate } from './evaluate'; import { evaluate } from './evaluate';
export interface CodeRuntimeOptions<T extends StringDictionary = StringDictionary> { export interface CodeRuntimeOptions<T extends StringDictionary = StringDictionary> {
initScopeValue?: Partial<T>; initScopeValue?: Partial<T>;
parentScope?: ICodeScope; parentScope?: ICodeScope;
evalCodeFunction?: EvalCodeFunction; evalCodeFunction?: EvalCodeFunction;
} }
export interface ICodeRuntime<T extends StringDictionary = StringDictionary> { export interface ICodeRuntime<T extends StringDictionary = StringDictionary> extends IDisposable {
getScope(): ICodeScope<T>; getScope(): ICodeScope<T>;
run<R = unknown>(code: string, scope?: ICodeScope): R | undefined; run<R = unknown>(code: string, scope?: ICodeScope): R | undefined;
resolve(value: StringDictionary): any; resolve(value: StringDictionary): any;
onResolve(handler: NodeResolverHandler): EventDisposable; onResolve(handler: NodeResolverHandler): IDisposable;
createChild<V extends StringDictionary = StringDictionary>( createChild<V extends StringDictionary = StringDictionary>(
options: Omit<CodeRuntimeOptions<V>, 'parentScope'>, options: Omit<CodeRuntimeOptions<V>, 'parentScope'>,
@ -34,49 +37,55 @@ export interface ICodeRuntime<T extends StringDictionary = StringDictionary> {
export type NodeResolverHandler = (node: JSNode) => JSNode | false | undefined; export type NodeResolverHandler = (node: JSNode) => JSNode | false | undefined;
let onResolveHandlers: NodeResolverHandler[] = [];
export type EvalCodeFunction = (code: string, scope: any) => any; export type EvalCodeFunction = (code: string, scope: any) => any;
export class CodeRuntime<T extends StringDictionary = StringDictionary> implements ICodeRuntime<T> { export class CodeRuntime<T extends StringDictionary = StringDictionary>
private codeScope: ICodeScope<T>; extends Disposable
implements ICodeRuntime<T>
{
private _codeScope: ICodeScope<T>;
private evalCodeFunction: EvalCodeFunction = evaluate; private _evalCodeFunction: EvalCodeFunction = evaluate;
private _resolveHandlers: NodeResolverHandler[] = [];
constructor(options: CodeRuntimeOptions<T> = {}) { constructor(options: CodeRuntimeOptions<T> = {}) {
if (options.evalCodeFunction) this.evalCodeFunction = options.evalCodeFunction; super();
if (options.parentScope) { if (options.evalCodeFunction) this._evalCodeFunction = options.evalCodeFunction;
this.codeScope = options.parentScope.createChild<T>(options.initScopeValue ?? {}); this._codeScope = this.addDispose(
} else { options.parentScope
this.codeScope = new CodeScope(options.initScopeValue ?? {}); ? options.parentScope.createChild<T>(options.initScopeValue ?? {})
} : new CodeScope(options.initScopeValue ?? {}),
);
} }
getScope() { getScope() {
return this.codeScope; return this._codeScope;
} }
run<R = unknown>(code: string): R | undefined { run<R = unknown>(code: string): R | undefined {
this._throwIfDisposed(`this code runtime has been disposed`);
if (!code) return undefined; if (!code) return undefined;
try { try {
const result = this.evalCodeFunction(code, this.codeScope.value); const result = this._evalCodeFunction(code, this._codeScope.value);
return result as R; return result as R;
} catch (err) { } catch (err) {
// todo replace logger // todo replace logger
console.error('eval error', code, this.codeScope.value, err); console.error('eval error', code, this._codeScope.value, err);
return undefined; return undefined;
} }
} }
resolve(data: StringDictionary): any { resolve(data: StringDictionary): any {
if (onResolveHandlers.length > 0) { if (this._resolveHandlers.length > 0) {
data = mapValue(data, isNode, (node: JSNode) => { data = mapValue(data, isNode, (node: JSNode) => {
let newNode: JSNode | false | undefined = node; let newNode: JSNode | false | undefined = node;
for (const handler of onResolveHandlers) { for (const handler of this._resolveHandlers) {
newNode = handler(newNode as JSNode); newNode = handler(newNode as JSNode);
if (newNode === false || typeof newNode === 'undefined') { if (newNode === false || typeof newNode === 'undefined') {
break; break;
@ -110,20 +119,25 @@ export class CodeRuntime<T extends StringDictionary = StringDictionary> implemen
/** /**
* handler * handler
*/ */
onResolve(handler: NodeResolverHandler): EventDisposable { onResolve(handler: NodeResolverHandler): IDisposable {
onResolveHandlers.push(handler); this._resolveHandlers.push(handler);
return () => {
onResolveHandlers = onResolveHandlers.filter((h) => h !== handler); return this.addDispose(
}; toDisposable(() => {
this._resolveHandlers = this._resolveHandlers.filter((h) => h !== handler);
}),
);
} }
createChild<V extends StringDictionary = StringDictionary>( createChild<V extends StringDictionary = StringDictionary>(
options?: Omit<CodeRuntimeOptions<V>, 'parentScope'>, options?: Omit<CodeRuntimeOptions<V>, 'parentScope'>,
): ICodeRuntime<V> { ): ICodeRuntime<V> {
return new CodeRuntime({ return this.addDispose(
initScopeValue: options?.initScopeValue, new CodeRuntime({
parentScope: this.codeScope, initScopeValue: options?.initScopeValue,
evalCodeFunction: options?.evalCodeFunction ?? this.evalCodeFunction, parentScope: this._codeScope,
}); evalCodeFunction: options?.evalCodeFunction ?? this._evalCodeFunction,
}),
);
} }
} }

View File

@ -1,13 +1,13 @@
import { import {
createDecorator, createDecorator,
invariant,
Disposable, Disposable,
type StringDictionary, type StringDictionary,
type IDisposable,
} from '@alilc/lowcode-shared'; } from '@alilc/lowcode-shared';
import { type ICodeRuntime, type CodeRuntimeOptions, CodeRuntime } from './codeRuntime'; import { type ICodeRuntime, type CodeRuntimeOptions, CodeRuntime } from './codeRuntime';
import { ISchemaService } from '../schema'; import { ISchemaService } from '../schema';
export interface ICodeRuntimeService { export interface ICodeRuntimeService extends IDisposable {
readonly rootRuntime: ICodeRuntime; readonly rootRuntime: ICodeRuntime;
createCodeRuntime<T extends StringDictionary = StringDictionary>( createCodeRuntime<T extends StringDictionary = StringDictionary>(
@ -18,15 +18,18 @@ export interface ICodeRuntimeService {
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService'); export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
export class CodeRuntimeService extends Disposable implements ICodeRuntimeService { export class CodeRuntimeService extends Disposable implements ICodeRuntimeService {
rootRuntime: ICodeRuntime; private _rootRuntime: ICodeRuntime;
get rootRuntime() {
return this._rootRuntime;
}
constructor( constructor(
options: CodeRuntimeOptions = {}, options: CodeRuntimeOptions = {},
@ISchemaService private schemaService: ISchemaService, @ISchemaService private schemaService: ISchemaService,
) { ) {
super(); super();
this.rootRuntime = new CodeRuntime(options);
this._rootRuntime = this.addDispose(new CodeRuntime(options));
this.addDispose( this.addDispose(
this.schemaService.onSchemaUpdate(({ key, data }) => { this.schemaService.onSchemaUpdate(({ key, data }) => {
if (key === 'constants') { if (key === 'constants') {
@ -39,10 +42,10 @@ export class CodeRuntimeService extends Disposable implements ICodeRuntimeServic
createCodeRuntime<T extends StringDictionary = StringDictionary>( createCodeRuntime<T extends StringDictionary = StringDictionary>(
options: CodeRuntimeOptions<T> = {}, options: CodeRuntimeOptions<T> = {},
): ICodeRuntime<T> { ): ICodeRuntime<T> {
invariant(this.rootRuntime, `please initialize codeRuntimeService on renderer starting!`); this._throwIfDisposed();
return options.parentScope return this.addDispose(
? new CodeRuntime(options) options.parentScope ? new CodeRuntime(options) : this.rootRuntime.createChild<T>(options),
: this.rootRuntime.createChild<T>(options); );
} }
} }

View File

@ -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'; import { trustedGlobals } from './globals-es2015';
/* /*
@ -9,33 +14,50 @@ const unscopables = trustedGlobals.reduce((acc, key) => ({ ...acc, [key]: true }
__proto__: null, __proto__: null,
}); });
export interface ICodeScope<T extends StringDictionary = StringDictionary> { export interface ICodeScope<T extends StringDictionary = StringDictionary> extends IDisposable {
readonly value: T; readonly value: T;
set(name: keyof T, value: any): void; set(name: keyof T, value: any): void;
setValue(value: Partial<T>, replace?: boolean): void; setValue(value: Partial<T>, replace?: boolean): void;
createChild<V extends StringDictionary = StringDictionary>(initValue: Partial<V>): ICodeScope<V>; createChild<V extends StringDictionary = StringDictionary>(initValue: Partial<V>): ICodeScope<V>;
dispose(): void;
} }
export class CodeScope<T extends StringDictionary = StringDictionary> implements ICodeScope<T> { export class CodeScope<T extends StringDictionary = StringDictionary>
extends Disposable
implements ICodeScope<T>
{
node = LinkedListNode.Undefined; node = LinkedListNode.Undefined;
private proxyValue: T; private _proxyValue?: T;
constructor(initValue: Partial<T>) { constructor(initValue: Partial<T>) {
super();
this.node.current = initValue; this.node.current = initValue;
this.proxyValue = this.createProxy();
} }
get value(): T { 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 { set(name: keyof T, value: any): void {
this._throwIfDisposed('code scope has been disposed');
this.node.current[name] = value; this.node.current[name] = value;
} }
setValue(value: Partial<T>, replace = false) { setValue(value: Partial<T>, replace = false) {
this._throwIfDisposed('code scope has been disposed');
if (replace) { if (replace) {
this.node.current = { ...value }; this.node.current = { ...value };
} else { } else {
@ -44,24 +66,30 @@ export class CodeScope<T extends StringDictionary = StringDictionary> implements
} }
createChild<V extends StringDictionary = StringDictionary>(initValue: Partial<V>): ICodeScope<V> { createChild<V extends StringDictionary = StringDictionary>(initValue: Partial<V>): ICodeScope<V> {
const childScope = new CodeScope(initValue); const childScope = this.addDispose(new CodeScope(initValue));
childScope.node.prev = this.node; childScope.node.prev = this.node;
return childScope; 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, { return new Proxy(Object.create(null) as T, {
set: (target, p, newValue) => { set: (target, p, newValue) => {
this.set(p as string, newValue); this.set(p as string, newValue);
return true; return true;
}, },
get: (_, p) => this.findValue(p) ?? undefined, get: (_, p) => this._findValue(p) ?? undefined,
has: (_, p) => this.hasProperty(p), has: (_, p) => this._hasProperty(p),
}); });
} }
private findValue(prop: PropertyKey) { private _findValue(prop: PropertyKey) {
if (prop === Symbol.unscopables) return unscopables; if (prop === Symbol.unscopables) return unscopables;
let node = this.node; let node = this.node;
@ -73,7 +101,7 @@ export class CodeScope<T extends StringDictionary = StringDictionary> implements
} }
} }
private hasProperty(prop: PropertyKey): boolean { private _hasProperty(prop: PropertyKey): boolean {
if (prop in unscopables) return true; if (prop in unscopables) return true;
let node = this.node; let node = this.node;

View File

@ -1,3 +1,4 @@
export * from './codeScope'; export * from './codeScope';
export * from './codeRuntimeService'; export * from './codeRuntimeService';
export * from './codeRuntime'; export * from './codeRuntime';
export * from './value';

View File

@ -10,8 +10,9 @@ import {
type InstanceDataSourceApi, type InstanceDataSourceApi,
type ComponentTree, type ComponentTree,
specTypes, specTypes,
invariant,
uniqueId, uniqueId,
type IDisposable,
Disposable,
} from '@alilc/lowcode-shared'; } from '@alilc/lowcode-shared';
import { type ICodeRuntime } from '../code-runtime'; import { type ICodeRuntime } from '../code-runtime';
import { IWidget, Widget } from '../../widget'; import { IWidget, Widget } from '../../widget';
@ -60,7 +61,7 @@ export type ModelDataSourceCreator = (
codeRuntime: ICodeRuntime<InstanceApi>, codeRuntime: ICodeRuntime<InstanceApi>,
) => InstanceDataSourceApi; ) => InstanceDataSourceApi;
export interface ComponentTreeModelOptions { export interface ComponentTreeModelOptions extends IDisposable {
id?: string; id?: string;
metadata?: StringDictionary; metadata?: StringDictionary;
@ -69,37 +70,53 @@ export interface ComponentTreeModelOptions {
} }
export class ComponentTreeModel<Component, ComponentInstance = unknown> export class ComponentTreeModel<Component, ComponentInstance = unknown>
extends Disposable
implements IComponentTreeModel<Component, ComponentInstance> implements IComponentTreeModel<Component, ComponentInstance>
{ {
private instanceMap = new Map<string, ComponentInstance[]>(); private _instanceMap = new Map<string, ComponentInstance[]>();
public id: string; private _id: string;
public widgets: IWidget<Component>[] = []; private _widgets?: IWidget<Component>[];
public metadata: StringDictionary = {}; private _metadata: StringDictionary;
constructor( constructor(
public componentsTree: ComponentTree, private _componentsTree: ComponentTree,
public codeRuntime: ICodeRuntime<InstanceApi>, private _codeRuntime: ICodeRuntime<InstanceApi>,
options: ComponentTreeModelOptions, options: ComponentTreeModelOptions,
) { ) {
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel'); super();
this.id = options?.id ?? `model_${uniqueId()}`;
if (options?.metadata) {
this.metadata = options.metadata;
}
if (componentsTree.children) {
this.widgets = this.buildWidgets(componentsTree.children);
}
this._id = options?.id ?? `model_${uniqueId()}`;
this._metadata = options?.metadata ?? {};
this.initialize(options); this.initialize(options);
this.addDispose(_codeRuntime);
}
get id() {
return this._id;
}
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) { 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 codeScope = this.codeRuntime.getScope();
const initalProps = this.codeRuntime.resolve(props); const initalProps = this.codeRuntime.resolve(props);
@ -119,12 +136,12 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
Object.assign( Object.assign(
{ {
$: (ref: string) => { $: (ref: string) => {
const insArr = this.instanceMap.get(ref); const insArr = this._instanceMap.get(ref);
if (!insArr) return undefined; if (!insArr) return undefined;
return insArr[0]; return insArr[0];
}, },
$$: (ref: string) => { $$: (ref: string) => {
return this.instanceMap.get(ref) ?? []; return this._instanceMap.get(ref) ?? [];
}, },
}, },
dataSourceApi, dataSourceApi,
@ -140,19 +157,19 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
} }
getCssText(): string | undefined { getCssText(): string | undefined {
return this.componentsTree.css; return this._componentsTree.css;
} }
triggerLifeCycle(lifeCycleName: ComponentLifeCycle, ...args: any[]) { triggerLifeCycle(lifeCycleName: ComponentLifeCycle, ...args: any[]) {
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象 // keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
if ( if (
!this.componentsTree.lifeCycles || !this._componentsTree.lifeCycles ||
!Object.keys(this.componentsTree.lifeCycles).includes(lifeCycleName) !Object.keys(this._componentsTree.lifeCycles).includes(lifeCycleName)
) { ) {
return; return;
} }
const lifeCycleSchema = this.componentsTree.lifeCycles[lifeCycleName]; const lifeCycleSchema = this._componentsTree.lifeCycles[lifeCycleName];
const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema); const lifeCycleFn = this.codeRuntime.resolve(lifeCycleSchema);
if (typeof lifeCycleFn === 'function') { if (typeof lifeCycleFn === 'function') {
@ -161,22 +178,22 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
} }
setComponentRef(ref: string, ins: ComponentInstance) { setComponentRef(ref: string, ins: ComponentInstance) {
let insArr = this.instanceMap.get(ref); let insArr = this._instanceMap.get(ref);
if (!insArr) { if (!insArr) {
insArr = []; insArr = [];
this.instanceMap.set(ref, insArr); this._instanceMap.set(ref, insArr);
} }
insArr!.push(ins); insArr!.push(ins);
} }
removeComponentRef(ref: string, ins?: ComponentInstance) { removeComponentRef(ref: string, ins?: ComponentInstance) {
const insArr = this.instanceMap.get(ref); const insArr = this._instanceMap.get(ref);
if (insArr) { if (insArr) {
if (ins) { if (ins) {
const idx = insArr.indexOf(ins); const idx = insArr.indexOf(ins);
if (idx > 0) insArr.splice(idx, 1); if (idx > 0) insArr.splice(idx, 1);
} else { } else {
this.instanceMap.delete(ref); this._instanceMap.delete(ref);
} }
} }
} }
@ -197,6 +214,11 @@ export class ComponentTreeModel<Component, ComponentInstance = unknown>
} }
}); });
} }
dispose(): void {
super.dispose();
this._instanceMap.clear();
}
} }
export function normalizeComponentNode(node: ComponentNode): NormalizedComponentNode { export function normalizeComponentNode(node: ComponentNode): NormalizedComponentNode {

View File

@ -1,5 +1,7 @@
import { import {
createDecorator, createDecorator,
type IDisposable,
Disposable,
invariant, invariant,
type ComponentTree, type ComponentTree,
type StringDictionary, type StringDictionary,
@ -16,7 +18,7 @@ export interface CreateComponentTreeModelOptions extends ComponentTreeModelOptio
codeScopeValue?: StringDictionary; codeScopeValue?: StringDictionary;
} }
export interface IComponentTreeModelService { export interface IComponentTreeModelService extends IDisposable {
create<Component>( create<Component>(
componentsTree: ComponentTree, componentsTree: ComponentTree,
options?: CreateComponentTreeModelOptions, options?: CreateComponentTreeModelOptions,
@ -32,23 +34,28 @@ export const IComponentTreeModelService = createDecorator<IComponentTreeModelSer
'componentTreeModelService', 'componentTreeModelService',
); );
export class ComponentTreeModelService implements IComponentTreeModelService { export class ComponentTreeModelService extends Disposable implements IComponentTreeModelService {
constructor( constructor(
@ISchemaService private schemaService: ISchemaService, @ISchemaService private schemaService: ISchemaService,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService, @ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) {} ) {
super();
}
create<Component>( create<Component>(
componentsTree: ComponentTree, componentsTree: ComponentTree,
options: CreateComponentTreeModelOptions, options: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component> { ): IComponentTreeModel<Component> {
return new ComponentTreeModel( this._throwIfDisposed(`ComponentTreeModelService has been disposed.`);
componentsTree,
// @ts-expect-error: preset scope value return this.addDispose(
this.codeRuntimeService.createCodeRuntime({ new ComponentTreeModel(
initScopeValue: options?.codeScopeValue, componentsTree,
}), this.codeRuntimeService.createCodeRuntime({
options, initScopeValue: options?.codeScopeValue as any,
}),
options,
),
); );
} }
@ -56,18 +63,11 @@ export class ComponentTreeModelService implements IComponentTreeModelService {
id: string, id: string,
options: CreateComponentTreeModelOptions, options: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component> { ): IComponentTreeModel<Component> {
const componentsTrees = this.schemaService.get('componentsTree'); const componentsTrees = this.schemaService.get<ComponentTree[]>('componentsTree', []);
const componentsTree = componentsTrees.find((item) => item.id === id); const componentsTree = componentsTrees.find((item) => item.id === id);
invariant(componentsTree, 'componentsTree not found'); invariant(componentsTree, 'componentsTree not found');
return new ComponentTreeModel( return this.create(componentsTree, options);
componentsTree,
// @ts-expect-error: preset scope value
this.codeRuntimeService.createCodeRuntime({
initScopeValue: options?.codeScopeValue,
}),
options,
);
} }
} }

View File

@ -5,6 +5,7 @@ import {
type Locale, type Locale,
type Translations, type Translations,
type LocaleTranslationsMap, type LocaleTranslationsMap,
Disposable,
} from '@alilc/lowcode-shared'; } from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from './code-runtime'; import { ICodeRuntimeService } from './code-runtime';
@ -26,27 +27,25 @@ export interface IRuntimeIntlService {
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService'); export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
export class RuntimeIntlService implements IRuntimeIntlService { export class RuntimeIntlService extends Disposable implements IRuntimeIntlService {
private intl: Intl = new Intl(); private _intl: Intl;
constructor( constructor(
defaultLocale: string | undefined, defaultLocale: string | undefined,
i18nTranslations: LocaleTranslationsMap, i18nTranslations: LocaleTranslationsMap,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService, @ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) { ) {
if (defaultLocale) this.setLocale(defaultLocale); super();
this._intl = this.addDispose(new Intl(defaultLocale));
for (const key of Object.keys(i18nTranslations)) { for (const key of Object.keys(i18nTranslations)) {
this.addTranslations(key, i18nTranslations[key]); this._intl.addTranslations(key, i18nTranslations[key]);
} }
this._injectScope(); this._injectScope();
} }
localize(descriptor: MessageDescriptor): string { localize(descriptor: MessageDescriptor): string {
const formatter = this.intl.getFormatter(); return this._intl.getFormatter().$t(
return formatter.$t(
{ {
id: descriptor.key, id: descriptor.key,
defaultMessage: descriptor.fallback, defaultMessage: descriptor.fallback,
@ -56,15 +55,15 @@ export class RuntimeIntlService implements IRuntimeIntlService {
} }
setLocale(locale: string): void { setLocale(locale: string): void {
this.intl.setLocale(locale); this._intl.setLocale(locale);
} }
getLocale(): string { getLocale(): string {
return this.intl.getLocale(); return this._intl.getLocale();
} }
addTranslations(locale: Locale, translations: Translations) { addTranslations(locale: Locale, translations: Translations) {
this.intl.addTranslations(locale, translations); this._intl.addTranslations(locale, translations);
} }
private _injectScope(): void { private _injectScope(): void {

View File

@ -18,7 +18,7 @@ export interface IRuntimeUtilService {
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService'); export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
export class RuntimeUtilService implements IRuntimeUtilService { export class RuntimeUtilService implements IRuntimeUtilService {
private utilsMap: Map<string, any> = new Map(); private _utilsMap: Map<string, any> = new Map();
constructor( constructor(
utils: UtilDescription[] = [], utils: UtilDescription[] = [],
@ -28,7 +28,7 @@ export class RuntimeUtilService implements IRuntimeUtilService {
for (const util of utils) { for (const util of utils) {
this.add(util); this.add(util);
} }
this.injectScope(); this._injectScope();
} }
add(utilItem: UtilDescription, force?: boolean): void; add(utilItem: UtilDescription, force?: boolean): void;
@ -54,39 +54,39 @@ export class RuntimeUtilService implements IRuntimeUtilService {
force = fn as boolean; force = fn as boolean;
} }
this.addUtilByName(name, utilObj, force); this._addUtilByName(name, utilObj, force);
} }
private addUtilByName( private _addUtilByName(
name: string, name: string,
fn: AnyFunction | StringDictionary | UtilDescription, fn: AnyFunction | StringDictionary | UtilDescription,
force?: boolean, force?: boolean,
): void { ): void {
if (this.utilsMap.has(name) && !force) return; if (this._utilsMap.has(name) && !force) return;
if (isPlainObject(fn)) { if (isPlainObject(fn)) {
if ((fn as UtilDescription).type === 'function' || (fn as UtilDescription).type === 'npm') { 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) { if (utilFn) {
this.addUtilByName(name, utilFn, force); this._addUtilByName(name, utilFn, force);
} }
} else if ((fn as StringDictionary).destructuring) { } else if ((fn as StringDictionary).destructuring) {
for (const key of Object.keys(fn)) { for (const key of Object.keys(fn)) {
this.addUtilByName(key, (fn as StringDictionary)[key], force); this._addUtilByName(key, (fn as StringDictionary)[key], force);
} }
} else { } else {
this.utilsMap.set(name, fn); this._utilsMap.set(name, fn);
} }
} else if (typeof fn === 'function') { } else if (typeof fn === 'function') {
this.utilsMap.set(name, fn); this._utilsMap.set(name, fn);
} }
} }
remove(name: string): void { remove(name: string): void {
this.utilsMap.delete(name); this._utilsMap.delete(name);
} }
private parseUtil(utilItem: UtilDescription) { private _parseUtil(utilItem: UtilDescription) {
if (utilItem.type === 'function') { if (utilItem.type === 'function') {
return this.codeRuntimeService.rootRuntime.run(utilItem.content.value); return this.codeRuntimeService.rootRuntime.run(utilItem.content.value);
} else { } else {
@ -94,16 +94,16 @@ export class RuntimeUtilService implements IRuntimeUtilService {
} }
} }
private injectScope(): void { private _injectScope(): void {
const exposed = new Proxy(Object.create(null), { const exposed = new Proxy(Object.create(null), {
get: (_, p: string) => { get: (_, p: string) => {
return this.utilsMap.get(p); return this._utilsMap.get(p);
}, },
set() { set() {
return false; return false;
}, },
has: (_, p: string) => { has: (_, p: string) => {
return this.utilsMap.has(p); return this._utilsMap.has(p);
}, },
}); });

View File

@ -22,9 +22,9 @@ export const ISchemaService = createDecorator<ISchemaService>('schemaService');
export class SchemaService extends Disposable implements ISchemaService { export class SchemaService extends Disposable implements ISchemaService {
private store: NormalizedSchema; private store: NormalizedSchema;
private _observer = this.addDispose(new Events.Observable<SchemaUpdateEvent>()); private _observer = this.addDispose(new Events.Emitter<SchemaUpdateEvent>());
readonly onSchemaUpdate = this._observer.subscribe; readonly onSchemaUpdate = this._observer.event;
constructor(schema: unknown) { constructor(schema: unknown) {
super(); super();

View File

@ -6,7 +6,7 @@ export interface IWidget<Component, ComponentInstance = unknown> {
readonly rawNode: NodeType; readonly rawNode: NodeType;
model: IComponentTreeModel<Component, ComponentInstance>; readonly model: IComponentTreeModel<Component, ComponentInstance>;
children?: IWidget<Component, ComponentInstance>[]; children?: IWidget<Component, ComponentInstance>[];
} }
@ -14,17 +14,26 @@ export interface IWidget<Component, ComponentInstance = unknown> {
export class Widget<Component, ComponentInstance = unknown> export class Widget<Component, ComponentInstance = unknown>
implements IWidget<Component, ComponentInstance> implements IWidget<Component, ComponentInstance>
{ {
public rawNode: NodeType; private _key: string;
public key: string; children?: IWidget<Component, ComponentInstance>[] | undefined;
public children?: IWidget<Component, ComponentInstance>[] | undefined;
constructor( constructor(
node: NodeType, private _node: NodeType,
public model: IComponentTreeModel<Component, ComponentInstance>, private _model: IComponentTreeModel<Component, ComponentInstance>,
) { ) {
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<Component, ComponentInstance> {
return this._model;
} }
} }

View File

@ -32,6 +32,14 @@ export abstract class Disposable implements IDisposable {
private readonly _store = new DisposableStore(); 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 { dispose(): void {
this._store.dispose(); this._store.dispose();
} }
@ -40,6 +48,7 @@ export abstract class Disposable implements IDisposable {
* Adds `o` to the collection of disposables managed by this object. * Adds `o` to the collection of disposables managed by this object.
*/ */
protected addDispose<T extends IDisposable>(o: T): T { protected addDispose<T extends IDisposable>(o: T): T {
this._throwIfDisposed();
if ((o as unknown as Disposable) === this) { if ((o as unknown as Disposable) === this) {
throw new Error('Cannot register a disposable on itself!'); throw new Error('Cannot register a disposable on itself!');
} }

View File

@ -1,18 +1,35 @@
import { Disposable, IDisposable, toDisposable } from './disposable'; import { AnyFunction } from '../types';
import { combinedDisposable, Disposable, IDisposable, toDisposable } from './disposable';
export type Event<T> = (listener: (arg: T, thisArg?: any) => any) => IDisposable; export type Event<T, R = any> = (listener: (arg: T) => R, thisArg?: any) => IDisposable;
export class Observable<T> { 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<T, R = any> {
private _isDisposed = false; private _isDisposed = false;
private _event?: Event<T>; private _event?: Event<T, R>;
private _listeners?: Set<(arg: T) => void>; private _listeners?: Set<(arg: T) => void>;
constructor(private _options?: EmitterOptions) {}
dispose(): void { dispose(): void {
if (this._isDisposed) return; if (this._isDisposed) return;
this._listeners?.clear(); if (this._listeners?.size !== 0) {
this._listeners = undefined; this._listeners?.clear();
this._listeners = undefined;
this._options?.onDidRemoveLastListener();
}
this._event = undefined; this._event = undefined;
this._isDisposed = true; this._isDisposed = true;
} }
@ -24,9 +41,9 @@ export class Observable<T> {
} }
/** /**
* 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<T> { get event(): Event<T, R> {
if (!this._event) { if (!this._event) {
this._event = (listener: (arg: T) => void, thisArg?: any) => { this._event = (listener: (arg: T) => void, thisArg?: any) => {
if (this._isDisposed) { if (this._isDisposed) {
@ -37,7 +54,10 @@ export class Observable<T> {
listener = listener.bind(thisArg); 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); this._listeners.add(listener);
return toDisposable(() => { return toDisposable(() => {
@ -54,6 +74,143 @@ export class Observable<T> {
if (this._listeners?.has(listener)) { if (this._listeners?.has(listener)) {
this._listeners.delete(listener); this._listeners.delete(listener);
if (this._listeners.size === 0) {
this._listeners = undefined;
this._options?.onDidRemoveLastListener?.(this);
}
} }
} }
} }
function snapshot<T>(event: Event<T>): Event<T> {
let listener: IDisposable | undefined;
const options: EmitterOptions | undefined = {
onWillAddFirstListener() {
listener = event(emitter.notify, emitter);
},
onDidRemoveLastListener() {
listener?.dispose();
},
};
const emitter = new Emitter<T>(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<T>(event: Event<T>): Event<T> {
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<I>(event: Event<I>, each: (i: I) => void): Event<I> {
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<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
return snapshot((listener, thisArgs = null) =>
event((i) => listener.call(thisArgs, map(i)), null),
);
}
export function reduce<I, O>(
event: Event<I>,
merge: (last: O | undefined, event: I) => O,
initial?: O,
): Event<O> {
let output: O | undefined = initial;
return map<I, O>(event, (e) => {
output = merge(output, e);
return output;
});
}
export function filter<T, U>(event: Event<T | U>, filter: (e: T | U) => e is T): Event<T>;
export function filter<T>(event: Event<T>, filter: (e: T) => boolean): Event<T>;
export function filter<T, R>(event: Event<T | R>, filter: (e: T | R) => e is R): Event<R>;
export function filter<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
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<T>(...events: Event<T>[]): Event<T>;
export function any(...events: Event<any>[]): Event<void>;
export function any<T>(...events: Event<T>[]): Event<T> {
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<T>(event: Event<T>): Promise<T> {
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<T>(promise: Promise<T>): Event<T | undefined> {
const result = new Emitter<T | undefined>();
promise
.then(
(res) => {
result.notify(res);
},
() => {
result.notify(undefined);
},
)
.finally(() => {
result.dispose();
});
return result.event;
}

View File

@ -2,6 +2,7 @@ import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@f
import { mapKeys } from 'lodash-es'; import { mapKeys } from 'lodash-es';
import { signal, computed, effect, type Signal, type ComputedSignal } from './signals'; import { signal, computed, effect, type Signal, type ComputedSignal } from './signals';
import { platformLocale } from './platform'; import { platformLocale } from './platform';
import { Disposable, toDisposable } from './disposable';
export { IntlFormatter }; export { IntlFormatter };
@ -9,59 +10,65 @@ export type Locale = string;
export type Translations = Record<string, string>; export type Translations = Record<string, string>;
export type LocaleTranslationsRecord = Record<Locale, Translations>; export type LocaleTranslationsRecord = Record<Locale, Translations>;
export class Intl { export class Intl extends Disposable {
private locale: Signal<Locale>; private _locale: Signal<Locale>;
private messageStore: Signal<LocaleTranslationsRecord>; private _messageStore: Signal<LocaleTranslationsRecord>;
private currentMessage: ComputedSignal<Translations>; private _currentMessage: ComputedSignal<Translations>;
private intlShape: IntlFormatter; private _intlShape: IntlFormatter;
constructor(defaultLocale: string = platformLocale, messages: LocaleTranslationsRecord = {}) { constructor(defaultLocale: string = platformLocale, messages: LocaleTranslationsRecord = {}) {
if (defaultLocale) { super();
defaultLocale = nomarlizeLocale(defaultLocale);
} else {
defaultLocale = 'zh-CN';
}
const messageStore = mapKeys(messages, (_, key) => { this._locale = signal(defaultLocale ? nomarlizeLocale(defaultLocale) : 'zh-CN');
return nomarlizeLocale(key); this._messageStore = signal(
mapKeys(messages, (_, key) => {
return nomarlizeLocale(key);
}),
);
this._currentMessage = computed(() => {
return this._messageStore.value[this._locale.value] ?? {};
}); });
this.locale = signal(defaultLocale); this.addDispose(
this.messageStore = signal(messageStore); toDisposable(
this.currentMessage = computed(() => { effect(() => {
return this.messageStore.value[this.locale.value] ?? {}; const cache = createIntlCache();
}); this._intlShape = createIntl(
{
effect(() => { locale: this._locale.value,
const cache = createIntlCache(); messages: this._currentMessage.value,
this.intlShape = createIntl( },
{ cache,
locale: this.locale.value, );
messages: this.currentMessage.value, }),
}, ),
cache, );
);
});
} }
getLocale() { getLocale() {
return this.locale.value; return this._locale.value;
} }
setLocale(locale: Locale) { setLocale(locale: Locale) {
this._throwIfDisposed(`this intl has been disposed`);
const nomarlizedLocale = nomarlizeLocale(locale); const nomarlizedLocale = nomarlizeLocale(locale);
this.locale.value = nomarlizedLocale; this._locale.value = nomarlizedLocale;
} }
addTranslations(locale: Locale, messages: Translations) { addTranslations(locale: Locale, messages: Translations) {
locale = nomarlizeLocale(locale); this._throwIfDisposed(`this intl has been disposed`);
const original = this.messageStore.value[locale];
this.messageStore.value[locale] = Object.assign(original, messages); locale = nomarlizeLocale(locale);
const original = this._messageStore.value[locale];
this._messageStore.value[locale] = Object.assign(original, messages);
} }
getFormatter(): IntlFormatter { getFormatter(): IntlFormatter {
return this.intlShape; this._throwIfDisposed(`this intl has been disposed`);
return this._intlShape;
} }
} }