From 3b160a6d7c7f4c7a6e303ffe87d35d0bb1ba316b Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 7 Oct 2022 12:51:19 +0200 Subject: [PATCH] delayed service shouldn't be created by early listeners Injected service proxies can be created via event listeners. We can be smart about that because these proxies won't ever fire aways --- .../common/instantiationService.ts | 40 ++++++++++++ .../test/common/instantiationService.test.ts | 65 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 3840b1bec119d..bf9deb002c5ae 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -4,11 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { IdleValue } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; import { illegalState } from 'vs/base/common/errors'; +import { toDisposable } from 'vs/base/common/lifecycle'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Graph } from 'vs/platform/instantiation/common/graph'; import { IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { LinkedList } from 'vs/base/common/linkedList'; // TRACING const _enableAllTracing = false @@ -246,15 +249,52 @@ export class InstantiationService implements IInstantiationService { // Return a proxy object that's backed by an idle value. That // strategy is to instantiate services in our idle time or when actually // needed but not when injected into a consumer + + // return "empty events" when the service isn't instantiated yet + const earlyListeners = new Map>>>(); + const idle = new IdleValue(() => { const result = child._createInstance(ctor, args, _trace); + + // early listeners that we kept are now being subscribed to + // the real service + for (const [key, values] of earlyListeners) { + const candidate = >(result)[key]; + if (typeof candidate === 'function') { + for (const listener of values) { + candidate.apply(result, listener); + } + } + } + earlyListeners.clear(); + return result; }); return new Proxy(Object.create(null), { get(target: any, key: PropertyKey): any { + + if (!idle.isInitialized) { + // looks like an event + if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) { + let list = earlyListeners.get(key); + if (!list) { + list = new LinkedList(); + earlyListeners.set(key, list); + } + const event: Event = (callback, thisArg, disposables) => { + const rm = list!.push([callback, thisArg, disposables]); + return toDisposable(rm); + }; + return event; + } + } + + // value already exists if (key in target) { return target[key]; } + + // create value const obj = idle.value; let prop = obj[key]; if (typeof prop !== 'function') { diff --git a/src/vs/platform/instantiation/test/common/instantiationService.test.ts b/src/vs/platform/instantiation/test/common/instantiationService.test.ts index a737b46533cd4..fabe310031861 100644 --- a/src/vs/platform/instantiation/test/common/instantiationService.test.ts +++ b/src/vs/platform/instantiation/test/common/instantiationService.test.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { Emitter, Event } from 'vs/base/common/event'; +import { dispose } from 'vs/base/common/lifecycle'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; @@ -459,4 +461,67 @@ suite('Instantiation Service', () => { assert.strictEqual(cycle, 'A -> B -> A'); } }); + + test('Delayed and events', function () { + const A = createDecorator('A'); + interface A { + _serviceBrand: undefined; + onDidDoIt: Event; + doIt(): void; + } + + let created = false; + class AImpl implements A { + _serviceBrand: undefined; + _doIt = 0; + + _onDidDoIt = new Emitter(); + onDidDoIt: Event = this._onDidDoIt.event; + + constructor() { + created = true; + } + + doIt(): void { + this._doIt += 1; + this._onDidDoIt.fire(this); + } + } + + const insta = new InstantiationService(new ServiceCollection( + [A, new SyncDescriptor(AImpl, undefined, true)], + ), true, undefined, true); + + class Consumer { + constructor(@A readonly a: A) { + // eager subscribe -> NO service instance + } + } + + const c: Consumer = insta.createInstance(Consumer); + let eventCount = 0; + + // subscribing to event doesn't trigger instantiation + const listener = (e: any) => { + assert.ok(e instanceof AImpl); + eventCount++; + }; + const d1 = c.a.onDidDoIt(listener); + const d2 = c.a.onDidDoIt(listener); + assert.strictEqual(created, false); + assert.strictEqual(eventCount, 0); + d2.dispose(); + + // instantiation happens on first call + c.a.doIt(); + assert.strictEqual(created, true); + assert.strictEqual(eventCount, 1); + + + const d3 = c.a.onDidDoIt(listener); + c.a.doIt(); + assert.strictEqual(eventCount, 3); + + dispose([d1, d3]); + }); });