From 21fbe2585887eb3d5794235c02aa33f4485778da Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 13 Jul 2019 19:50:16 -0700 Subject: [PATCH] Dependency Injection prototype --- src/Terminal.ts | 15 +++- src/common/services/BufferService.ts | 2 +- src/common/services/CoreService.ts | 4 +- src/common/services/DirtyRowService.ts | 2 +- src/common/services/InstantiationService.ts | 81 +++++++++++++++++++ src/common/services/ServiceRegistry.ts | 43 ++++++++++ .../services/{Services.d.ts => Services.ts} | 15 ++++ src/common/tsconfig.json | 3 + src/tsconfig-library-base.json | 3 +- tslint.json | 7 +- 10 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 src/common/services/InstantiationService.ts create mode 100644 src/common/services/ServiceRegistry.ts rename src/common/services/{Services.d.ts => Services.ts} (86%) diff --git a/src/Terminal.ts b/src/Terminal.ts index e23ea7783..11ef2d6ee 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -47,7 +47,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { IOptionsService, IBufferService, ICoreService, ILogService, IDirtyRowService } from 'common/services/Services'; +import { IOptionsService, IBufferService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; @@ -61,6 +61,7 @@ import { CoreService } from 'common/services/CoreService'; import { LogService } from 'common/services/LogService'; import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types'; import { DirtyRowService } from 'common/services/DirtyRowService'; +import { InstantiationService } from 'common/services/InstantiationService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -113,6 +114,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _bufferService: IBufferService; private _coreService: ICoreService; private _dirtyRowService: IDirtyRowService; + private _instantiationService: IInstantiationService; private _logService: ILogService; public optionsService: IOptionsService; @@ -239,11 +241,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp super(); // Setup and initialize common services + this._instantiationService = new InstantiationService(); this.optionsService = new OptionsService(options); - this._bufferService = new BufferService(this.optionsService); - this._coreService = new CoreService(() => this.scrollToBottom(), this._bufferService, this.optionsService); + this._instantiationService.setService(IOptionsService, this.optionsService); + this._bufferService = this._instantiationService.createInstance(BufferService); + this._instantiationService.setService(IBufferService, this._bufferService); + this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); + this._instantiationService.setService(ICoreService, this._coreService); this._coreService.onData(e => this._onData.fire(e)); - this._dirtyRowService = new DirtyRowService(this._bufferService); + this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); + // this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); this._logService = new LogService(this.optionsService); this._setupOptionsListeners(); diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 6ff080617..e7dd1b646 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -18,7 +18,7 @@ export class BufferService implements IBufferService { public get buffer(): IBuffer { return this.buffers.active; } constructor( - private _optionsService: IOptionsService + @IOptionsService private _optionsService: IOptionsService ) { this.cols = Math.max(_optionsService.options.cols, MINIMUM_COLS); this.rows = Math.max(_optionsService.options.rows, MINIMUM_ROWS); diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 295b5a08e..da60e7878 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -23,8 +23,8 @@ export class CoreService implements ICoreService { constructor( // TODO: Move this into a service private readonly _scrollToBottom: () => void, - private readonly _bufferService: IBufferService, - private readonly _optionsService: IOptionsService + @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService ) { this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); } diff --git a/src/common/services/DirtyRowService.ts b/src/common/services/DirtyRowService.ts index 58f40dca2..b89fb918d 100644 --- a/src/common/services/DirtyRowService.ts +++ b/src/common/services/DirtyRowService.ts @@ -13,7 +13,7 @@ export class DirtyRowService implements IDirtyRowService { public get end(): number { return this._end; } constructor( - private readonly _bufferService: IBufferService + @IBufferService private readonly _bufferService: IBufferService ) { this.clearRange(); } diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts new file mode 100644 index 000000000..fb846e7a8 --- /dev/null +++ b/src/common/services/InstantiationService.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IInstantiationService, IServiceIdentifier } from 'common/services/Services'; +import { getServiceDependencies } from 'common/services/ServiceRegistry'; + +declare const console: any; + +export class ServiceCollection { + + private _entries = new Map, any>(); + + constructor(...entries: [IServiceIdentifier, any][]) { + for (const [id, service] of entries) { + this.set(id, service); + } + } + + set(id: IServiceIdentifier, instance: T): T { + const result = this._entries.get(id); + this._entries.set(id, instance); + return result; + } + + forEach(callback: (id: IServiceIdentifier, instance: any) => any): void { + this._entries.forEach((value, key) => callback(key, value)); + } + + has(id: IServiceIdentifier): boolean { + return this._entries.has(id); + } + + get(id: IServiceIdentifier): T { + return this._entries.get(id); + } +} + +export class InstantiationService implements IInstantiationService { + private readonly _services: ServiceCollection = new ServiceCollection(); + + constructor() { + this._services.set(IInstantiationService, this); + } + + public setService(id: IServiceIdentifier, instance: T): void { + this._services.set(id, instance); + } + + public createInstance(ctor: any, ...args: any[]): any { + const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index); + + let serviceArgs: any[] = []; + for (const dependency of serviceDependencies) { + let service = this._services.get(dependency.id); + if (!service) { + throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`); + } + serviceArgs.push(service); + } + + let firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length; + + // check for argument mismatches, adjust static args if needed + if (args.length !== firstServiceArgPos) { + console.warn(`[createInstance] First service dependency of ${ctor.name} at position ${ + firstServiceArgPos + 1} conflicts with ${args.length} static arguments`); + + let delta = firstServiceArgPos - args.length; + if (delta > 0) { + args = args.concat(new Array(delta)); + } else { + args = args.slice(0, firstServiceArgPos); + } + } + console.log('args', args, 'serviceArgs', serviceArgs); + // now create the instance + return new ctor(...[...args, ...serviceArgs]); + } +} diff --git a/src/common/services/ServiceRegistry.ts b/src/common/services/ServiceRegistry.ts new file mode 100644 index 000000000..2d7ed8113 --- /dev/null +++ b/src/common/services/ServiceRegistry.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IServiceIdentifier } from 'common/services/Services'; + +const DI_TARGET = 'di$target'; +const DI_DEPENDENCIES = 'di$dependencies'; + +export const serviceRegistry: Map> = new Map(); + +export function getServiceDependencies(ctor: any): { id: IServiceIdentifier, index: number, optional: boolean }[] { + return ctor[DI_DEPENDENCIES] || []; +} + +export function createDecorator(id: string): IServiceIdentifier { + if (serviceRegistry.has(id)) { + return serviceRegistry.get(id)!; + } + + const decorator = function (target: Function, key: string, index: number): any { + if (arguments.length !== 3) { + throw new Error('@IServiceName-decorator can only be used to decorate a parameter'); + } + + storeServiceDependency(decorator, target, index); + }; + + decorator.toString = () => id; + + serviceRegistry.set(id, decorator); + return decorator; +} + +function storeServiceDependency(id: Function, target: Function, index: number): void { + if ((target as any)[DI_TARGET] === target) { + (target as any)[DI_DEPENDENCIES].push({ id, index }); + } else { + (target as any)[DI_DEPENDENCIES] = [{ id, index }]; + (target as any)[DI_TARGET] = target; + } +} diff --git a/src/common/services/Services.d.ts b/src/common/services/Services.ts similarity index 86% rename from src/common/services/Services.d.ts rename to src/common/services/Services.ts index 427728f17..837b9b809 100644 --- a/src/common/services/Services.d.ts +++ b/src/common/services/Services.ts @@ -6,7 +6,9 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IDecPrivateModes } from 'common/Types'; +import { createDecorator } from 'common/services/ServiceRegistry'; +export const IBufferService = createDecorator('BufferService'); export interface IBufferService { readonly cols: number; readonly rows: number; @@ -19,6 +21,7 @@ export interface IBufferService { reset(): void; } +export const ICoreService = createDecorator('CoreService'); export interface ICoreService { readonly decPrivateModes: IDecPrivateModes; @@ -48,6 +51,17 @@ export interface IDirtyRowService { markAllDirty(): void; } +export interface IServiceIdentifier { + (...args: any[]): void; + type: T; +} + +export const IInstantiationService = createDecorator('InstantiationService'); +export interface IInstantiationService { + setService(id: IServiceIdentifier, instance: T): void; + createInstance(ctor: any, ...rest: any[]): any; +} + export interface ILogService { debug(message: any, ...optionalParams: any[]): void; info(message: any, ...optionalParams: any[]): void; @@ -55,6 +69,7 @@ export interface ILogService { error(message: any, ...optionalParams: any[]): void; } +export const IOptionsService = createDecorator('OptionsService'); export interface IOptionsService { readonly options: ITerminalOptions; diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index dca04f9ac..59050a0ba 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../tsconfig-library-base", "compilerOptions": { + "lib": [ + "es2015" + ], "outDir": "../../out", "types": [ "../../node_modules/@types/mocha" diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json index 7a9eedf3d..e08695d75 100644 --- a/src/tsconfig-library-base.json +++ b/src/tsconfig-library-base.json @@ -3,6 +3,7 @@ "compilerOptions": { "composite": true, "strict": true, - "declarationMap": true + "declarationMap": true, + "experimentalDecorators": true } } diff --git a/tslint.json b/tslint.json index a18111bfd..43662039f 100644 --- a/tslint.json +++ b/tslint.json @@ -71,12 +71,6 @@ "variable-declaration": "nospace" } ], - "variable-name": [ - true, - "ban-keywords", - "check-format", - "allow-leading-underscore" - ], "whitespace": [ true, "check-branch", @@ -99,6 +93,7 @@ {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "require"}, {"type": "member", "modifiers": ["private"], "format": "camelCase", "leadingUnderscore": "require"}, {"type": "variable", "modifiers": ["const"], "format": ["camelCase", "UPPER_CASE"]}, + {"type": "variable", "modifiers": ["const", "export"], "filter": "^I.+Service$", "format": "PascalCase", "prefix": "I"}, {"type": "interface", "prefix": "I"} ], "no-else-after-return": {