Skip to content

Commit

Permalink
feat(loader): support options.group
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 30, 2024
1 parent 16b1a89 commit 1641e2e
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 44 deletions.
96 changes: 57 additions & 39 deletions packages/loader/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,56 @@ export class Entry {
return id
}

get requiredDeps() {
return Array.isArray(this.options.inject)
? this.options.inject
: this.options.inject?.required ?? []
}

get deps() {
return Array.isArray(this.options.inject)
? this.options.inject
: [
...this.options.inject?.required ?? [],
...this.options.inject?.optional ?? [],
]
}

get disabled() {
// group is always enabled
if (this.options.group) return false
let entry: Entry | undefined = this
do {
if (entry.options.disabled) return true
entry = entry.parent.ctx.scope.entry
} while (entry)
return false
}

_check() {
if (this.disabled) return false
for (const name of this.requiredDeps) {
let key = this.parent.ctx[Context.isolate][name]
const label = this.options.isolate?.[name]
if (label) {
const realm = this.resolveRealm(label)
key = (this.loader.realms[realm] ?? Object.create(null))[name] ?? Symbol(`${name}${realm}`)
}
if (!key || isNullable(this.parent.ctx[key])) return false
}
return true
}

async checkService(name: string) {
if (!this.requiredDeps.includes(name)) return
const ready = this._check()
if (ready && !this.fork) {
await this.start()
} else if (!ready && this.fork) {
await this.stop()
}
}

resolveRealm(label: string | true) {
if (label === true) {
return '#' + this.id
Expand Down Expand Up @@ -131,6 +181,13 @@ export class Entry {
if (this.fork && 'config' in options) {
this.suspend = true
this.fork.update(this.options.config)
} else if (this.subgroup && 'disabled' in options) {
const tree = this.subtree ?? this.parent.tree
for (const options of this.subgroup.data) {
tree.store[options.id].update({
disabled: options.disabled,
})
}
}

// step 4.3: replace service impl
Expand Down Expand Up @@ -161,45 +218,6 @@ export class Entry {
return ctx
}

get requiredDeps() {
return Array.isArray(this.options.inject)
? this.options.inject
: this.options.inject?.required ?? []
}

get deps() {
return Array.isArray(this.options.inject)
? this.options.inject
: [
...this.options.inject?.required ?? [],
...this.options.inject?.optional ?? [],
]
}

_check() {
if (this.options.disabled) return false
for (const name of this.requiredDeps) {
let key = this.parent.ctx[Context.isolate][name]
const label = this.options.isolate?.[name]
if (label) {
const realm = this.resolveRealm(label)
key = (this.loader.realms[realm] ?? Object.create(null))[name] ?? Symbol(`${name}${realm}`)
}
if (!key || isNullable(this.parent.ctx[key])) return false
}
return true
}

async checkService(name: string) {
if (!this.requiredDeps.includes(name)) return
const ready = this._check()
if (ready && !this.fork) {
await this.start()
} else if (!ready && this.fork) {
await this.stop()
}
}

async update(options: Partial<Entry.Options>, override = false) {
const legacy = { ...this.options }

Expand Down
22 changes: 17 additions & 5 deletions packages/loader/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,29 @@ export abstract class Loader extends ImportTree {
})

this.ctx.on('internal/fork', (fork) => {
// 1. set `fork.entry`
if (fork.parent[Entry.key]) {
fork.entry = fork.parent[Entry.key]
delete fork.parent[Entry.key]
}
// fork.uid: fork is created (we only care about fork dispose event)
// fork.parent.runtime.plugin !== group: fork is not tracked by loader
if (fork.uid || !fork.entry) return
// fork is disposed by main scope (e.g. hmr plugin)
// normal: ctx.dispose() -> fork / runtime dispose -> delete(plugin)

// 2. handle self-dispose
// We only care about `ctx.scope.dispose()`, so we need to filter out other cases.

// case 1: fork is created
if (fork.uid) return

// case 2: fork is not tracked by loader
if (!fork.entry) return

// case 3: fork is disposed outside of plugin
// self-dispose: ctx.scope.dispose() -> fork / runtime dispose -> delete(plugin)
// hmr: delete(plugin) -> runtime dispose -> fork dispose
if (!this.ctx.registry.has(fork.runtime.plugin)) return

// case 4: fork is disposed by inject checker / config file hmr / ancestor group
if (!fork.entry._check()) return

fork.parent.emit('loader/entry', 'unload', fork.entry)
fork.entry.options.disabled = true
fork.entry.fork = undefined
Expand Down
164 changes: 164 additions & 0 deletions packages/loader/tests/group.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { mock } from 'node:test'
import { expect } from 'chai'
import { Context } from '@cordisjs/core'
import { defineProperty } from 'cosmokit'
import MockLoader from './utils'

describe('group management: basic support', () => {
const root = new Context()
root.plugin(MockLoader)
const loader = root.loader

const dispose = mock.fn()
const foo = loader.mock('foo', defineProperty((ctx: Context) => {
ctx.on('dispose', dispose)
}, 'reusable', true))

before(() => loader.start())

beforeEach(() => {
foo.mock.resetCalls()
dispose.mock.resetCalls()
})

let outer!: string
let inner!: string

it('initialize', async () => {
outer = await loader.create({
name: 'cordis/group',
group: true,
config: [{
name: 'foo',
}],
})

inner = await loader.create({
name: 'cordis/group',
group: true,
config: [{
name: 'foo',
}],
}, outer)

expect(foo.mock.calls).to.have.length(2)
expect(dispose.mock.calls).to.have.length(0)
expect([...loader.entries()]).to.have.length(4)
})

it('disable inner', async () => {
await loader.update(inner, { disabled: true })

expect(foo.mock.calls).to.have.length(0)
expect(dispose.mock.calls).to.have.length(1)
expect([...loader.entries()]).to.have.length(4)
})

it('disable outer', async () => {
await loader.update(outer, { disabled: true })

expect(foo.mock.calls).to.have.length(0)
expect(dispose.mock.calls).to.have.length(1)
expect([...loader.entries()]).to.have.length(4)
})

it('enable inner', async () => {
await loader.update(inner, { disabled: null })

expect(foo.mock.calls).to.have.length(0) // outer is still disabled
expect(dispose.mock.calls).to.have.length(0)
expect([...loader.entries()]).to.have.length(4)
})

it('enable outer', async () => {
await loader.update(outer, { disabled: null })

expect(foo.mock.calls).to.have.length(2)
expect(dispose.mock.calls).to.have.length(0)
expect([...loader.entries()]).to.have.length(4)
})
})

describe('group management: transfer', () => {
const root = new Context()
root.plugin(MockLoader)
const loader = root.loader

const dispose = mock.fn()
const foo = loader.mock('foo', defineProperty((ctx: Context) => {
ctx.on('dispose', dispose)
}, 'reusable', true))

before(() => loader.start())

beforeEach(() => {
foo.mock.resetCalls()
dispose.mock.resetCalls()
})

let alpha!: string
let beta!: string
let gamma!: string
let id!: string

it('initialize', async () => {
id = await loader.create({
name: 'foo',
})

alpha = await loader.create({
name: 'cordis/group',
group: true,
config: [],
})

beta = await loader.create({
name: 'cordis/group',
group: true,
disabled: true,
config: [],
}, alpha)

gamma = await loader.create({
name: 'cordis/group',
group: true,
config: [],
}, beta)

expect(foo.mock.calls).to.have.length(1)
expect(dispose.mock.calls).to.have.length(0)
expect([...loader.entries()]).to.have.length(4)
})

it('enabled -> enabled', async () => {
await loader.update(id, {}, alpha)

expect(foo.mock.calls).to.have.length(0)
expect(dispose.mock.calls).to.have.length(0)
expect([...loader.entries()]).to.have.length(4)
})

it('enabled -> disabled', async () => {
await loader.update(id, {}, beta)

expect(foo.mock.calls).to.have.length(0)
expect(dispose.mock.calls).to.have.length(1)
expect([...loader.entries()]).to.have.length(4)
})

it('disabled -> disabled', async () => {
await loader.update(id, {}, gamma)

expect(foo.mock.calls).to.have.length(0) // outer is still disabled
expect(dispose.mock.calls).to.have.length(0)
expect([...loader.entries()]).to.have.length(4)
})

it('disabled -> enabled', async () => {
await loader.update(id, {}, null)

expect(foo.mock.calls).to.have.length(1)
expect(dispose.mock.calls).to.have.length(0)
expect([...loader.entries()]).to.have.length(4)
})
})

0 comments on commit 1641e2e

Please sign in to comment.