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: () => {
lifeCycleService.setPhase(LifecyclePhase.Destroying);
instantiationService.dispose();
},
};

View File

@ -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';

View File

@ -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<T extends StringDictionary = StringDictionary> {
initScopeValue?: Partial<T>;
parentScope?: ICodeScope;
evalCodeFunction?: EvalCodeFunction;
}
export interface ICodeRuntime<T extends StringDictionary = StringDictionary> {
export interface ICodeRuntime<T extends StringDictionary = StringDictionary> extends IDisposable {
getScope(): ICodeScope<T>;
run<R = unknown>(code: string, scope?: ICodeScope): R | undefined;
resolve(value: StringDictionary): any;
onResolve(handler: NodeResolverHandler): EventDisposable;
onResolve(handler: NodeResolverHandler): IDisposable;
createChild<V extends StringDictionary = StringDictionary>(
options: Omit<CodeRuntimeOptions<V>, 'parentScope'>,
@ -34,49 +37,55 @@ export interface ICodeRuntime<T extends StringDictionary = StringDictionary> {
export type NodeResolverHandler = (node: JSNode) => JSNode | false | undefined;
let onResolveHandlers: NodeResolverHandler[] = [];
export type EvalCodeFunction = (code: string, scope: any) => any;
export class CodeRuntime<T extends StringDictionary = StringDictionary> implements ICodeRuntime<T> {
private codeScope: ICodeScope<T>;
export class CodeRuntime<T extends StringDictionary = StringDictionary>
extends Disposable
implements ICodeRuntime<T>
{
private _codeScope: ICodeScope<T>;
private evalCodeFunction: EvalCodeFunction = evaluate;
private _evalCodeFunction: EvalCodeFunction = evaluate;
private _resolveHandlers: NodeResolverHandler[] = [];
constructor(options: CodeRuntimeOptions<T> = {}) {
if (options.evalCodeFunction) this.evalCodeFunction = options.evalCodeFunction;
super();
if (options.parentScope) {
this.codeScope = options.parentScope.createChild<T>(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<T>(options.initScopeValue ?? {})
: new CodeScope(options.initScopeValue ?? {}),
);
}
getScope() {
return this.codeScope;
return this._codeScope;
}
run<R = unknown>(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<T extends StringDictionary = StringDictionary> 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<V extends StringDictionary = StringDictionary>(
options?: Omit<CodeRuntimeOptions<V>, 'parentScope'>,
): ICodeRuntime<V> {
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,
}),
);
}
}

View File

@ -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<T extends StringDictionary = StringDictionary>(
@ -18,15 +18,18 @@ export interface ICodeRuntimeService {
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('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<T extends StringDictionary = StringDictionary>(
options: CodeRuntimeOptions<T> = {},
): ICodeRuntime<T> {
invariant(this.rootRuntime, `please initialize codeRuntimeService on renderer starting!`);
this._throwIfDisposed();
return options.parentScope
? new CodeRuntime(options)
: this.rootRuntime.createChild<T>(options);
return this.addDispose(
options.parentScope ? new CodeRuntime(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';
/*
@ -9,33 +14,50 @@ const unscopables = trustedGlobals.reduce((acc, key) => ({ ...acc, [key]: true }
__proto__: null,
});
export interface ICodeScope<T extends StringDictionary = StringDictionary> {
export interface ICodeScope<T extends StringDictionary = StringDictionary> extends IDisposable {
readonly value: T;
set(name: keyof T, value: any): void;
setValue(value: Partial<T>, replace?: boolean): void;
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;
private proxyValue: T;
private _proxyValue?: T;
constructor(initValue: Partial<T>) {
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<T>, replace = false) {
this._throwIfDisposed('code scope has been disposed');
if (replace) {
this.node.current = { ...value };
} else {
@ -44,24 +66,30 @@ export class CodeScope<T extends StringDictionary = StringDictionary> implements
}
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;
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<T extends StringDictionary = StringDictionary> implements
}
}
private hasProperty(prop: PropertyKey): boolean {
private _hasProperty(prop: PropertyKey): boolean {
if (prop in unscopables) return true;
let node = this.node;

View File

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

View File

@ -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<InstanceApi>,
) => InstanceDataSourceApi;
export interface ComponentTreeModelOptions {
export interface ComponentTreeModelOptions extends IDisposable {
id?: string;
metadata?: StringDictionary;
@ -69,37 +70,53 @@ export interface ComponentTreeModelOptions {
}
export class ComponentTreeModel<Component, ComponentInstance = unknown>
extends Disposable
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(
public componentsTree: ComponentTree,
public codeRuntime: ICodeRuntime<InstanceApi>,
private _componentsTree: ComponentTree,
private _codeRuntime: ICodeRuntime<InstanceApi>,
options: ComponentTreeModelOptions,
) {
invariant(componentsTree, 'componentsTree must to provide', 'ComponentTreeModel');
this.id = options?.id ?? `model_${uniqueId()}`;
if (options?.metadata) {
this.metadata = options.metadata;
}
if (componentsTree.children) {
this.widgets = this.buildWidgets(componentsTree.children);
}
super();
this._id = options?.id ?? `model_${uniqueId()}`;
this._metadata = options?.metadata ?? {};
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) {
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<Component, ComponentInstance = unknown>
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<Component, ComponentInstance = unknown>
}
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<Component, ComponentInstance = unknown>
}
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<Component, ComponentInstance = unknown>
}
});
}
dispose(): void {
super.dispose();
this._instanceMap.clear();
}
}
export function normalizeComponentNode(node: ComponentNode): NormalizedComponentNode {

View File

@ -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<Component>(
componentsTree: ComponentTree,
options?: CreateComponentTreeModelOptions,
@ -32,23 +34,28 @@ export const IComponentTreeModelService = createDecorator<IComponentTreeModelSer
'componentTreeModelService',
);
export class ComponentTreeModelService implements IComponentTreeModelService {
export class ComponentTreeModelService extends Disposable implements IComponentTreeModelService {
constructor(
@ISchemaService private schemaService: ISchemaService,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) {}
) {
super();
}
create<Component>(
componentsTree: ComponentTree,
options: CreateComponentTreeModelOptions,
): IComponentTreeModel<Component> {
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<Component> {
const componentsTrees = this.schemaService.get('componentsTree');
const componentsTrees = this.schemaService.get<ComponentTree[]>('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);
}
}

View File

@ -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>('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 {

View File

@ -18,7 +18,7 @@ export interface IRuntimeUtilService {
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
export class RuntimeUtilService implements IRuntimeUtilService {
private utilsMap: Map<string, any> = new Map();
private _utilsMap: Map<string, any> = 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);
},
});

View File

@ -22,9 +22,9 @@ export const ISchemaService = createDecorator<ISchemaService>('schemaService');
export class SchemaService extends Disposable implements ISchemaService {
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) {
super();

View File

@ -6,7 +6,7 @@ export interface IWidget<Component, ComponentInstance = unknown> {
readonly rawNode: NodeType;
model: IComponentTreeModel<Component, ComponentInstance>;
readonly model: IComponentTreeModel<Component, ComponentInstance>;
children?: IWidget<Component, ComponentInstance>[];
}
@ -14,17 +14,26 @@ export interface IWidget<Component, ComponentInstance = unknown> {
export class Widget<Component, ComponentInstance = unknown>
implements IWidget<Component, ComponentInstance>
{
public rawNode: NodeType;
private _key: string;
public key: string;
public children?: IWidget<Component, ComponentInstance>[] | undefined;
children?: IWidget<Component, ComponentInstance>[] | undefined;
constructor(
node: NodeType,
public model: IComponentTreeModel<Component, ComponentInstance>,
private _node: NodeType,
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();
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<T extends IDisposable>(o: T): T {
this._throwIfDisposed();
if ((o as unknown as Disposable) === this) {
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 _event?: Event<T>;
private _event?: Event<T, R>;
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<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) {
this._event = (listener: (arg: T) => void, thisArg?: any) => {
if (this._isDisposed) {
@ -37,7 +54,10 @@ export class Observable<T> {
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<T> {
if (this._listeners?.has(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 { 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<string, string>;
export type LocaleTranslationsRecord = Record<Locale, Translations>;
export class Intl {
private locale: Signal<Locale>;
private messageStore: Signal<LocaleTranslationsRecord>;
private currentMessage: ComputedSignal<Translations>;
private intlShape: IntlFormatter;
export class Intl extends Disposable {
private _locale: Signal<Locale>;
private _messageStore: Signal<LocaleTranslationsRecord>;
private _currentMessage: ComputedSignal<Translations>;
private _intlShape: IntlFormatter;
constructor(defaultLocale: string = platformLocale, messages: LocaleTranslationsRecord = {}) {
if (defaultLocale) {
defaultLocale = nomarlizeLocale(defaultLocale);
} else {
defaultLocale = 'zh-CN';
}
super();
const messageStore = mapKeys(messages, (_, key) => {
return nomarlizeLocale(key);
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) {
locale = nomarlizeLocale(locale);
const original = this.messageStore.value[locale];
this._throwIfDisposed(`this intl has been disposed`);
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 {
return this.intlShape;
this._throwIfDisposed(`this intl has been disposed`);
return this._intlShape;
}
}