Skip to content

Commit

Permalink
feat(loader): support reactive options.isolate
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 22, 2024
1 parent a763e1c commit cf65001
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 7 deletions.
44 changes: 39 additions & 5 deletions packages/loader/src/shared.ts
Expand Up @@ -49,21 +49,53 @@ export namespace Entry {
config?: any
disabled?: boolean
intercept?: Dict
isolate?: Dict<boolean | string>
when?: any
}
}

function swapAssign<T extends {}>(target: T, source?: T): T {
const result = { ...target }
for (const key in result) {
delete target[key]
}
Object.assign(target, source)
return result
}

export class Entry {
public fork: ForkScope | null = null
public isUpdate = false

constructor(public loader: Loader, public parent: Context, public options: Entry.Options) {}

amend(ctx: Context) {
for (const key of Reflect.ownKeys(ctx[Context.intercept])) {
delete ctx[Context.intercept][key]
swapAssign(ctx[Context.intercept], this.options.intercept)
const neoMap: Dict<symbol> = Object.create(Object.getPrototypeOf(ctx[Context.isolate]))
for (const [key, label] of Object.entries(this.options.isolate ?? {})) {
if (typeof label === 'string') {
neoMap[key] = (this.loader.realms[label] ??= Object.create(null))[key] ??= Symbol(key)
} else if (label) {
neoMap[key] = Symbol(key)
}
}
for (const key in { ...ctx[Context.isolate], ...neoMap }) {
if (neoMap[key] === ctx[Context.isolate][key]) continue
const self = Object.create(null)
self[Context.filter] = (ctx2: Context) => {
return ctx[Context.isolate][key] === ctx2[Context.isolate][key]
}
ctx.emit(self, 'internal/before-service', key)
}
const oldMap = swapAssign(ctx[Context.isolate], neoMap)
for (const key in { ...oldMap, ...ctx[Context.isolate] }) {
if (oldMap[key] === ctx[Context.isolate][key]) continue
const self = Object.create(null)
self[Context.filter] = (ctx2: Context) => {
return ctx[Context.isolate][key] === ctx2[Context.isolate][key]
}
ctx.emit(self, 'internal/service', key)
}
Object.assign(ctx[Context.intercept], this.options.intercept)
}

// TODO: handle parent change
Expand All @@ -85,9 +117,9 @@ export class Entry {
this.parent.emit('loader/entry', 'apply', this)
const plugin = await this.loader.resolve(this.options.name)
if (!plugin) return
const intercept = Object.create(this.parent[Context.intercept])
const ctx = this.parent.extend({
[Context.intercept]: intercept,
[Context.intercept]: Object.create(this.parent[Context.intercept]),
[Context.isolate]: Object.create(this.parent[Context.isolate]),
})
this.amend(ctx)
this.fork = ctx.plugin(plugin, this.loader.interpolate(this.options.config))
Expand Down Expand Up @@ -127,13 +159,15 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
public mimeType!: string
public filename!: string
public entries: Dict<Entry> = Object.create(null)
public realms: Dict<Dict<symbol>> = Object.create(null)

private tasks = new Set<Promise<any>>()

abstract import(name: string): Promise<any>

constructor(public app: Context, public options: T) {
super(app, 'loader', true)
this.realms.root = app.root[Context.isolate]
}

async init(filename?: string) {
Expand Down
99 changes: 97 additions & 2 deletions packages/loader/tests/index.spec.ts
@@ -1,12 +1,12 @@
import { describe, mock, test } from 'node:test'
import { expect } from 'chai'
import { Context } from '@cordisjs/core'
import { Context, Service } from '@cordisjs/core'
import { defineProperty } from 'cosmokit'
import MockLoader from './utils'

describe('@cordisjs/loader', () => {
const root = new Context()
root.plugin(MockLoader)
root.loader.writable = true

const foo = mock.fn((ctx: Context) => {
ctx.accept()
Expand Down Expand Up @@ -100,4 +100,99 @@ describe('@cordisjs/loader', () => {
name: 'qux',
}])
})

describe('service isolation', async () => {
const root = new Context()
root.plugin(MockLoader)

const dispose = mock.fn()
const foo = mock.fn((ctx: Context) => {
ctx.on('dispose', dispose)
})
defineProperty(foo, 'inject', ['bar'])
class Bar extends Service {
static [Service.provide] = 'bar'
static [Service.immediate] = true
}
class Qux extends Service {
static [Service.provide] = 'qux'
static [Service.immediate] = true
}
root.loader.register('foo', foo)
root.loader.register('bar', Bar)
root.loader.register('qux', Qux)

test('basic support', async () => {
root.loader.config = [{
id: '1',
name: 'bar',
}, {
id: '2',
name: 'qux',
}, {
id: '3',
name: 'group',
config: [{
id: '4',
name: 'foo',
}],
}]

await root.start()
expect(root.registry.get(foo)).to.be.ok
expect(root.registry.get(Bar)).to.be.ok
expect(root.registry.get(Qux)).to.be.ok
expect(foo.mock.calls).to.have.length(1)
})

test('isolate', async () => {
root.loader.config = [{
id: '1',
name: 'bar',
}, {
id: '2',
name: 'qux',
}, {
id: '3',
name: 'group',
isolate: {
bar: true,
},
config: [{
id: '4',
name: 'foo',
}],
}]

expect(dispose.mock.calls).to.have.length(0)
root.loader.entryFork.update(root.loader.config)
await new Promise((resolve) => setTimeout(resolve, 0))
expect(foo.mock.calls).to.have.length(1)
expect(dispose.mock.calls).to.have.length(1)

root.loader.config = [{
id: '1',
name: 'bar',
}, {
id: '2',
name: 'qux',
}, {
id: '3',
name: 'group',
isolate: {
bar: false,
qux: true,
},
config: [{
id: '4',
name: 'foo',
}],
}]

root.loader.entryFork.update(root.loader.config)
await new Promise((resolve) => setTimeout(resolve, 0))
expect(foo.mock.calls).to.have.length(2)
expect(dispose.mock.calls).to.have.length(1)
})
})
})
1 change: 1 addition & 0 deletions packages/loader/tests/utils.ts
Expand Up @@ -14,6 +14,7 @@ export default class MockLoader extends Loader {
constructor(ctx: Context) {
super(ctx, { name: 'cordis' })
this.register('group', group)
this.writable = true
}

register(name: string, plugin: any) {
Expand Down

0 comments on commit cf65001

Please sign in to comment.