Skip to content

Commit

Permalink
fix: support using and reusable in isolated contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 16, 2022
1 parent 9024432 commit c339c50
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 55 deletions.
46 changes: 27 additions & 19 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ function isConstructor(func: Function) {
return true
}

export const kPreserve = Symbol('preserve')

export abstract class State {
uid: number
runtime: Runtime
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}

Expand All @@ -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)
Expand Down
63 changes: 63 additions & 0 deletions tests/isolate.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
34 changes: 0 additions & 34 deletions tests/service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
6 changes: 4 additions & 2 deletions tests/update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})

0 comments on commit c339c50

Please sign in to comment.