From c339c508e2d65af12af09cfe329a1a1afc5a3fc6 Mon Sep 17 00:00:00 2001 From: Shigma Date: Thu, 16 Jun 2022 18:36:22 +0800 Subject: [PATCH] fix: support using and reusable in isolated contexts --- src/state.ts | 46 ++++++++++++++++++------------- tests/isolate.spec.ts | 63 +++++++++++++++++++++++++++++++++++++++++++ tests/service.spec.ts | 34 ----------------------- tests/update.spec.ts | 6 +++-- 4 files changed, 94 insertions(+), 55 deletions(-) create mode 100644 tests/isolate.spec.ts diff --git a/src/state.ts b/src/state.ts index 4ebb5b3..3a7fcc2 100644 --- a/src/state.ts +++ b/src/state.ts @@ -12,6 +12,8 @@ function isConstructor(func: Function) { return true } +export const kPreserve = Symbol('preserve') + export abstract class State { uid: number runtime: Runtime @@ -27,6 +29,20 @@ export abstract class State { this.context = parent.extend({ state: this }) } + protected init() { + if (this.runtime.using.length) { + const dispose = this.context.on('internal/service', (name) => { + if (!this.runtime.using.includes(name)) return + this.restart() + }) + defineProperty(dispose, kPreserve, true) + } + } + + protected check() { + return this.runtime.using.every(name => this.context[name]) + } + protected clear(preserve = false) { this.disposables = this.disposables.splice(0, Infinity).filter((dispose) => { if (preserve && dispose[kPreserve]) return true @@ -35,8 +51,6 @@ export abstract class State { } } -export const kPreserve = Symbol('preserve') - export class Fork extends State { constructor(parent: Context, config: any, runtime: Runtime) { super(parent, config) @@ -47,12 +61,13 @@ export class Fork extends State { runtime.children.push(this) runtime.disposables.push(this.dispose) parent.state?.disposables.push(this.dispose) + if (runtime.isReusable) this.init() this.restart() } restart() { - this.clear() - if (!this.runtime.isActive) return + this.clear(true) + if (!this.check()) return for (const fork of this.runtime.forkables) { fork(this.context, this.config) } @@ -89,7 +104,7 @@ export class Runtime extends State { using: readonly string[] = [] forkables: Function[] = [] children: Fork[] = [] - isActive: boolean + isReusable: boolean constructor(private registry: Registry, public plugin: Plugin, config: any) { super(registry.caller, config) @@ -117,18 +132,13 @@ export class Runtime extends State { init() { this.schema = this.plugin['Config'] || this.plugin['schema'] this.using = this.plugin['using'] || [] + this.isReusable = this.plugin['reusable'] this.context.emit('plugin-added', this) - if (this.plugin['reusable']) { + if (this.isReusable) { this.forkables.push(this.apply) - } - - if (this.using.length) { - const dispose = this.context.on('internal/service', (name) => { - if (!this.using.includes(name)) return - this.restart() - }) - defineProperty(dispose, kPreserve, true) + } else { + super.init() } this.restart() @@ -153,13 +163,11 @@ export class Runtime extends State { } restart() { - this.isActive = false this.clear(true) - if (this.using.some(name => !this.context[name])) return + if (!this.check()) return // execute plugin body - this.isActive = true - if (!this.plugin['reusable']) { + if (!this.isReusable) { this.apply(this.context, this.config) } @@ -170,7 +178,7 @@ export class Runtime extends State { update(config: any, manual = false) { if (this.isForkable) { - this.context.emit('internal/warn', `attempting to update forkable plugin "${this.plugin.name}", which may lead unexpected behavior`) + this.context.emit('internal/warn', `attempting to update forkable plugin "${this.plugin.name}", which may lead to unexpected behavior`) } const oldConfig = this.config const resolved = Registry.validate(this.runtime.plugin, config) diff --git a/tests/isolate.spec.ts b/tests/isolate.spec.ts new file mode 100644 index 0000000..95abb6f --- /dev/null +++ b/tests/isolate.spec.ts @@ -0,0 +1,63 @@ +import { App, Context } from '../src' +import { expect } from 'chai' +import * as jest from 'jest-mock' +import './shared' + +describe('Isolation', () => { + it('isolated context', async () => { + const app = new App() + const ctx = app.isolate(['foo']) + + const outer = jest.fn() + const inner = jest.fn() + app.on('internal/service', outer) + ctx.on('internal/service', inner) + + app.foo = { bar: 100 } + expect(app.foo).to.deep.equal({ bar: 100 }) + expect(ctx.foo).to.be.not.ok + expect(outer.mock.calls).to.have.length(1) + expect(inner.mock.calls).to.have.length(0) + + ctx.foo = { bar: 200 } + expect(app.foo).to.deep.equal({ bar: 100 }) + expect(ctx.foo).to.deep.equal({ bar: 200 }) + expect(outer.mock.calls).to.have.length(1) + expect(inner.mock.calls).to.have.length(1) + + app.foo = null + expect(app.foo).to.be.not.ok + expect(ctx.foo).to.deep.equal({ bar: 200 }) + expect(outer.mock.calls).to.have.length(2) + expect(inner.mock.calls).to.have.length(1) + + ctx.foo = null + expect(app.foo).to.be.not.ok + expect(ctx.foo).to.be.not.ok + expect(outer.mock.calls).to.have.length(2) + expect(inner.mock.calls).to.have.length(2) + }) + + it('isolated fork', () => { + const app = new App() + const callback = jest.fn() + const plugin = { + reusable: true, + using: ['foo'], + apply: callback, + } + + const ctx1 = app.isolate(['foo']) + ctx1.plugin(plugin) + const ctx2 = app.isolate(['foo']) + ctx2.plugin(plugin) + expect(callback.mock.calls).to.have.length(0) + + app.foo = { bar: 100 } + expect(callback.mock.calls).to.have.length(0) + ctx1.foo = { bar: 200 } + expect(callback.mock.calls).to.have.length(1) + ctx2.foo = { bar: 300 } + expect(callback.mock.calls).to.have.length(2) + }) +}) diff --git a/tests/service.spec.ts b/tests/service.spec.ts index 82fbbe1..27b52ec 100644 --- a/tests/service.spec.ts +++ b/tests/service.spec.ts @@ -104,38 +104,4 @@ describe('Service', () => { expect(stop.mock.calls).to.have.length(1) expect(fork.mock.calls).to.have.length(2) }) - - it('isolated context', async () => { - const app = new App() - const ctx = app.isolate(['foo']) - - const outer = jest.fn() - const inner = jest.fn() - app.on('internal/service', outer) - ctx.on('internal/service', inner) - - app.foo = { bar: 100 } - expect(app.foo).to.deep.equal({ bar: 100 }) - expect(ctx.foo).to.be.not.ok - expect(outer.mock.calls).to.have.length(1) - expect(inner.mock.calls).to.have.length(0) - - ctx.foo = { bar: 200 } - expect(app.foo).to.deep.equal({ bar: 100 }) - expect(ctx.foo).to.deep.equal({ bar: 200 }) - expect(outer.mock.calls).to.have.length(1) - expect(inner.mock.calls).to.have.length(1) - - app.foo = null - expect(app.foo).to.be.not.ok - expect(ctx.foo).to.deep.equal({ bar: 200 }) - expect(outer.mock.calls).to.have.length(2) - expect(inner.mock.calls).to.have.length(1) - - ctx.foo = null - expect(app.foo).to.be.not.ok - expect(ctx.foo).to.be.not.ok - expect(outer.mock.calls).to.have.length(2) - expect(inner.mock.calls).to.have.length(2) - }) }) diff --git a/tests/update.spec.ts b/tests/update.spec.ts index 8fbf323..2a2014c 100644 --- a/tests/update.spec.ts +++ b/tests/update.spec.ts @@ -81,11 +81,13 @@ describe('Update', () => { app.foo = {} expect(callback.mock.calls).to.have.length(1) expect(callback.mock.calls[0][1]).to.deep.equal({ value: 2 }) - expect(fork.runtime.disposables).to.have.length(2) + expect(fork.disposables).to.have.length(1) // service listener + expect(fork.runtime.disposables).to.have.length(1) // fork fork.update({ value: 3 }) expect(callback.mock.calls).to.have.length(2) expect(callback.mock.calls[1][1]).to.deep.equal({ value: 3 }) - expect(fork.runtime.disposables).to.have.length(2) + expect(fork.disposables).to.have.length(1) // service listener + expect(fork.runtime.disposables).to.have.length(1) // fork }) })