refactor: diService、eventManager

This commit is contained in:
1ncounter 2024-07-30 19:56:43 +08:00
parent 9d0f178a06
commit 006b9b615e
69 changed files with 2947 additions and 947 deletions

View File

@ -9,7 +9,7 @@ import {
Iterable,
} from '@alilc/lowcode-shared';
import { ICommand, ICommandHandler } from './command';
import { Extensions, Registry } from '../common/registry';
import { Extensions, Registry } from '../extension/registry';
import { ICommandService } from './commandService';
export type ICommandsMap = Map<string, ICommand>;
@ -26,7 +26,7 @@ export interface ICommandRegistry {
getCommands(): ICommandsMap;
}
class CommandsRegistry implements ICommandRegistry {
class CommandsRegistryImpl implements ICommandRegistry {
private readonly _commands = new Map<string, LinkedList<ICommand>>();
private readonly _didRegisterCommandEmitter = new Emitter<string>();
@ -111,6 +111,6 @@ class CommandsRegistry implements ICommandRegistry {
}
}
const commandsRegistry = new CommandsRegistry();
export const CommandsRegistry = new CommandsRegistryImpl();
Registry.add(Extensions.Command, commandsRegistry);
Registry.add(Extensions.Command, CommandsRegistry);

View File

@ -1,6 +1,5 @@
import { createDecorator, Provide, IInstantiationService } from '@alilc/lowcode-shared';
import { Registry, Extensions } from '../common/registry';
import { ICommandRegistry } from './commandRegistry';
import { createDecorator, IInstantiationService } from '@alilc/lowcode-shared';
import { CommandsRegistry } from './commandRegistry';
export interface ICommandService {
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
@ -8,7 +7,6 @@ export interface ICommandService {
export const ICommandService = createDecorator<ICommandService>('commandService');
@Provide(ICommandService)
export class CommandService implements ICommandService {
constructor(@IInstantiationService private instantiationService: IInstantiationService) {}
@ -17,7 +15,7 @@ export class CommandService implements ICommandService {
}
private tryExecuteCommand(id: string, args: any[]): Promise<any> {
const command = Registry.as<ICommandRegistry>(Extensions.Command).getCommand(id);
const command = CommandsRegistry.getCommand(id);
if (!command) {
return Promise.reject(new Error(`command '${id}' not found`));
}

View File

@ -8,7 +8,7 @@ import {
types,
} from '@alilc/lowcode-shared';
import { isUndefined, isObject } from 'lodash-es';
import { Extensions, Registry } from '../common/registry';
import { Extensions, Registry } from '../extension/registry';
import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration';
export interface IConfigurationRegistry {
@ -133,9 +133,9 @@ export const allSettings: {
patternProperties: StringDictionary<IConfigurationPropertySchema>;
} = { properties: {}, patternProperties: {} };
export class ConfigurationRegistry implements IConfigurationRegistry {
export class ConfigurationRegistryImpl implements IConfigurationRegistry {
private registeredConfigurationDefaults: IConfigurationDefaults[] = [];
private configurationDefaultsOverrides: Map<
private readonly configurationDefaultsOverrides: Map<
string,
{
configurationDefaultOverrides: IConfigurationDefaultOverride[];
@ -143,8 +143,8 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
}
>;
private configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
private excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
private readonly configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
private readonly excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
private overrideIdentifiers = new Set<string>();
private propertiesChangeEmitter = new Emitter<{
@ -645,4 +645,6 @@ function isSameExtension(a?: IExtensionInfo, b?: IExtensionInfo): boolean {
return a.id === b.id && a.version === b.version;
}
Registry.add(Extensions.Configuration, new ConfigurationRegistry());
export const ConfigurationRegistry = new ConfigurationRegistryImpl();
Registry.add(Extensions.Configuration, ConfigurationRegistry);

View File

@ -1,10 +1,4 @@
import {
createDecorator,
Emitter,
Provide,
type Event,
type EventListener,
} from '@alilc/lowcode-shared';
import { createDecorator, Emitter, type Event, type EventListener } from '@alilc/lowcode-shared';
import {
Configuration,
DefaultConfiguration,
@ -69,7 +63,6 @@ export interface IConfigurationService {
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
@Provide(IConfigurationService)
export class ConfigurationService implements IConfigurationService {
private configuration: Configuration;
private readonly defaultConfiguration: DefaultConfiguration;

View File

@ -6,11 +6,11 @@ import {
type IOverrides,
} from './configurationModel';
import {
ConfigurationRegistry,
type IConfigurationPropertySchema,
type IConfigurationRegistry,
type IRegisteredConfigurationPropertySchema,
} from './configurationRegistry';
import { Registry, Extensions } from '../common/registry';
import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es';
import {
IInspectValue,
@ -37,8 +37,8 @@ export class DefaultConfiguration {
initialize(): ConfigurationModel {
this.resetConfigurationModel();
Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(
({ properties }) => this.onDidUpdateConfiguration([...properties]),
ConfigurationRegistry.onDidUpdateConfiguration(({ properties }) =>
this.onDidUpdateConfiguration([...properties]),
);
return this.configurationModel;
@ -56,19 +56,14 @@ export class DefaultConfiguration {
}
private onDidUpdateConfiguration(properties: string[]): void {
this.updateConfigurationModel(
properties,
Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties(),
);
this.updateConfigurationModel(properties, ConfigurationRegistry.getConfigurationProperties());
this.emitter.emit({ defaults: this.configurationModel, properties });
}
private resetConfigurationModel(): void {
this._configurationModel = ConfigurationModel.createEmptyModel();
const properties = Registry.as<IConfigurationRegistry>(
Extensions.Configuration,
).getConfigurationProperties();
const properties = ConfigurationRegistry.getConfigurationProperties();
this.updateConfigurationModel(Object.keys(properties), properties);
}
@ -156,9 +151,7 @@ class ConfigurationModelParser {
raw: any,
options?: ConfigurationParseOptions,
): IConfigurationModel & { hasExcludedProperties?: boolean } {
const configurationProperties = Registry.as<IConfigurationRegistry>(
Extensions.Configuration,
).getConfigurationProperties();
const configurationProperties = ConfigurationRegistry.getConfigurationProperties();
const filtered = this.filter(raw, configurationProperties, true, options);
raw = filtered.raw;

View File

@ -1,5 +1,4 @@
import { type IConfigurationRegistry, type IConfigurationNode } from '../configuration';
import { Registry, Extensions } from '../common/registry';
import { ConfigurationRegistry, type IConfigurationNode } from '../configuration';
import { type ExtensionInitializer, type IExtensionInstance } from './extension';
import { invariant } from '@alilc/lowcode-shared';
@ -19,9 +18,8 @@ export class ExtensionHost {
initializer: ExtensionInitializer,
preferenceConfigurations: IConfigurationNode[],
) {
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
this.configurationProperties =
configurationRegistry.registerConfigurations(preferenceConfigurations);
ConfigurationRegistry.registerConfigurations(preferenceConfigurations);
this.instance = initializer({});
}

View File

@ -37,7 +37,7 @@ export class ExtensionManagement {
): Promise<void> {
if (!this.validateExtension(extension, override)) return;
const metadata = extension.meta ?? {};
const metadata = extension.metadata ?? {};
const host = new ExtensionHost(
extension.name,
extension,

View File

@ -1,4 +1,4 @@
import { createDecorator, Provide } from '@alilc/lowcode-shared';
import { createDecorator } from '@alilc/lowcode-shared';
import { ExtensionManagement, type IExtensionRegisterOptions } from './extensionManagement';
import { type IFunctionExtension } from './extension';
import { ExtensionHost } from './extensionHost';
@ -15,7 +15,6 @@ export interface IExtensionService {
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
@Provide(IExtensionService)
export class ExtensionService implements IExtensionService {
private extensionManagement = new ExtensionManagement();

View File

@ -41,5 +41,6 @@ export const Registry: IRegistry = new RegistryImpl();
export const Extensions = {
Configuration: 'base.contributions.configuration',
Command: 'base.contributions.command',
Keybinding: 'base.contributions.keybinding',
Widget: 'base.contributions.widget',
};

View File

@ -4,5 +4,9 @@ export * from './resource';
export * from './command';
// test
export * from './common/registry';
export * from './extension/registry';
export * from './main';
export * from './keybinding/keybindingRegistry';
export * from './keybinding/keybindingParser';
export * from './keybinding/keybindingResolver';
export * from './keybinding/keybindings';

View File

@ -1,39 +0,0 @@
/**
* A keybinding is a sequence of chords.
*/
export class Keybinding {
public readonly chords: Chord[];
constructor(chords: Chord[]) {
if (chords.length === 0) {
throw illegalArgument(`chords`);
}
this.chords = chords;
}
public getHashCode(): string {
let result = '';
for (let i = 0, len = this.chords.length; i < len; i++) {
if (i !== 0) {
result += ';';
}
result += this.chords[i].getHashCode();
}
return result;
}
public equals(other: Keybinding | null): boolean {
if (other === null) {
return false;
}
if (this.chords.length !== other.chords.length) {
return false;
}
for (let i = 0; i < this.chords.length; i++) {
if (!this.chords[i].equals(other.chords[i])) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,99 @@
import { KeyCodeUtils, ScanCodeUtils } from '@alilc/lowcode-shared';
import { KeyCodeChord, ScanCodeChord, Keybinding, Chord } from './keybindings';
export class KeybindingParser {
private static _readModifiers(input: string) {
input = input.toLowerCase().trim();
let ctrl = false;
let shift = false;
let alt = false;
let meta = false;
let matchedModifier: boolean;
do {
matchedModifier = false;
if (/^ctrl(\+|-)/.test(input)) {
ctrl = true;
input = input.slice('ctrl-'.length);
matchedModifier = true;
}
if (/^shift(\+|-)/.test(input)) {
shift = true;
input = input.slice('shift-'.length);
matchedModifier = true;
}
if (/^alt(\+|-)/.test(input)) {
alt = true;
input = input.slice('alt-'.length);
matchedModifier = true;
}
if (/^meta(\+|-)/.test(input)) {
meta = true;
input = input.slice('meta-'.length);
matchedModifier = true;
}
if (/^win(\+|-)/.test(input)) {
meta = true;
input = input.slice('win-'.length);
matchedModifier = true;
}
if (/^cmd(\+|-)/.test(input)) {
meta = true;
input = input.slice('cmd-'.length);
matchedModifier = true;
}
} while (matchedModifier);
let key: string;
const firstSpaceIdx = input.indexOf(' ');
if (firstSpaceIdx > 0) {
key = input.substring(0, firstSpaceIdx);
input = input.substring(firstSpaceIdx);
} else {
key = input;
input = '';
}
return {
remains: input,
ctrl,
shift,
alt,
meta,
key,
};
}
private static parseChord(input: string): [Chord, string] {
const mods = this._readModifiers(input);
const scanCodeMatch = mods.key.match(/^\[([^\]]+)\]$/);
if (scanCodeMatch) {
const strScanCode = scanCodeMatch[1];
const scanCode = ScanCodeUtils.lowerCaseToEnum(strScanCode);
return [
new ScanCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, scanCode),
mods.remains,
];
}
const keyCode = KeyCodeUtils.fromUserSettings(mods.key);
return [new KeyCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains];
}
static parseKeybinding(input: string): Keybinding | null {
if (!input) {
return null;
}
const chords: Chord[] = [];
let chord: Chord;
while (input.length > 0) {
[chord, input] = this.parseChord(input);
chords.push(chord);
}
return chords.length > 0 ? new Keybinding(chords) : null;
}
}

View File

@ -1,3 +1,8 @@
import { OperatingSystem, OS } from '@alilc/lowcode-shared';
import { ICommandHandler, ICommandMetadata, CommandsRegistry } from '../command';
import { Keybinding } from './keybindings';
import { Extensions, Registry } from '../extension/registry';
export interface IKeybindingItem {
keybinding: Keybinding | null;
command: string | null;
@ -24,3 +29,81 @@ export interface IKeybindings {
secondary?: number[];
};
}
export interface IKeybindingRule extends IKeybindings {
id: string;
weight: number;
args?: any;
}
export interface ICommandAndKeybindingRule extends IKeybindingRule {
handler: ICommandHandler;
metadata?: ICommandMetadata | null;
}
export interface IExtensionKeybindingRule {
keybinding: Keybinding | null;
id: string;
args?: any;
weight: number;
extensionId?: string;
isBuiltinExtension?: boolean;
}
export const enum KeybindingWeight {
EditorCore = 0,
EditorContrib = 100,
WorkbenchContrib = 200,
BuiltinExtension = 300,
ExternalExtension = 400,
}
export interface IKeybindingsRegistry {
registerKeybindingRule(rule: IKeybindingRule): void;
setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void;
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void;
getDefaultKeybindings(): IKeybindingItem[];
}
export class KeybindingsRegistryImpl implements IKeybindingsRegistry {
/**
* Take current platform into account and reduce to primary & secondary.
*/
private static bindToCurrentPlatform(kb: IKeybindings): {
primary?: number;
secondary?: number[];
} {
if (OS === OperatingSystem.Windows) {
if (kb && kb.win) {
return kb.win;
}
} else if (OS === OperatingSystem.Macintosh) {
if (kb && kb.mac) {
return kb.mac;
}
} else {
if (kb && kb.linux) {
return kb.linux;
}
}
return kb;
}
registerKeybindingRule(rule: IKeybindingRule): void {
const actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform(rule);
}
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void {
this.registerKeybindingRule(desc);
CommandsRegistry.registerCommand(desc);
}
setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void {}
getDefaultKeybindings(): IKeybindingItem[] {}
}
export const KeybindingsRegistry = new KeybindingsRegistryImpl();
Registry.add(Extensions.Keybinding, KeybindingsRegistry);

View File

@ -0,0 +1,79 @@
export const enum ResultKind {
/** No keybinding found this sequence of chords */
NoMatchingKb,
/** There're several keybindings that have the given sequence of chords as a prefix */
MoreChordsNeeded,
/** A single keybinding found to be dispatched/invoked */
KbFound,
}
export type ResolutionResult =
| { kind: ResultKind.NoMatchingKb }
| { kind: ResultKind.MoreChordsNeeded }
| { kind: ResultKind.KbFound; commandId: string | null; commandArgs: any; isBubble: boolean };
// util definitions to make working with the above types easier within this module:
export const NoMatchingKb: ResolutionResult = { kind: ResultKind.NoMatchingKb };
const MoreChordsNeeded: ResolutionResult = { kind: ResultKind.MoreChordsNeeded };
function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean): ResolutionResult {
return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble };
}
//#endregion
export class ResolvedKeybindingItem {
_resolvedKeybindingItemBrand: void = undefined;
public readonly resolvedKeybinding: ResolvedKeybinding | undefined;
public readonly chords: string[];
public readonly bubble: boolean;
public readonly command: string | null;
public readonly commandArgs: any;
public readonly when: ContextKeyExpression | undefined;
public readonly isDefault: boolean;
public readonly extensionId: string | null;
public readonly isBuiltinExtension: boolean;
constructor(
resolvedKeybinding: ResolvedKeybinding | undefined,
command: string | null,
commandArgs: any,
when: ContextKeyExpression | undefined,
isDefault: boolean,
extensionId: string | null,
isBuiltinExtension: boolean,
) {
this.resolvedKeybinding = resolvedKeybinding;
this.chords = resolvedKeybinding
? toEmptyArrayIfContainsNull(resolvedKeybinding.getDispatchChords())
: [];
if (resolvedKeybinding && this.chords.length === 0) {
// handle possible single modifier chord keybindings
this.chords = toEmptyArrayIfContainsNull(
resolvedKeybinding.getSingleModifierDispatchChords(),
);
}
this.bubble = command ? command.charCodeAt(0) === CharCode.Caret : false;
this.command = this.bubble ? command!.substr(1) : command;
this.commandArgs = commandArgs;
this.when = when;
this.isDefault = isDefault;
this.extensionId = extensionId;
this.isBuiltinExtension = isBuiltinExtension;
}
}
export function toEmptyArrayIfContainsNull<T>(arr: (T | null)[]): T[] {
const result: T[] = [];
for (let i = 0, len = arr.length; i < len; i++) {
const element = arr[i];
if (!element) {
return [];
}
result.push(element);
}
return result;
}

View File

@ -0,0 +1,100 @@
import { createDecorator, type IJSONSchema, KeyCode, type Event } from '@alilc/lowcode-shared';
import { Keybinding, ResolvedKeybinding } from './keybindings';
import { ResolutionResult, ResolvedKeybindingItem } from './keybindingResolver';
export interface IUserFriendlyKeybinding {
key: string;
command: string;
args?: any;
when?: string;
}
export interface IKeyboardEvent {
readonly _standardKeyboardEventBrand: true;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly altGraphKey: boolean;
readonly keyCode: KeyCode;
readonly code: string;
}
export interface KeybindingsSchemaContribution {
readonly onDidChange?: Event<void>;
getSchemaAdditions(): IJSONSchema[];
}
export interface IKeybindingService {
readonly _serviceBrand: undefined;
readonly inChordMode: boolean;
onDidUpdateKeybindings: Event<void>;
/**
* Returns none, one or many (depending on keyboard layout)!
*/
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
resolveUserBinding(userBinding: string): ResolvedKeybinding[];
/**
* Resolve and dispatch `keyboardEvent` and invoke the command.
*/
dispatchEvent(e: IKeyboardEvent, target: any): boolean;
/**
* Resolve and dispatch `keyboardEvent`, but do not invoke the command or change inner state.
*/
softDispatch(keyboardEvent: IKeyboardEvent, target: any): ResolutionResult;
/**
* Enable hold mode for this command. This is only possible if the command is current being dispatched, meaning
* we are after its keydown and before is keyup event.
*
* @returns A promise that resolves when hold stops, returns undefined if hold mode could not be enabled.
*/
enableKeybindingHoldMode(commandId: string): Promise<void> | undefined;
dispatchByUserSettingsLabel(userSettingsLabel: string, target: any): void;
/**
* Look up keybindings for a command.
* Use `lookupKeybinding` if you are interested in the preferred keybinding.
*/
lookupKeybindings(commandId: string): ResolvedKeybinding[];
/**
* Look up the preferred (last defined) keybinding for a command.
* @returns The preferred keybinding or null if the command is not bound.
*/
lookupKeybinding(commandId: string, context?: any): ResolvedKeybinding | undefined;
getDefaultKeybindingsContent(): string;
getDefaultKeybindings(): readonly ResolvedKeybindingItem[];
getKeybindings(): readonly ResolvedKeybindingItem[];
customKeybindingsCount(): number;
/**
* Will the given key event produce a character that's rendered on screen, e.g. in a
* text box. *Note* that the results of this function can be incorrect.
*/
mightProducePrintableCharacter(event: IKeyboardEvent): boolean;
registerSchemaContribution(contribution: KeybindingsSchemaContribution): void;
toggleLogging(): boolean;
_dumpDebugInfo(): string;
_dumpDebugInfoJSON(): string;
}
export const IKeybindingService = createDecorator<IKeybindingService>('keybindingService');

View File

@ -0,0 +1,280 @@
import { illegalArgument, KeyCode, OperatingSystem, ScanCode } from '@alilc/lowcode-shared';
/**
* Binary encoding strategy:
* ```
* 1111 11
* 5432 1098 7654 3210
* ---- CSAW KKKK KKKK
* C = bit 11 = ctrlCmd flag
* S = bit 10 = shift flag
* A = bit 9 = alt flag
* W = bit 8 = winCtrl flag
* K = bits 0-7 = key code
* ```
*/
const enum BinaryKeybindingsMask {
CtrlCmd = (1 << 11) >>> 0,
Shift = (1 << 10) >>> 0,
Alt = (1 << 9) >>> 0,
WinCtrl = (1 << 8) >>> 0,
KeyCode = 0x000000ff,
}
export function decodeKeybinding(
keybinding: number | number[],
OS: OperatingSystem,
): Keybinding | null {
if (typeof keybinding === 'number') {
if (keybinding === 0) {
return null;
}
const firstChord = (keybinding & 0x0000ffff) >>> 0;
const secondChord = (keybinding & 0xffff0000) >>> 16;
if (secondChord !== 0) {
return new Keybinding([
createSimpleKeybinding(firstChord, OS),
createSimpleKeybinding(secondChord, OS),
]);
}
return new Keybinding([createSimpleKeybinding(firstChord, OS)]);
} else {
const chords = [];
for (let i = 0; i < keybinding.length; i++) {
chords.push(createSimpleKeybinding(keybinding[i], OS));
}
return new Keybinding(chords);
}
}
export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): KeyCodeChord {
const ctrlCmd = keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false;
const winCtrl = keybinding & BinaryKeybindingsMask.WinCtrl ? true : false;
const ctrlKey = OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd;
const shiftKey = keybinding & BinaryKeybindingsMask.Shift ? true : false;
const altKey = keybinding & BinaryKeybindingsMask.Alt ? true : false;
const metaKey = OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl;
const keyCode = keybinding & BinaryKeybindingsMask.KeyCode;
return new KeyCodeChord(ctrlKey, shiftKey, altKey, metaKey, keyCode);
}
export interface Modifiers {
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
}
/**
* Represents a chord which uses the `keyCode` field of keyboard events.
* A chord is a combination of keys pressed simultaneously.
*/
export class KeyCodeChord implements Modifiers {
constructor(
public readonly ctrlKey: boolean,
public readonly shiftKey: boolean,
public readonly altKey: boolean,
public readonly metaKey: boolean,
public readonly keyCode: KeyCode,
) {}
equals(other: Chord): boolean {
return (
other instanceof KeyCodeChord &&
this.ctrlKey === other.ctrlKey &&
this.shiftKey === other.shiftKey &&
this.altKey === other.altKey &&
this.metaKey === other.metaKey &&
this.keyCode === other.keyCode
);
}
getHashCode(): string {
const ctrl = this.ctrlKey ? '1' : '0';
const shift = this.shiftKey ? '1' : '0';
const alt = this.altKey ? '1' : '0';
const meta = this.metaKey ? '1' : '0';
return `K${ctrl}${shift}${alt}${meta}${this.keyCode}`;
}
isModifierKey(): boolean {
return (
this.keyCode === KeyCode.Unknown ||
this.keyCode === KeyCode.Ctrl ||
this.keyCode === KeyCode.Meta ||
this.keyCode === KeyCode.Alt ||
this.keyCode === KeyCode.Shift
);
}
toKeybinding(): Keybinding {
return new Keybinding([this]);
}
/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
isDuplicateModifierCase(): boolean {
return (
(this.ctrlKey && this.keyCode === KeyCode.Ctrl) ||
(this.shiftKey && this.keyCode === KeyCode.Shift) ||
(this.altKey && this.keyCode === KeyCode.Alt) ||
(this.metaKey && this.keyCode === KeyCode.Meta)
);
}
}
/**
* Represents a chord which uses the `code` field of keyboard events.
* A chord is a combination of keys pressed simultaneously.
*/
export class ScanCodeChord implements Modifiers {
constructor(
public readonly ctrlKey: boolean,
public readonly shiftKey: boolean,
public readonly altKey: boolean,
public readonly metaKey: boolean,
public readonly scanCode: ScanCode,
) {}
equals(other: Chord): boolean {
return (
other instanceof ScanCodeChord &&
this.ctrlKey === other.ctrlKey &&
this.shiftKey === other.shiftKey &&
this.altKey === other.altKey &&
this.metaKey === other.metaKey &&
this.scanCode === other.scanCode
);
}
getHashCode(): string {
const ctrl = this.ctrlKey ? '1' : '0';
const shift = this.shiftKey ? '1' : '0';
const alt = this.altKey ? '1' : '0';
const meta = this.metaKey ? '1' : '0';
return `S${ctrl}${shift}${alt}${meta}${this.scanCode}`;
}
/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
isDuplicateModifierCase(): boolean {
return (
(this.ctrlKey &&
(this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight)) ||
(this.shiftKey &&
(this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight)) ||
(this.altKey &&
(this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight)) ||
(this.metaKey &&
(this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight))
);
}
}
export type Chord = KeyCodeChord | ScanCodeChord;
/**
* A keybinding is a sequence of chords.
*/
export class Keybinding {
readonly chords: Chord[];
constructor(chords: Chord[]) {
if (chords.length === 0) {
throw illegalArgument(`chords`);
}
this.chords = chords;
}
getHashCode(): string {
let result = '';
for (let i = 0, len = this.chords.length; i < len; i++) {
if (i !== 0) {
result += ';';
}
result += this.chords[i].getHashCode();
}
return result;
}
equals(other: Keybinding | null): boolean {
if (other === null) {
return false;
}
if (this.chords.length !== other.chords.length) {
return false;
}
for (let i = 0; i < this.chords.length; i++) {
if (!this.chords[i].equals(other.chords[i])) {
return false;
}
}
return true;
}
}
export class ResolvedChord {
constructor(
public readonly ctrlKey: boolean,
public readonly shiftKey: boolean,
public readonly altKey: boolean,
public readonly metaKey: boolean,
public readonly keyLabel: string | null,
public readonly keyAriaLabel: string | null,
) {}
}
export type SingleModifierChord = 'ctrl' | 'shift' | 'alt' | 'meta';
/**
* A resolved keybinding. Consists of one or multiple chords.
*/
export abstract class ResolvedKeybinding {
/**
* This prints the binding in a format suitable for displaying in the UI.
*/
public abstract getLabel(): string | null;
/**
* This prints the binding in a format suitable for ARIA.
*/
public abstract getAriaLabel(): string | null;
/**
* This prints the binding in a format suitable for electron's accelerators.
* See https://github.com/electron/electron/blob/master/docs/api/accelerator.md
*/
public abstract getElectronAccelerator(): string | null;
/**
* This prints the binding in a format suitable for user settings.
*/
public abstract getUserSettingsLabel(): string | null;
/**
* Is the user settings label reflecting the label?
*/
public abstract isWYSIWYG(): boolean;
/**
* Does the keybinding consist of more than one chord?
*/
public abstract hasMultipleChords(): boolean;
/**
* Returns the chords that comprise of the keybinding.
*/
public abstract getChords(): ResolvedChord[];
/**
* Returns the chords as strings useful for dispatching.
* Returns null for modifier only chords.
* @example keybinding "Shift" -> null
* @example keybinding ("D" with shift == true) -> "shift+D"
*/
public abstract getDispatchChords(): (string | null)[];
/**
* Returns the modifier only chords as strings useful for dispatching.
* Returns null for chords that contain more than one modifier or a regular key.
* @example keybinding "Shift" -> "shift"
* @example keybinding ("D" with shift == true") -> null
*/
public abstract getSingleModifierDispatchChords(): (SingleModifierChord | null)[];
}

View File

@ -1,6 +1,5 @@
import {
createDecorator,
Provide,
type Package,
type Reference,
mapPackageToUniqueId,
@ -22,7 +21,6 @@ export interface IResourceService {
export const IResourceService = createDecorator<IResourceService>('resourceService');
@Provide(IResourceService)
export class ResourceService implements IResourceService {
private resourceModel = new ResourceModel();

View File

@ -1,5 +1,4 @@
import { Extensions, Registry } from '../../extension/extension';
import { IWidgetRegistry } from '../widget/widgetRegistry';
import { WidgetRegistry } from '../widget/widgetRegistry';
export const enum LayoutParts {
TopBar = 1,
@ -21,7 +20,7 @@ export interface ILayout {
export class Layout<View> implements ILayout {
constructor(public mainContainer: HTMLElement) {
Registry.as<IWidgetRegistry<View>>(Extensions.Widget).onDidRegister(() => {});
WidgetRegistry.onDidRegister(() => {});
}
registerPart(part: LayoutParts): void {}

View File

@ -1,6 +1,6 @@
import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared';
import { IWidget } from './widget';
import { Extensions, Registry } from '../../extension/extension';
import { Extensions, Registry } from '../../extension/registry';
export interface IWidgetRegistry<View> {
onDidRegister: Event<IWidget<View>[]>;
@ -12,7 +12,7 @@ export interface IWidgetRegistry<View> {
getWidgets(): IWidget<View>[];
}
export class WidgetRegistry<View> implements IWidgetRegistry<View> {
export class WidgetRegistryImpl<View> implements IWidgetRegistry<View> {
private _widgets: Map<string, IWidget<View>> = new Map();
private emitter = new Emitter<IWidget<View>[]>();
@ -34,4 +34,6 @@ export class WidgetRegistry<View> implements IWidgetRegistry<View> {
}
}
Registry.add(Extensions.Widget, new WidgetRegistry<any>());
export const WidgetRegistry = new WidgetRegistryImpl<any>();
Registry.add(Extensions.Widget, WidgetRegistry);

View File

@ -1,4 +1,4 @@
import { createDecorator, Provide } from '@alilc/lowcode-shared';
import { createDecorator } from '@alilc/lowcode-shared';
export interface IWorkbenchService {
initialize(): void;
@ -6,7 +6,6 @@ export interface IWorkbenchService {
export const IWorkbenchService = createDecorator<IWorkbenchService>('workbenchService');
@Provide(IWorkbenchService)
export class WorkbenchService implements IWorkbenchService {
initialize(): void {
console.log('workbench service');

View File

@ -1,8 +1,7 @@
import { createDecorator, Provide } from '@alilc/lowcode-shared';
import { createDecorator } from '@alilc/lowcode-shared';
export interface IWorkspaceService {}
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
@Provide(IWorkspaceService)
export class WorkspaceService implements IWorkspaceService {}

View File

@ -1,4 +1,4 @@
import { type Event, type EventListener, createDecorator, Provide } from '@alilc/lowcode-shared';
import { type Event, type EventListener, createDecorator } from '@alilc/lowcode-shared';
export interface ITheme {
type: string;
@ -14,7 +14,6 @@ export interface IThemeService {
export const IThemeService = createDecorator<IThemeService>('themeService');
@Provide(IThemeService)
export class ThemeService implements IThemeService {
private activeTheme: ITheme;

View File

@ -0,0 +1,4 @@
// Global compile-time constants
declare var __DEV__: boolean;
declare var __VERSION__: string;
declare var __COMPAT__: boolean;

View File

@ -1,9 +1,10 @@
import {
IBoostsService,
IBoostsManager,
IComponentTreeModelService,
ILifeCycleService,
IPackageManagementService,
ISchemaService,
IExtensionHostService,
} from '@alilc/lowcode-renderer-core';
import { InstanceAccessor } from '@alilc/lowcode-shared';
import { createContext, useContext } from 'react';
@ -16,7 +17,7 @@ export interface IRendererContext {
readonly packageManager: IPackageManagementService;
readonly boostsManager: IBoostsService;
readonly boostsManager: IBoostsManager;
readonly componentTreeModel: IComponentTreeModelService;
@ -27,7 +28,7 @@ export const getRenderInstancesByAccessor = (accessor: InstanceAccessor) => {
return {
schema: accessor.get(ISchemaService),
packageManager: accessor.get(IPackageManagementService),
boostsManager: accessor.get(IBoostsService),
boostsManager: accessor.get(IExtensionHostService).boostsManager,
componentTreeModel: accessor.get(IComponentTreeModelService),
lifeCycle: accessor.get(ILifeCycleService),
};

View File

@ -3,8 +3,7 @@ import {
type AnyFunction,
type StringDictionary,
specTypes,
computed,
watch,
Signals,
invariant,
} from '@alilc/lowcode-shared';
import { useRef } from 'react';
@ -60,10 +59,10 @@ function createReactiveStore<Snapshot = StringDictionary>(
};
if (getter) {
const computedValue = computed<any>(() => getter(target));
const computedValue = Signals.computed<any>(() => getter(target));
cleanups.push(
watch(
Signals.watch(
computedValue,
(newValue) => {
Promise.resolve().then(() => {
@ -76,8 +75,8 @@ function createReactiveStore<Snapshot = StringDictionary>(
);
} else if (valueGetter) {
const initValue = mapValue(target, filter, (node: any, paths) => {
const computedValue = computed(() => valueGetter(node));
const unwatch = watch(computedValue, (newValue) => {
const computedValue = Signals.computed(() => valueGetter(node));
const unwatch = Signals.watch(computedValue, (newValue) => {
waitPathToSetValueMap.set(paths, newValue);
if (!isFlushPending && !isFlushing) {

View File

@ -1,8 +1,8 @@
import { signal, type StringDictionary, type InstanceStateApi } from '@alilc/lowcode-shared';
import { Signals, type StringDictionary, type InstanceStateApi } from '@alilc/lowcode-shared';
import { isPlainObject } from 'lodash-es';
export function reactiveStateFactory(initState: StringDictionary): InstanceStateApi {
const proxyState = signal(initState);
const proxyState = Signals.signal(initState);
return {
get state() {

View File

@ -2,6 +2,5 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "../../playground/renderer/src/plugin/remote/element.ts"]
}
}

View File

@ -1,5 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('createComponentFunction', () => {
it('', () => {});
});

View File

@ -1,57 +0,0 @@
/**
* @vitest-environment jsdom
*/
import { describe, it, expect } from 'vitest';
import { CodeScope } from '../../../src/services/code-runtime';
describe('CodeScope', () => {
it('should return initial values', () => {
const initValue = { a: 1, b: 2 };
const scope = new CodeScope(initValue);
expect(scope.value.a).toBe(1);
expect(scope.value.b).toBe(2);
});
it('inject should add new values', () => {
const scope = new CodeScope({});
scope.set('c', 3);
expect(scope.value.c).toBe(3);
});
it('inject should not overwrite existing values without force', () => {
const initValue = { a: 1 };
const scope = new CodeScope(initValue);
expect(scope.value.a).not.toBe(2);
scope.set('a', 3);
expect(scope.value.a).toBe(3);
});
it('setValue should merge values by default', () => {
const initValue = { a: 1 };
const scope = new CodeScope(initValue);
scope.setValue({ b: 2 });
expect(scope.value.a).toBe(1);
expect(scope.value.b).toBe(2);
});
it('setValue should replace values when replace is true', () => {
const initValue = { a: 1 };
const scope = new CodeScope(initValue);
scope.setValue({ b: 2 }, true);
expect(scope.value.a).toBeUndefined();
expect(scope.value.b).toBe(2);
});
it('should create child scopes and respect scope hierarchy', () => {
const parentValue = { a: 1, b: 2 };
const childValue = { b: 3, c: 4 };
const parentScope = new CodeScope(parentValue);
const childScope = parentScope.createChild(childValue);
expect(childScope.value.a).toBe(1); // Inherits from parent scope
expect(childScope.value.b).toBe(3); // Overridden by child scope
expect(childScope.value.c).toBe(4); // Unique to child scope
expect(parentScope.value.c).toBeUndefined(); // Parent scope should not have child's properties
});
});

View File

@ -1,36 +0,0 @@
import { describe, it, expect } from 'vitest';
import { LifeCycleService, LifecyclePhase } from '../../src/services/lifeCycleService';
const sleep = () => new Promise((r) => setTimeout(r, 500));
describe('LifeCycleService', () => {
it('it works', async () => {
let result = '';
const lifeCycle = new LifeCycleService();
lifeCycle.when(LifecyclePhase.Ready).then(() => {
result += '1';
});
lifeCycle.when(LifecyclePhase.Ready).finally(() => {
result += '2';
});
lifeCycle.when(LifecyclePhase.Inited).then(() => {
result += '3';
});
lifeCycle.when(LifecyclePhase.Inited).finally(() => {
result += '4';
});
lifeCycle.phase = LifecyclePhase.Ready;
await sleep();
expect(result).toEqual('12');
lifeCycle.phase = LifecyclePhase.Inited;
await sleep();
expect(result).toEqual('1234');
});
});

View File

@ -1,79 +0,0 @@
import { describe, it, expect } from 'vitest';
import { walk } from '../../src/utils/node';
describe('sync walker', () => {
it('down', () => {
const ast = {
hasMask: true,
visible: false,
footer: false,
cancelProps: {
text: false,
type: 'normal',
},
confirmState: '确定',
confirmStyle: 'primary',
footerActions: 'cancel,ok',
className: 'dialog_lkz6xvcv',
confirmText: {
en_US: 'Confirm',
use: '',
zh_CN: '确定',
type: 'JSExpression',
value: `({"en_US":"OK","key":"i18n-xgse6q6a","type":"i18n","zh_CN":"确定"})[this.utils.getLocale()]`,
key: 'i18n-xgse6q6a',
extType: 'i18n',
},
autoFocus: true,
title: {
mock: {
en_US: 'Dialog Title',
use: '',
zh_CN: 'Dialog标题',
type: 'JSExpression',
value: `({"en_US":"Dialog Title","key":"i18n-0m3kaceq","type":"i18n","zh_CN":"Dialog标题"})[this.utils.getLocale()]`,
key: 'i18n-0m3kaceq',
extType: 'i18n',
},
type: 'JSExpression',
value: 'state.dialogInfo && state.dialogInfo.title',
},
closeable: [
'esc',
'mask',
{
type: 'JSExpression',
value: '1',
},
],
cancelText: {
en_US: 'Cancel',
use: '',
zh_CN: '取消',
type: 'JSExpression',
value: `({"en_US":"Cancel","key":"i18n-wtq23279","type":"i18n","zh_CN":"取消"})[this.utils.getLocale()]`,
key: 'i18n-wtq23279',
extType: 'i18n',
},
width: '800px',
footerAlign: 'right',
popupOutDialog: true,
__style__: ':root {}',
fieldId: 'dialog_case',
height: '500px',
};
const newAst = walk(ast, {
enter(node, parent, key, index) {
if (node.type === 'JSExpression') {
this.replace({
type: '1',
value: '2',
});
}
},
});
console.log(newAst, Object.is(newAst, ast));
});
});

View File

@ -1,98 +0,0 @@
import { describe, it, expect } from 'vitest';
import { someValue, mapValue } from '../../src/utils/value';
describe('someValue', () => {
it('should return false for non-plain objects or empty objects', () => {
expect(someValue([], (val) => val > 10)).toBe(false);
expect(someValue({}, (val) => val > 10)).toBe(false);
expect(someValue(null, (val) => val > 10)).toBe(false);
expect(someValue(undefined, (val) => val > 10)).toBe(false);
});
it('should return true if the predicate matches object value', () => {
const obj = { a: 5, b: { c: 15 } };
expect(someValue(obj, (val) => val.c > 10)).toBe(true);
});
it('should return true if the predicate matches nested array element', () => {
const obj = { a: [1, 2, { d: 14 }] };
expect(someValue(obj, (val) => val.d > 10)).toBe(true);
});
it('should return false if the predicate does not match any value', () => {
const obj = { a: 5, b: { c: 9 } };
expect(someValue(obj, (val) => val.c > 10)).toBe(false);
});
it('should handle primitives in object values', () => {
const obj = { a: 1, b: 'string', c: true };
const strPredicate = (val: any) => typeof val.b === 'string';
expect(someValue(obj, strPredicate)).toBe(true);
const boolPredicate = (val: any) => typeof val.c === 'boolean';
expect(someValue(obj, boolPredicate)).toBe(true);
});
it('should handle deep nesting with mixed arrays and objects', () => {
const complexObj = { a: { b: [1, 2, { c: 3 }, [{ d: 4 }]] } };
expect(someValue(complexObj, (val) => val.d === 4)).toBe(true);
});
it('should handle functions and undefined values', () => {
const objWithFunc = { a: () => {}, b: undefined };
const funcPredicate = (val: any) => typeof val.a === 'function';
expect(someValue(objWithFunc, funcPredicate)).toBe(true);
const undefinedPredicate = (val: any) => val.b === undefined;
expect(someValue(objWithFunc, undefinedPredicate)).toBe(true);
});
});
describe('mapValue', () => {
const predicate = (obj: any) => obj && obj.process;
const processor = (obj: any, paths: any[]) => ({ ...obj, processed: true, paths });
it('should not process object if it does not match the predicate', () => {
const obj = { a: 3, b: { c: 4 } };
expect(mapValue(obj, predicate, processor)).toEqual(obj);
});
it('should process object that matches the predicate', () => {
const obj = { a: { process: true } };
expect(mapValue(obj, predicate, processor)).toEqual({
a: { process: true, processed: true, paths: ['a'] },
});
});
it('should handle nested objects and arrays with various types of predicates', () => {
const complexObj = {
a: { key: 'value' },
b: [{ key: 'value' }, undefined, null, 0, false],
c: () => {},
};
const truthyPredicate = (obj: any) => 'key' in obj && obj.key === 'value';
const falsePredicate = (obj: any) => false;
expect(mapValue(complexObj, truthyPredicate, processor)).toEqual({
a: { key: 'value', processed: true, paths: ['a'] },
b: [{ key: 'value', processed: true, paths: ['b', 0] }, undefined, null, 0, false],
c: complexObj.c,
});
expect(mapValue(complexObj, falsePredicate, processor)).toEqual(complexObj);
});
it('should process nested object and arrays that match the predicate', () => {
const nestedObj = {
a: { key: 'value', nested: { key: 'value' } },
};
const predicate = (obj: any) => 'key' in obj;
const result = mapValue(nestedObj, predicate, processor);
expect(result).toEqual({
a: { key: 'value', processed: true, paths: ['a'], nested: { key: 'value' } },
});
expect(result.a.nested).not.toHaveProperty('processed');
});
});

View File

@ -1,6 +1,6 @@
/* --------------- api -------------------- */
export { createRenderer } from './main';
export { IBoostsService, IExtensionHostService } from './services/extension';
export { IExtensionHostService } from './services/extension';
export { definePackageLoader, IPackageManagementService } from './services/package';
export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService';
export { IComponentTreeModelService } from './services/model';

View File

@ -1,54 +1,78 @@
import { invariant, InstantiationService } from '@alilc/lowcode-shared';
import { ICodeRuntimeService } from './services/code-runtime';
import type { AppOptions, RendererApplication } from './types';
import { CodeRuntimeService, ICodeRuntimeService } from './services/code-runtime';
import {
IExtensionHostService,
type RenderAdapter,
type IRenderObject,
ExtensionHostService,
} from './services/extension';
import { IPackageManagementService } from './services/package';
import { ISchemaService } from './services/schema';
import { ILifeCycleService, LifecyclePhase } from './services/lifeCycleService';
import type { AppOptions, RendererApplication } from './types';
import { IPackageManagementService, PackageManagementService } from './services/package';
import { ISchemaService, SchemaService } from './services/schema';
import { ILifeCycleService, LifecyclePhase, LifeCycleService } from './services/lifeCycleService';
import { IRuntimeIntlService, RuntimeIntlService } from './services/runtimeIntlService';
import { IRuntimeUtilService, RuntimeUtilService } from './services/runtimeUtilService';
/**
* createRenderer
* @param schema
* @param options
* @returns
*/
export function createRenderer<RenderObject = IRenderObject>(
renderAdapter: RenderAdapter<RenderObject>,
): (options: AppOptions) => Promise<RendererApplication<RenderObject>> {
invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.');
const accessor = new InstantiationService({ defaultScope: 'Singleton' });
let mode: 'development' | 'production' = 'production';
const instantiationService = new InstantiationService();
const schemaService = accessor.get(ISchemaService);
const packageManagementService = accessor.get(IPackageManagementService);
const codeRuntimeService = accessor.get(ICodeRuntimeService);
const lifeCycleService = accessor.get(ILifeCycleService);
const extensionHostService = accessor.get(IExtensionHostService);
// create services
const lifeCycleService = new LifeCycleService();
instantiationService.container.set(ILifeCycleService, lifeCycleService);
return async (options) => {
if (options.mode) mode = options.mode;
const schemaService = new SchemaService(options.schema);
instantiationService.container.set(ISchemaService, schemaService);
// valid schema
schemaService.initialize(options.schema);
codeRuntimeService.initialize(options.codeRuntime ?? {});
await lifeCycleService.setPhase(LifecyclePhase.OptionsResolved);
const codeRuntimeService = instantiationService.createInstance(
CodeRuntimeService,
options.codeRuntime,
);
instantiationService.container.set(ICodeRuntimeService, codeRuntimeService);
const renderObject = await renderAdapter(accessor);
const packageManagementService = instantiationService.createInstance(PackageManagementService);
instantiationService.container.set(IPackageManagementService, packageManagementService);
const utils = schemaService.get('utils');
const runtimeUtilService = instantiationService.createInstance(RuntimeUtilService, utils);
instantiationService.container.set(IRuntimeUtilService, runtimeUtilService);
const defaultLocale = schemaService.get('config.defaultLocale');
const i18ns = schemaService.get('i18n', {});
const runtimeIntlService = instantiationService.createInstance(
RuntimeIntlService,
defaultLocale,
i18ns,
);
instantiationService.container.set(IRuntimeIntlService, runtimeIntlService);
const extensionHostService = new ExtensionHostService(
lifeCycleService,
packageManagementService,
schemaService,
codeRuntimeService,
runtimeIntlService,
runtimeUtilService,
);
instantiationService.container.set(IExtensionHostService, extensionHostService);
lifeCycleService.setPhase(LifecyclePhase.OptionsResolved);
const renderObject = await renderAdapter(instantiationService);
await extensionHostService.registerPlugin(options.plugins ?? []);
// 先加载插件提供 package loader
await packageManagementService.loadPackages(options.packages ?? []);
await lifeCycleService.setPhase(LifecyclePhase.Ready);
lifeCycleService.setPhase(LifecyclePhase.Ready);
const app: RendererApplication<RenderObject> = {
get mode() {
return mode;
return __DEV__ ? 'development' : 'production';
},
schema: schemaService,
packageManager: packageManagementService,
@ -57,12 +81,12 @@ export function createRenderer<RenderObject = IRenderObject>(
use: (plugin) => {
return extensionHostService.registerPlugin(plugin);
},
destroy: async () => {
return lifeCycleService.setPhase(LifecyclePhase.Destroying);
destroy: () => {
lifeCycleService.setPhase(LifecyclePhase.Destroying);
},
};
if (mode === 'development') {
if (__DEV__) {
Object.defineProperty(app, '__options', { get: () => options });
}

View File

@ -1,11 +1,15 @@
import { createDecorator, invariant, Provide, type StringDictionary } from '@alilc/lowcode-shared';
import {
createDecorator,
invariant,
Disposable,
type StringDictionary,
} from '@alilc/lowcode-shared';
import { type ICodeRuntime, type CodeRuntimeOptions, CodeRuntime } from './codeRuntime';
import { ISchemaService } from '../schema';
export interface ICodeRuntimeService {
readonly rootRuntime: ICodeRuntime;
initialize(options: CodeRuntimeOptions): void;
createCodeRuntime<T extends StringDictionary = StringDictionary>(
options?: CodeRuntimeOptions<T>,
): ICodeRuntime<T>;
@ -13,12 +17,23 @@ export interface ICodeRuntimeService {
export const ICodeRuntimeService = createDecorator<ICodeRuntimeService>('codeRuntimeService');
@Provide(ICodeRuntimeService)
export class CodeRuntimeService implements ICodeRuntimeService {
export class CodeRuntimeService extends Disposable implements ICodeRuntimeService {
rootRuntime: ICodeRuntime;
initialize(options?: CodeRuntimeOptions) {
constructor(
options: CodeRuntimeOptions = {},
@ISchemaService private schemaService: ISchemaService,
) {
super();
this.rootRuntime = new CodeRuntime(options);
this.addDispose(
this.schemaService.onSchemaUpdate(({ key, data }) => {
if (key === 'constants') {
this.rootRuntime.getScope().set('constants', data);
}
}),
);
}
createCodeRuntime<T extends StringDictionary = StringDictionary>(

View File

@ -1,4 +1,4 @@
import { createDecorator, Provide, type StringDictionary } from '@alilc/lowcode-shared';
import { type StringDictionary } from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { ICodeRuntime, ICodeRuntimeService } from '../code-runtime';
import { IRuntimeUtilService } from '../runtimeUtilService';
@ -24,17 +24,14 @@ export interface IBoostsApi {
/**
*
*/
export interface IBoostsService {
export interface IBoostsManager {
extend(name: string, value: any, force?: boolean): void;
extend(value: StringDictionary, force?: boolean): void;
toExpose<Extends>(): IBoosts<Extends>;
}
export const IBoostsService = createDecorator<IBoostsService>('boostsService');
@Provide(IBoostsService)
export class BoostsService implements IBoostsService {
export class BoostsManager implements IBoostsManager {
private builtInApis: IBoostsApi;
private extendsValue: StringDictionary = {};
@ -42,16 +39,16 @@ export class BoostsService implements IBoostsService {
private _expose: any;
constructor(
@ICodeRuntimeService codeRuntimeService: ICodeRuntimeService,
@IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService,
@IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService,
codeRuntimeService: ICodeRuntimeService,
runtimeIntlService: IRuntimeIntlService,
runtimeUtilService: IRuntimeUtilService,
) {
this.builtInApis = {
get codeRuntime() {
return codeRuntimeService.rootRuntime;
},
intl: this.runtimeIntlService,
util: this.runtimeUtilService,
intl: runtimeIntlService,
util: runtimeUtilService,
temporaryUse: (name, value) => {
this.extend(name, value);
},

View File

@ -1,102 +1,112 @@
import { createDecorator, Provide, EventEmitter } from '@alilc/lowcode-shared';
import { createDecorator, CyclicDependencyError, Disposable, Graph } from '@alilc/lowcode-shared';
import { type Plugin, type PluginContext } from './plugin';
import { IBoostsService } from './boosts';
import { BoostsManager } from './boosts';
import { IPackageManagementService } from '../package';
import { ISchemaService } from '../schema';
import { ILifeCycleService } from '../lifeCycleService';
import { KeyValueStore } from '../../utils/store';
interface IPluginRuntime extends Plugin {
status: 'setup' | 'ready';
}
import { ICodeRuntimeService } from '../code-runtime';
import { IRuntimeIntlService } from '../runtimeIntlService';
import { IRuntimeUtilService } from '../runtimeUtilService';
export interface IExtensionHostService {
readonly boostsManager: BoostsManager;
registerPlugin(plugin: Plugin | Plugin[]): Promise<void>;
getPlugin(name: string): Plugin | undefined;
dispose(): Promise<void>;
}
export const IExtensionHostService =
createDecorator<IExtensionHostService>('pluginManagementService');
@Provide(IExtensionHostService)
export class ExtensionHostService implements IExtensionHostService {
private pluginRuntimes: IPluginRuntime[] = [];
export class ExtensionHostService extends Disposable implements IExtensionHostService {
boostsManager: BoostsManager;
private eventEmitter: EventEmitter;
private pluginSetupContext: PluginContext;
private _activePlugins = new Set<string>();
private _pluginStore = new Map<string, Plugin>();
private _pluginDependencyGraph = new Graph<string>((name) => name);
private _pluginSetupContext: PluginContext;
constructor(
@IPackageManagementService private packageManagementService: IPackageManagementService,
@IBoostsService private boostsService: IBoostsService,
@ISchemaService private schemaService: ISchemaService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
lifeCycleService: ILifeCycleService,
packageManagementService: IPackageManagementService,
schemaService: ISchemaService,
codeRuntimeService: ICodeRuntimeService,
runtimeIntlService: IRuntimeIntlService,
runtimeUtilService: IRuntimeUtilService,
) {
this.eventEmitter = new EventEmitter('ExtensionHost');
this.pluginSetupContext = {
eventEmitter: this.eventEmitter,
globalState: new KeyValueStore(),
boosts: this.boostsService.toExpose(),
schema: this.schemaService,
packageManager: this.packageManagementService,
super();
whenLifeCylePhaseChange: (phase, listener) => {
return this.lifeCycleService.when(phase, listener);
this.boostsManager = new BoostsManager(
codeRuntimeService,
runtimeIntlService,
runtimeUtilService,
);
this._pluginSetupContext = {
globalState: new Map(),
boosts: this.boostsManager.toExpose(),
schema: schemaService,
packageManager: packageManagementService,
whenLifeCylePhaseChange: (phase) => {
return lifeCycleService.when(phase);
},
};
}
async registerPlugin(plugins: Plugin | Plugin[]) {
plugins = Array.isArray(plugins) ? plugins : [plugins];
const items = (Array.isArray(plugins) ? plugins : [plugins]).filter(
(plugin) => !this._pluginStore.has(plugin.name),
);
for (const item of items) {
this._pluginStore.set(item.name, item);
}
await this._doRegisterPlugins(items);
}
private async _doRegisterPlugins(plugins: Plugin[]) {
for (const plugin of plugins) {
if (this.pluginRuntimes.find((item) => item.name === plugin.name)) {
console.warn(`${plugin.name} 插件已注册`);
continue;
this._pluginDependencyGraph.lookupOrInsertNode(plugin.name);
if (plugin.dependsOn) {
for (const dependency of plugin.dependsOn) {
this._pluginDependencyGraph.insertEdge(plugin.name, dependency);
}
}
}
while (true) {
const roots = this._pluginDependencyGraph.roots();
if (roots.length === 0 || roots.every((node) => !this._pluginStore.has(node.data))) {
if (this._pluginDependencyGraph.isEmpty()) {
throw new CyclicDependencyError(this._pluginDependencyGraph);
}
break;
}
const pluginRuntime = plugin as IPluginRuntime;
pluginRuntime.status = 'ready';
this.pluginRuntimes.push(pluginRuntime);
await this.doSetupPlugin(pluginRuntime);
for (const { data } of roots) {
const plugin = this._pluginStore.get(data);
if (plugin) {
await this._doSetupPlugin(plugin);
this._pluginDependencyGraph.removeNode(plugin.name);
}
}
}
}
private async doSetupPlugin(pluginRuntime: IPluginRuntime) {
if (pluginRuntime.status === 'setup') return;
private async _doSetupPlugin(plugin: Plugin) {
if (this._activePlugins.has(plugin.name)) return;
const isSetup = (name: string) => {
const setupPlugins = this.pluginRuntimes.filter((item) => item.status === 'setup');
return setupPlugins.some((p) => p.name === name);
};
if (pluginRuntime.dependsOn?.some((dep) => !isSetup(dep))) {
return;
}
await pluginRuntime.setup(this.pluginSetupContext);
pluginRuntime.status = 'setup';
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
const readyPlugins = this.pluginRuntimes.filter((item) => item.status === 'ready');
const readyPlugin = readyPlugins.find((item) => item.dependsOn?.every((dep) => isSetup(dep)));
if (readyPlugin) {
await this.doSetupPlugin(readyPlugin);
}
await plugin.setup(this._pluginSetupContext);
this._activePlugins.add(plugin.name);
this.addDispose(plugin);
}
getPlugin(name: string): Plugin | undefined {
return this.pluginRuntimes.find((item) => item.name === name);
}
async dispose(): Promise<void> {
for (const plugin of this.pluginRuntimes) {
await plugin.destory?.();
}
return this._pluginStore.get(name);
}
}

View File

@ -1,4 +1,4 @@
import { type EventEmitter, type StringDictionary } from '@alilc/lowcode-shared';
import { type StringDictionary, type IDisposable } from '@alilc/lowcode-shared';
import { type IBoosts } from './boosts';
import { ILifeCycleService } from '../lifeCycleService';
import { type ISchemaService } from '../schema';
@ -6,23 +6,29 @@ import { type IPackageManagementService } from '../package';
import { type IStore } from '../../utils/store';
export interface PluginContext<BoostsExtends = object> {
eventEmitter: EventEmitter;
globalState: IStore<StringDictionary, string>;
boosts: IBoosts<BoostsExtends>;
schema: Pick<ISchemaService, 'get' | 'set'>;
packageManager: IPackageManagementService;
/**
*
*/
whenLifeCylePhaseChange: ILifeCycleService['when'];
}
export interface Plugin<BoostsExtends = object> {
export interface Plugin<BoostsExtends = object> extends IDisposable {
/**
* name
*/
name: string;
/**
*
* @param context
*/
setup(context: PluginContext<BoostsExtends>): void | Promise<void>;
destory?(): void | Promise<void>;
/**
*
*/
dependsOn?: string[];
}

View File

@ -1,4 +1,4 @@
import { type InstanceAccessor } from '@alilc/lowcode-shared';
import { type IInstantiationService } from '@alilc/lowcode-shared';
export interface IRenderObject {
mount: (containerOrId?: string | HTMLElement) => void | Promise<void>;
@ -6,5 +6,5 @@ export interface IRenderObject {
}
export interface RenderAdapter<Render> {
(accessor: InstanceAccessor): Render | Promise<Render>;
(instantiationService: IInstantiationService): Render | Promise<Render>;
}

View File

@ -1,4 +1,4 @@
import { Provide, createDecorator, EventEmitter, EventDisposable } from '@alilc/lowcode-shared';
import { createDecorator, Barrier } from '@alilc/lowcode-shared';
/**
*
@ -28,13 +28,15 @@ export interface ILifeCycleService {
*/
phase: LifecyclePhase;
setPhase(phase: LifecyclePhase): Promise<void>;
setPhase(phase: LifecyclePhase): void;
/**
* Returns a promise that resolves when a certain lifecycle phase
* has started.
*/
when(phase: LifecyclePhase, listener: () => void | Promise<void>): EventDisposable;
when(phase: LifecyclePhase): Promise<void>;
onWillDestory(): void;
}
export function LifecyclePhaseToString(phase: LifecyclePhase): string {
@ -52,9 +54,8 @@ export function LifecyclePhaseToString(phase: LifecyclePhase): string {
export const ILifeCycleService = createDecorator<ILifeCycleService>('lifeCycleService');
@Provide(ILifeCycleService)
export class LifeCycleService implements ILifeCycleService {
private readonly phaseWhen = new EventEmitter();
private readonly phaseWhen = new Map<LifecyclePhase, Barrier>();
private _phase = LifecyclePhase.Starting;
@ -62,7 +63,7 @@ export class LifeCycleService implements ILifeCycleService {
return this._phase;
}
async setPhase(value: LifecyclePhase) {
setPhase(value: LifecyclePhase) {
if (value < this._phase) {
throw new Error('Lifecycle cannot go backwards');
}
@ -73,10 +74,26 @@ export class LifeCycleService implements ILifeCycleService {
this._phase = value;
await this.phaseWhen.emit(LifecyclePhaseToString(value));
const barrier = this.phaseWhen.get(this._phase);
if (barrier) {
barrier.open();
this.phaseWhen.delete(this._phase);
}
}
when(phase: LifecyclePhase, listener: () => void | Promise<void>) {
return this.phaseWhen.on(LifecyclePhaseToString(phase), listener);
async when(phase: LifecyclePhase): Promise<void> {
if (phase <= this._phase) {
return;
}
let barrier = this.phaseWhen.get(phase);
if (!barrier) {
barrier = new Barrier();
this.phaseWhen.set(phase, barrier);
}
await barrier.wait();
}
onWillDestory(): void {}
}

View File

@ -1,6 +1,5 @@
import {
createDecorator,
Provide,
invariant,
type ComponentTree,
type StringDictionary,
@ -33,7 +32,6 @@ export const IComponentTreeModelService = createDecorator<IComponentTreeModelSer
'componentTreeModelService',
);
@Provide(IComponentTreeModelService)
export class ComponentTreeModelService implements IComponentTreeModelService {
constructor(
@ISchemaService private schemaService: ISchemaService,

View File

@ -1,2 +1,2 @@
export * from './loader';
export * from './package';
export * from './managementService';

View File

@ -4,16 +4,15 @@ import {
type ProCodeComponent,
type ComponentMap,
createDecorator,
Provide,
specTypes,
exportByReference,
mapPackageToUniqueId,
type Reference,
Disposable,
} from '@alilc/lowcode-shared';
import { get as lodashGet } from 'lodash-es';
import { PackageLoader } from './loader';
import { get as lodashGet, differenceWith, isEqual } from 'lodash-es';
import { PackageLoader } from './package';
import { ISchemaService } from '../schema';
import { ILifeCycleService } from '../lifeCycleService';
export interface NormalizedPackage {
id: string;
@ -50,8 +49,7 @@ export const IPackageManagementService = createDecorator<IPackageManagementServi
'packageManagementService',
);
@Provide(IPackageManagementService)
export class PackageManagementService implements IPackageManagementService {
export class PackageManagementService extends Disposable implements IPackageManagementService {
private componentsRecord: Record<string, any> = {};
private packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
@ -62,13 +60,18 @@ export class PackageManagementService implements IPackageManagementService {
private packageLoaders: PackageLoader[] = [];
constructor(
@ISchemaService private schemaService: ISchemaService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.schemaService.onChange('componentsMap', (componentsMaps) => {
this.resolveComponentMaps(componentsMaps);
});
constructor(@ISchemaService private schemaService: ISchemaService) {
super();
this.addDispose(
this.schemaService.onSchemaUpdate(({ key, previous, data }) => {
if (key === 'componentsMap') {
// todo: add remove ...
const diff = differenceWith(data, previous, isEqual);
if (diff.length > 0) this.resolveComponentMaps(diff);
}
}),
);
}
async loadPackages(packages: Package[]) {

View File

@ -1,15 +1,12 @@
import {
createDecorator,
Provide,
Intl,
type IntlApi,
type Locale,
type Translations,
Platform,
type LocaleTranslationsMap,
} from '@alilc/lowcode-shared';
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
import { ICodeRuntimeService } from './code-runtime';
import { ISchemaService } from './schema';
export interface MessageDescriptor {
key: string;
@ -18,8 +15,6 @@ export interface MessageDescriptor {
}
export interface IRuntimeIntlService {
locale: string;
localize(descriptor: MessageDescriptor): string;
setLocale(locale: Locale): void;
@ -31,32 +26,21 @@ export interface IRuntimeIntlService {
export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntimeIntlService');
@Provide(IRuntimeIntlService)
export class RuntimeIntlService implements IRuntimeIntlService {
private intl: Intl = new Intl();
public locale: string = Platform.platformLocale;
constructor(
@ILifeCycleService private lifeCycleService: ILifeCycleService,
defaultLocale: string | undefined,
i18nTranslations: LocaleTranslationsMap,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@ISchemaService private schemaService: ISchemaService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
const config = this.schemaService.get('config');
const i18nTranslations = this.schemaService.get('i18n');
if (defaultLocale) this.setLocale(defaultLocale);
if (config?.defaultLocale) {
this.setLocale(config.defaultLocale);
}
if (i18nTranslations) {
Object.keys(i18nTranslations).forEach((key) => {
this.addTranslations(key, i18nTranslations[key]);
});
}
for (const key of Object.keys(i18nTranslations)) {
this.addTranslations(key, i18nTranslations[key]);
}
this.injectScope();
});
this._injectScope();
}
localize(descriptor: MessageDescriptor): string {
@ -83,7 +67,7 @@ export class RuntimeIntlService implements IRuntimeIntlService {
this.intl.addTranslations(locale, translations);
}
private injectScope(): void {
private _injectScope(): void {
const exposed: IntlApi = {
i18n: (key, params) => {
return this.localize({ key, params });

View File

@ -2,14 +2,11 @@ import {
type AnyFunction,
type UtilDescription,
createDecorator,
Provide,
type StringDictionary,
} from '@alilc/lowcode-shared';
import { isPlainObject } from 'lodash-es';
import { IPackageManagementService } from './package';
import { ICodeRuntimeService } from './code-runtime';
import { ISchemaService } from './schema';
import { ILifeCycleService, LifecyclePhase } from './lifeCycleService';
export interface IRuntimeUtilService {
add(utilItem: UtilDescription, force?: boolean): void;
@ -20,25 +17,18 @@ export interface IRuntimeUtilService {
export const IRuntimeUtilService = createDecorator<IRuntimeUtilService>('rendererUtilService');
@Provide(IRuntimeUtilService)
export class RuntimeUtilService implements IRuntimeUtilService {
private utilsMap: Map<string, any> = new Map();
constructor(
utils: UtilDescription[] = [],
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
@IPackageManagementService private packageManagementService: IPackageManagementService,
@ISchemaService private schemaService: ISchemaService,
@ILifeCycleService private lifeCycleService: ILifeCycleService,
) {
this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => {
this.injectScope();
});
this.schemaService.onChange('utils', (utils = []) => {
for (const util of utils) {
this.add(util);
}
});
for (const util of utils) {
this.add(util);
}
this.injectScope();
}
add(utilItem: UtilDescription, force?: boolean): void;

View File

@ -1,85 +1,63 @@
import {
type Project,
createDecorator,
Provide,
EventEmitter,
type EventDisposable,
} from '@alilc/lowcode-shared';
import { isObject } from 'lodash-es';
import { Disposable, type Project, createDecorator, Events } from '@alilc/lowcode-shared';
import { isObject, isEqual, get as lodashGet, set as lodashSet } from 'lodash-es';
import { schemaValidation } from './validation';
import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService';
import { ICodeRuntimeService } from '../code-runtime';
import { type IStore, KeyValueStore } from '../../utils/store';
export interface NormalizedSchema extends Project {}
export type NormalizedSchemaKey = keyof NormalizedSchema;
export type SchemaUpdateEvent = { key: string; previous: any; data: any };
export interface ISchemaService {
initialize(schema: Project): void;
readonly onSchemaUpdate: Events.Event<SchemaUpdateEvent>;
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K];
get<T>(key: string): T | undefined;
get<T>(key: string, defaultValue?: T): T;
set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): Promise<void>;
onChange<K extends NormalizedSchemaKey>(
key: K,
listener: (v: NormalizedSchema[K]) => void,
): EventDisposable;
set(key: string, value: any): void;
}
export const ISchemaService = createDecorator<ISchemaService>('schemaService');
@Provide(ISchemaService)
export class SchemaService implements ISchemaService {
private store: IStore<NormalizedSchema, NormalizedSchemaKey> = new KeyValueStore<
NormalizedSchema,
NormalizedSchemaKey
>({
setterValidation: schemaValidation,
});
export class SchemaService extends Disposable implements ISchemaService {
private store: NormalizedSchema;
private notifyEmiiter = new EventEmitter();
private _observer = this.addDispose(new Events.Observable<SchemaUpdateEvent>());
constructor(
@ILifeCycleService private lifeCycleService: ILifeCycleService,
@ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService,
) {
this.onChange('constants', (value = {}) => {
this.codeRuntimeService.rootRuntime.getScope().set('constants', value);
});
readonly onSchemaUpdate = this._observer.subscribe;
this.lifeCycleService.when(LifecyclePhase.Destroying, () => {
this.notifyEmiiter.removeAll();
});
}
constructor(schema: unknown) {
super();
initialize(schema: unknown): void {
if (!isObject(schema)) {
throw Error('schema must a object');
}
Object.keys(schema).forEach((key) => {
// @ts-expect-error: ignore initialization
this.set(key, schema[key]);
});
}
this.store = {} as any;
for (const key of Object.keys(schema)) {
const value = (schema as any)[key];
async set<K extends NormalizedSchemaKey>(key: K, value: NormalizedSchema[K]): Promise<void> {
if (value !== this.get(key)) {
this.store.set(key, value);
await this.notifyEmiiter.emit(key, value);
// todo schemas validate
const valid = schemaValidation(key as any, value);
if (valid !== true) {
throw new Error(
`failed to config ${key.toString()}, validation error: ${valid ? valid : ''}`,
);
}
this.set(key, value);
}
}
get<K extends NormalizedSchemaKey>(key: K): NormalizedSchema[K] {
return this.store.get(key) as NormalizedSchema[K];
set(key: string, value: any): void {
const previous = this.get(key);
if (!isEqual(previous, value)) {
lodashSet(this.store, key, value);
this._observer.notify({ key, previous, data: value });
}
}
onChange<K extends keyof NormalizedSchema>(
key: K,
listener: (v: NormalizedSchema[K]) => void | Promise<void>,
): EventDisposable {
return this.notifyEmiiter.on(key, listener);
get<T>(key: string, defaultValue?: T): T {
return (lodashGet(this.store, key) ?? defaultValue) as T;
}
}

View File

@ -38,12 +38,12 @@ const SCHEMA_VALIDATIONS_OPTIONS: Partial<ValidOptionRecord> = {
},
};
export function schemaValidation<K extends keyof Project>(key: K, value: Project[K]) {
export function schemaValidation(key: string, value: unknown) {
if (!SCHEMA_KEYS.includes(key)) {
return `schema 的字段名必须是${JSON.stringify(SCHEMA_KEYS)}中的一个`;
}
const validOption = SCHEMA_VALIDATIONS_OPTIONS[key];
const validOption = (SCHEMA_VALIDATIONS_OPTIONS as any)[key];
if (validOption) {
const result = validOption.valid(value);

View File

@ -9,10 +9,6 @@ export interface AppOptions {
schema: Project;
packages?: Package[];
plugins?: Plugin[];
/**
*
*/
mode?: 'development' | 'production';
/**
* code runtime
*/
@ -32,5 +28,5 @@ export type RendererApplication<Render = unknown> = {
use(plugin: Plugin): Promise<void>;
destroy(): Promise<void>;
destroy(): void;
} & Render;

View File

@ -1,63 +0,0 @@
import { type StringDictionary } from '@alilc/lowcode-shared';
/**
* MapLike interface
*/
export interface IStore<O, K extends keyof O> {
readonly size: number;
get(key: K, defaultValue: O[K]): O[K];
get(key: K, defaultValue?: O[K]): O[K] | undefined;
set(key: K, value: O[K]): void;
delete(key: K): void;
clear(): void;
}
export class KeyValueStore<O = StringDictionary, K extends keyof O = keyof O>
implements IStore<O, K>
{
private readonly store = new Map();
private setterValidation: ((key: K, value: O[K]) => boolean | string) | undefined;
constructor(options?: { setterValidation?: (key: K, value: O[K]) => boolean | string }) {
if (options?.setterValidation) {
this.setterValidation = options.setterValidation;
}
}
get(key: K, defaultValue: O[K]): O[K];
get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined;
get(key: K, defaultValue?: O[K]): O[K] | undefined {
const value = this.store.get(key);
return value ?? defaultValue;
}
set(key: K, value: O[K]): void {
if (this.setterValidation) {
const valid = this.setterValidation(key, value);
if (valid !== true) {
console.warn(`failed to config ${key.toString()}, validation error: ${valid ? valid : ''}`);
return;
}
}
this.store.set(key, value);
}
delete(key: K): void {
this.store.delete(key);
}
clear(): void {
this.store.clear();
}
get size(): number {
return this.store.size;
}
}

View File

@ -3,5 +3,4 @@
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "../shared/src/utils/node.ts"]
}

View File

@ -2,6 +2,5 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
}

View File

@ -1,56 +0,0 @@
import { describe, it, expect } from 'vitest';
import { Barrier } from '../../src';
describe('Barrier', () => {
it('waits for barrier to open', async () => {
const barrier = new Barrier();
setTimeout(() => {
barrier.open();
}, 500); // Simulate opening the barrier after 500ms
const start = Date.now();
await barrier.wait(); // Async operation should await here
const duration = Date.now() - start;
expect(barrier.isOpen()).toBeTruthy();
expect(duration).toBeGreaterThanOrEqual(500); // Ensures we waited for at least 500ms
});
it('mutiple', async () => {
let result = '';
const b1 = new Barrier();
const b2 = new Barrier();
async function run1() {
await b1.wait();
}
async function run2() {
await b2.wait();
}
run1().then(() => {
result += 1;
});
run1().finally(() => {
result += 2;
});
run2().then(() => {
result += 3;
});
run2().finally(() => {
result += 4;
});
b1.open();
await run1();
b2.open();
await run2();
expect(result).toBe('1234');
});
});

View File

@ -24,12 +24,8 @@
"test:watch": "vitest"
},
"dependencies": {
"@abraham/reflection": "^0.12.0",
"@formatjs/intl": "^2.10.2",
"@vue/reactivity": "^3.4.23",
"inversify": "^6.0.2",
"inversify-binding-decorators": "^4.0.0",
"hookable": "^5.5.3",
"lodash-es": "^4.17.21",
"store": "^2.0.12"
},

View File

@ -0,0 +1,208 @@
import { createSingleCallFunction, Iterable } from '../utils';
/**
* An object that performs a cleanup operation when `.dispose()` is called.
*
* Some examples of how disposables are used:
*
* - An event listener that removes itself when `.dispose()` is called.
* - A resource such as a file system watcher that cleans up the resource when `.dispose()` is called.
* - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.
*/
export interface IDisposable {
dispose(): void;
}
/**
* Check if `thing` is {@link IDisposable disposable}.
*/
export function isDisposable<E>(thing: E): thing is E & IDisposable {
return (
typeof thing === 'object' &&
thing !== null &&
typeof (thing as unknown as IDisposable).dispose === 'function' &&
(thing as unknown as IDisposable).dispose.length === 0
);
}
const noop = () => {};
export abstract class Disposable implements IDisposable {
static Noop: IDisposable = { dispose: noop };
private readonly _store = new DisposableStore();
dispose(): void {
this._store.dispose();
}
/**
* Adds `o` to the collection of disposables managed by this object.
*/
protected addDispose<T extends IDisposable>(o: T): T {
if ((o as unknown as Disposable) === this) {
throw new Error('Cannot register a disposable on itself!');
}
return this._store.add(o);
}
}
/**
* Manages a collection of disposable values.
*
* This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an
* `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a
* store that has already been disposed of.
*/
export class DisposableStore implements IDisposable {
static DISABLE_DISPOSED_WARNING = false;
private readonly _toDispose = new Set<IDisposable>();
private _isDisposed = false;
/**
* Dispose of all registered disposables and mark this object as disposed.
*
* Any future disposables added to this object will be disposed of on `add`.
*/
dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this.clear();
}
/**
* @return `true` if this object has been disposed of.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Dispose of all registered disposables but do not mark this object as disposed.
*/
clear(): void {
if (this._toDispose.size === 0) {
return;
}
try {
dispose(this._toDispose);
} finally {
this._toDispose.clear();
}
}
/**
* Add a new {@link IDisposable disposable} to the collection.
*/
add<T extends IDisposable>(o: T): T {
if (!o) {
return o;
}
if ((o as unknown as DisposableStore) === this) {
throw new Error('Cannot register a disposable on itself!');
}
if (this._isDisposed) {
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
console.warn(
new Error(
'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!',
).stack,
);
}
} else {
this._toDispose.add(o);
}
return o;
}
/**
* Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the
* disposable even when the disposable is not part in the store.
*/
delete<T extends IDisposable>(o: T): void {
if (!o) {
return;
}
if ((o as unknown as DisposableStore) === this) {
throw new Error('Cannot dispose a disposable on itself!');
}
this._toDispose.delete(o);
o.dispose();
}
/**
* Deletes the value from the store, but does not dispose it.
*/
deleteAndLeak<T extends IDisposable>(o: T): void {
if (!o) {
return;
}
if (this._toDispose.has(o)) {
this._toDispose.delete(o);
}
}
}
/**
* Disposes of the value(s) passed in.
*/
export function dispose<T extends IDisposable>(disposable: T): T;
export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(
disposables: A,
): A;
export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
export function dispose<T extends IDisposable>(arg: T | Iterable<T> | undefined): any {
if (Iterable.is(arg)) {
const errors: any[] = [];
for (const d of arg) {
if (d) {
try {
d.dispose();
} catch (e) {
errors.push(e);
}
}
}
if (errors.length === 1) {
throw errors[0];
} else if (errors.length > 1) {
throw new AggregateError(errors, 'Encountered errors while disposing of store');
}
return Array.isArray(arg) ? [] : arg;
} else if (arg) {
arg.dispose();
return arg;
}
}
/**
* Turn a function that implements dispose into an {@link IDisposable}.
*
* @param fn Clean up function, guaranteed to be called only **once**.
*/
export function toDisposable(fn: () => void): IDisposable {
return {
dispose: createSingleCallFunction(() => {
fn();
}),
};
}
/**
* Combine multiple disposable values into a single {@link IDisposable}.
*/
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
return toDisposable(() => dispose(disposables));
}

View File

@ -0,0 +1,7 @@
export function illegalArgument(name?: string): Error {
if (name) {
return new Error(`Illegal argument: ${name}`);
} else {
return new Error('Illegal argument');
}
}

View File

@ -1,132 +1,59 @@
import { Hookable, type HookKeys } from 'hookable';
import { Disposable, IDisposable, toDisposable } from './disposable';
type ArrayT<T> = T extends any[] ? T : [T];
export type Event<T> = (listener: (arg: T, thisArg?: any) => any) => IDisposable;
export type Event<T = any> = (listener: EventListener<T>) => EventDisposable;
export type EventListener<T = any> = (...arguments_: ArrayT<T>) => Promise<void> | void;
export type EventDisposable = () => void;
export class Observable<T> {
private _isDisposed = false;
export interface IEmitter<T = any> {
on: Event<T>;
emit(...args: ArrayT<T>): void;
emitAsync(...args: ArrayT<T>): Promise<void>;
clear(): void;
}
private _event?: Event<T>;
private _listeners?: Set<(arg: T) => void>;
export class Emitter<T = any[]> implements IEmitter<T> {
private events: EventListener<T>[] = [];
dispose(): void {
if (this._isDisposed) return;
on(fn: EventListener<T>): EventDisposable {
this.events.push(fn);
return () => {
this.events = this.events.filter((e) => e !== fn);
};
this._listeners?.clear();
this._listeners = undefined;
this._event = undefined;
this._isDisposed = true;
}
emit(...args: ArrayT<T>) {
for (const event of this.events) {
event.call(null, ...args);
notify(arg: T): void {
if (this._isDisposed) return;
this._listeners?.forEach((listener) => listener(arg));
}
/**
* For the public to allow to subscribe to events from this Observable
*/
get subscribe(): Event<T> {
if (!this._event) {
this._event = (listener: (arg: T) => void, thisArg?: any) => {
if (this._isDisposed) {
return Disposable.Noop;
}
if (thisArg) {
listener = listener.bind(thisArg);
}
if (!this._listeners) this._listeners = new Set();
this._listeners.add(listener);
return toDisposable(() => {
this._removeListener(listener);
});
};
}
return this._event;
}
private _removeListener(listener: (arg: T) => void) {
if (this._isDisposed) return;
if (this._listeners?.has(listener)) {
this._listeners.delete(listener);
}
}
async emitAsync(...args: ArrayT<T>) {
for (const event of this.events) {
await event.call(null, ...args);
}
}
clear() {
this.events.length = 0;
}
}
export interface IEventEmitter<
EventT extends Record<string, any> = Record<string, EventListener>,
EventNameT extends HookKeys<EventT> = HookKeys<EventT>,
> {
/**
*
* add monitor to a event
* @param event
* @param listener
*/
on(event: EventNameT, listener: EventT[EventNameT]): EventDisposable;
/**
*
* @param event
* @param listener
*/
once(event: EventNameT, listener: EventT[EventNameT]): void;
/**
*
* emit a message for a event
* @param event
* @param args
*/
emit(event: EventNameT, ...args: any): Promise<any>;
/**
*
* cancel a monitor from a event
* @param event
* @param listener
*/
off(event: EventNameT, listener: EventT[EventNameT]): void;
/**
*
* @param event
* @param listener
*/
prependListener(event: EventNameT, listener: EventT[EventNameT]): EventDisposable;
/**
*
*/
removeAll(): void;
}
export class EventEmitter<
EventT extends Record<string, any> = Record<string, EventListener<any[]>>,
EventNameT extends HookKeys<EventT> = HookKeys<EventT>,
> implements IEventEmitter<EventT, EventNameT>
{
private namespace: string | undefined;
private hooks = new Hookable<EventT, EventNameT>();
constructor(namespace?: string) {
this.namespace = namespace;
}
on(event: EventNameT, listener: EventT[EventNameT]): EventDisposable {
return this.hooks.hook(event, listener);
}
once(event: EventNameT, listener: EventT[EventNameT]): void {
this.hooks.hookOnce(event, listener);
}
async emit(event: EventNameT, ...args: any) {
return this.hooks.callHook(event, ...args);
}
off(event: EventNameT, listener: EventT[EventNameT]): void {
this.hooks.removeHook(event, listener);
}
/**
*
* @param event
* @param listener
*/
prependListener(event: EventNameT, listener: EventT[EventNameT]): EventDisposable {
return this.hooks.hook(`${event}:before` as EventNameT, listener);
}
removeAll(): void {
this.hooks.removeAllHooks();
}
}

View File

@ -0,0 +1,112 @@
export class Node<T> {
readonly incoming = new Map<string, Node<T>>();
readonly outgoing = new Map<string, Node<T>>();
constructor(
readonly key: string,
readonly data: T,
) {}
}
/**
*
*/
export class Graph<T> {
private readonly _nodes = new Map<string, Node<T>>();
constructor(private readonly _hashFn: (element: T) => string) {}
roots(): Node<T>[] {
const ret: Node<T>[] = [];
for (const node of this._nodes.values()) {
if (node.outgoing.size === 0) {
ret.push(node);
}
}
return ret;
}
insertEdge(from: T, to: T): void {
const fromNode = this.lookupOrInsertNode(from);
const toNode = this.lookupOrInsertNode(to);
fromNode.outgoing.set(toNode.key, toNode);
toNode.incoming.set(fromNode.key, fromNode);
}
removeNode(data: T): void {
const key = this._hashFn(data);
this._nodes.delete(key);
for (const node of this._nodes.values()) {
node.outgoing.delete(key);
node.incoming.delete(key);
}
}
lookupOrInsertNode(data: T): Node<T> {
const key = this._hashFn(data);
let node = this._nodes.get(key);
if (!node) {
node = new Node(key, data);
this._nodes.set(key, node);
}
return node;
}
lookup(data: T): Node<T> | undefined {
return this._nodes.get(this._hashFn(data));
}
isEmpty(): boolean {
return this._nodes.size === 0;
}
toString(): string {
const data: string[] = [];
for (const [key, value] of this._nodes) {
data.push(
`${key}\n\t(-> incoming)[${[...value.incoming.keys()].join(', ')}]\n\t(outgoing ->)[${[...value.outgoing.keys()].join(',')}]\n`,
);
}
return data.join('\n');
}
/**
*
*/
findCycleSlow() {
for (const [id, node] of this._nodes) {
const seen = new Set<string>([id]);
const res = this._findCycle(node, seen);
if (res) {
return res;
}
}
return undefined;
}
private _findCycle(node: Node<T>, seen: Set<string>): string | undefined {
for (const [id, outgoing] of node.outgoing) {
if (seen.has(id)) {
return [...seen, id].join(' -> ');
}
seen.add(id);
const value = this._findCycle(outgoing, seen);
if (value) {
return value;
}
seen.delete(id);
}
return undefined;
}
}
export class CyclicDependencyError extends Error {
constructor(graph: Graph<any>) {
super('cyclic dependency between services');
this.message =
graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`;
}
}

View File

@ -1,10 +1,16 @@
import * as Platform from './platform';
import * as Events from './event';
import * as Signals from './signals';
export { Platform };
export { Events, Signals };
export * from './event';
export * from './platform';
export * from './logger';
export * from './intl';
export * from './instantiation';
export * from './signals';
export * from './keyCodes';
export * from './errors';
export * from './disposable';
export * from './linkedList';
export * from './graph';

View File

@ -0,0 +1,59 @@
/**
* Identifies a bean of type `T`.
* The name Bean comes from Spring(Java)
*/
export interface BeanIdentifier<T> {
(...args: any[]): void;
type: T;
}
export class CtorDescriptor<T> {
constructor(
readonly ctor: Constructor<T>,
readonly staticArguments: any[] = [],
) {}
}
export class BeanContainer {
private _entries = new Map<BeanIdentifier<any>, any>();
constructor(...entries: [BeanIdentifier<any>, any][]) {
for (const [id, instance] of entries) {
this.set(id, instance);
}
}
set<T>(id: BeanIdentifier<T>, instance: T | CtorDescriptor<T>): T | CtorDescriptor<T> {
const result = this._entries.get(id);
this._entries.set(id, instance);
return result;
}
has(id: BeanIdentifier<any>): boolean {
return this._entries.has(id);
}
get<T>(id: BeanIdentifier<T>): T | CtorDescriptor<T> {
return this._entries.get(id);
}
}
export type Constructor<T = any> = new (...args: any[]) => T;
const TARGET = '$TARGET$';
const DEPENDENCIES = '$DEPENDENCIES$';
export function mapDepsToBeanId(beanId: BeanIdentifier<any>, target: Constructor, index: number) {
if ((target as any)[TARGET] === target) {
(target as any)[DEPENDENCIES].push({ beanId, index });
} else {
(target as any)[DEPENDENCIES] = [{ beanId, index }];
(target as any)[TARGET] = target;
}
}
export function getBeanDependecies(
target: Constructor,
): { id: BeanIdentifier<any>; index: number }[] {
return (target as any)[DEPENDENCIES] || [];
}

View File

@ -1,34 +1,17 @@
import { inject, injectable } from 'inversify';
import { fluentProvide } from 'inversify-binding-decorators';
import { type BeanIdentifier, type Constructor, mapDepsToBeanId } from './container';
/**
* Identifies a service of type `T`.
*/
export interface ServiceIdentifier<T> {
(...args: any[]): void;
type: T;
}
const idsMap = new Map<string, BeanIdentifier<any>>();
export type Constructor<T = any> = new (...args: any[]) => T;
export function createDecorator<T>(beanId: string): BeanIdentifier<T> {
if (idsMap.has(beanId)) {
return idsMap.get(beanId)!;
}
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
const id = <any>(
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
}
);
id.toString = () => serviceId;
const id = <any>function (target: Constructor, _: string, indexOrPropertyDescriptor: any): any {
return mapDepsToBeanId(id, target, indexOrPropertyDescriptor);
};
id.toString = () => beanId;
idsMap.set(beanId, id);
return id;
}
export const Injectable = injectable;
export function Provide<T>(serviceId: ServiceIdentifier<T>, isSingleTon?: boolean) {
const ret = fluentProvide(serviceId.toString());
if (isSingleTon) {
return ret.inSingletonScope().done();
}
return ret.done();
}

View File

@ -1,2 +1,4 @@
export * from './instantiationService';
export * from './decorators';
export { createDecorator } from './decorators';
export { CtorDescriptor, BeanContainer } from './container';
export type { Constructor } from './container';

View File

@ -1,44 +1,41 @@
import '@abraham/reflection';
import { Container, interfaces, injectable } from 'inversify';
import { buildProviderModule } from 'inversify-binding-decorators';
import { ServiceIdentifier, Constructor, createDecorator } from './decorators';
import { isDisposable } from '../disposable';
import { Graph, CyclicDependencyError } from '../graph';
import {
type BeanIdentifier,
BeanContainer,
type Constructor,
getBeanDependecies,
CtorDescriptor,
} from './container';
import { createDecorator } from './decorators';
export interface InstanceAccessor {
get<T>(id: ServiceIdentifier<T>): T;
get<T>(id: BeanIdentifier<T>): T;
}
export interface IInstantiationService {
get<T>(serviceIdentifier: ServiceIdentifier<T>): T;
readonly container: BeanContainer;
bind<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>): void;
createInstance<T extends Constructor>(Ctor: T, ...args: any[]): InstanceType<T>;
set<T>(serviceIdentifier: ServiceIdentifier<T>, instance: T): void;
invokeFunction<R, TS extends any[] = []>(
fn: (accessor: InstanceAccessor, ...args: TS) => R,
...args: TS
invokeFunction<R, Args extends any[] = []>(
fn: (accessor: InstanceAccessor, ...args: Args) => R,
...args: Args
): R;
createInstance<T extends Constructor>(App: T): InstanceType<T>;
dispose(): void;
}
export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');
export class InstantiationService implements IInstantiationService {
container: Container;
private _activeInstantiations = new Set<BeanIdentifier<any>>();
constructor(options?: interfaces.ContainerOptions) {
this.container = new Container(options);
this.set(IInstantiationService, this);
this.container.load(buildProviderModule());
}
private _isDisposed = false;
private readonly _beansToMaybeDispose = new Set<any>();
get<T>(serviceIdentifier: ServiceIdentifier<T>) {
return this.container.get<T>(serviceIdentifier.toString());
}
set<T>(serviceIdentifier: ServiceIdentifier<T>, instance: T): void {
this.container.bind<T>(serviceIdentifier).toConstantValue(instance);
constructor(public readonly container: BeanContainer = new BeanContainer()) {
this.container.set(IInstantiationService, this);
}
/**
@ -48,20 +45,166 @@ export class InstantiationService implements IInstantiationService {
fn: (accessor: InstanceAccessor, ...args: TS) => R,
...args: TS
): R {
this._throwIfDisposed();
const accessor: InstanceAccessor = {
get: (id) => {
return this.get(id);
const result = this._getOrCreateInstance(id);
if (!result) {
throw new Error(`[invokeFunction] unknown service '${id}'`);
}
return result;
},
};
return fn(accessor, ...args);
}
bind<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>) {
this.container.bind<T>(serviceIdentifier).to(constructor);
/**
*
*/
createInstance<T extends Constructor>(Ctor: T, ...args: any[]): InstanceType<T> {
this._throwIfDisposed();
const beanDependencies = getBeanDependecies(Ctor).sort((a, b) => a.index - b.index);
const beanArgs = [];
for (const dependency of beanDependencies) {
const instance = this._getOrCreateInstance(dependency.id);
if (!instance) {
throw new Error(`[createInstance] ${Ctor.name} depends on UNKNOWN bean ${dependency.id}.`);
}
beanArgs.push(instance);
}
// 检查传入参数的个数,进行参数微调
const firstArgPos = beanDependencies.length > 0 ? beanDependencies[0].index : args.length;
if (args.length !== firstArgPos) {
const delta = firstArgPos - args.length;
if (delta > 0) {
args = args.concat(new Array(delta));
} else {
args = args.slice(0, firstArgPos);
}
}
return Reflect.construct<any, InstanceType<T>>(Ctor, args.concat(beanArgs));
}
createInstance<T extends Constructor>(App: T) {
injectable()(App);
return this.container.resolve<InstanceType<T>>(App);
private _getOrCreateInstance<T>(id: BeanIdentifier<T>): T {
const thing = this.container.get(id);
if (thing instanceof CtorDescriptor) {
return this._safeCreateAndCacheInstance<T>(id, thing);
} else {
return thing;
}
}
private _safeCreateAndCacheInstance<T>(id: BeanIdentifier<T>, desc: CtorDescriptor<T>): T {
if (this._activeInstantiations.has(id)) {
throw new Error(`[createInstance] illegal state - RECURSIVELY instantiating ${id}`);
}
this._activeInstantiations.add(id);
try {
return this._createAndCacheServiceInstance(id, desc);
} finally {
this._activeInstantiations.delete(id);
}
}
private _createAndCacheServiceInstance<T>(id: BeanIdentifier<T>, desc: CtorDescriptor<T>): T {
const graph = new Graph<{ id: BeanIdentifier<T>; desc: CtorDescriptor<T> }>((data) =>
data.id.toString(),
);
let cycleCount = 0;
const stack = [{ id, desc }];
const seen = new Set<string>();
while (stack.length > 0) {
const item = stack.pop()!;
if (seen.has(item.id.toString())) {
continue;
}
seen.add(item.id.toString());
graph.lookupOrInsertNode(item);
// 一个较弱但有效的循环检查启发式方法
if (cycleCount++ > 1000) {
throw new CyclicDependencyError(graph);
}
// check all dependencies for existence and if they need to be created first
for (const dependency of getBeanDependecies(item.desc.ctor)) {
const instanceOrDesc = this.container.get(dependency.id);
if (!instanceOrDesc) {
throw new Error(
`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`,
);
}
if (instanceOrDesc instanceof CtorDescriptor) {
const d = {
id: dependency.id,
desc: instanceOrDesc,
};
graph.insertEdge(item, d);
stack.push(d);
}
}
while (true) {
const roots = graph.roots();
// if there is no more roots but still
// nodes in the graph we have a cycle
if (roots.length === 0) {
if (!graph.isEmpty()) {
throw new CyclicDependencyError(graph);
}
break;
}
for (const { data } of roots) {
// Repeat the check for this still being a service sync descriptor. That's because
// instantiating a dependency might have side-effect and recursively trigger instantiation
// so that some dependencies are now fullfilled already.
const instanceOrDesc = this.container.get(data.id);
if (instanceOrDesc instanceof CtorDescriptor) {
// create instance and overwrite the service collections
const instance = this.createInstance(
instanceOrDesc.ctor,
instanceOrDesc.staticArguments,
);
this._beansToMaybeDispose.add(instance);
this.container.set(data.id, instance);
}
graph.removeNode(data);
}
}
}
return this.container.get(id) as T;
}
dispose(): void {
if (this._isDisposed) return;
// dispose all services created by this service
for (const candidate of this._beansToMaybeDispose) {
if (isDisposable(candidate)) {
candidate.dispose();
}
}
this._beansToMaybeDispose.clear();
this._isDisposed = true;
}
private _throwIfDisposed(): void {
if (this._isDisposed) {
throw new Error('InstantiationService has been disposed');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,13 @@ export function platformToString(platform: PlatformEnum): PlatformName {
}
}
export const enum OperatingSystem {
Windows = 1,
Macintosh = 2,
Linux = 3
}
export const OS = (isMacintosh || isIOS ? OperatingSystem.Macintosh : (isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
export let platform: PlatformEnum = PlatformEnum.Unknown;
if (isMacintosh) {
platform = PlatformEnum.Mac;

View File

@ -0,0 +1,33 @@
import { AnyFunction } from '../types';
/**
* Given a function, returns a function that is only calling that function once.
*/
export function createSingleCallFunction<T extends AnyFunction>(
this: unknown,
fn: T,
fnDidRunCallback?: () => void,
): T {
const _this = this;
let didCall = false;
let result: unknown;
return function (...args: any[]) {
if (didCall) {
return result;
}
didCall = true;
if (fnDidRunCallback) {
try {
result = fn.apply(_this, args);
} finally {
fnDidRunCallback();
}
} else {
result = fn.apply(_this, args);
}
return result;
} as unknown as T;
}

View File

@ -8,3 +8,4 @@ export * from './types';
export * from './async';
export * from './node';
export * from './resource';
export * from './functional';

View File

@ -5,3 +5,7 @@ export function first<T>(iterable: Iterable<T>): T | undefined {
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
return !iterable || iterable[Symbol.iterator]().next().done === true;
}
export function is<T = any>(thing: any): thing is Iterable<T> {
return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function';
}

View File

@ -4,5 +4,4 @@
"allowJs": true,
"outDir": "dist"
},
"include": ["src"]
}