Skip to content

Commit

Permalink
feat(loader): use entry group for loader root
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 23, 2024
1 parent 92df648 commit 94bd84d
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 68 deletions.
25 changes: 13 additions & 12 deletions packages/loader/src/entry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Context, ForkScope, Inject } from '@cordisjs/core'
import { Dict } from 'cosmokit'
import Loader from './shared.ts'
import { EntryGroup } from './group.ts'

export namespace Entry {
export interface Options {
Expand Down Expand Up @@ -42,16 +43,17 @@ function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['
}

export class Entry {
static key = Symbol('cordis.entry')

public fork?: ForkScope
public isUpdate = false
public parent!: Context
public options!: Entry.Options
public group: Entry.Options[] | null = null
public children?: EntryGroup

constructor(public loader: Loader) {}
constructor(public loader: Loader, public parent: EntryGroup) {}

unlink() {
const config = this.parent.config as Entry.Options[]
const config = this.parent.config
const index = config.indexOf(this.options)
if (index >= 0) config.splice(index, 1)
}
Expand Down Expand Up @@ -86,7 +88,7 @@ export class Entry {
if (!(value instanceof Object)) continue
const source = Reflect.getOwnPropertyDescriptor(value, Context.origin)?.value
if (!source) {
this.parent.emit('internal/warning', new Error(`expected service ${key} to be implemented`))
ctx.emit('internal/warning', new Error(`expected service ${key} to be implemented`))
continue
}
diff.push([key, oldMap[key], newMap[key], ctx[delim], source[delim]])
Expand Down Expand Up @@ -142,15 +144,14 @@ export class Entry {
}

createContext() {
return this.parent.extend({
[Context.intercept]: Object.create(this.parent[Context.intercept]),
[Context.isolate]: Object.create(this.parent[Context.isolate]),
return this.parent.ctx.extend({
[Context.intercept]: Object.create(this.parent.ctx[Context.intercept]),
[Context.isolate]: Object.create(this.parent.ctx[Context.isolate]),
})
}

async update(parent: Context, options: Entry.Options) {
async update(options: Entry.Options) {
const legacy = this.options
this.parent = parent
this.options = sortKeys(options)
if (!this.loader.isTruthyLike(options.when) || options.disabled) {
this.stop()
Expand All @@ -167,9 +168,9 @@ export class Entry {
if (!plugin) return
const ctx = this.createContext()
this.patch(ctx)
ctx[Entry.key] = this
this.fork = ctx.plugin(plugin, this.options.config)
this.fork.entry = this
this.parent.emit('loader/entry', 'apply', this)
ctx.emit('loader/entry', 'apply', this)
}
}

Expand Down
39 changes: 29 additions & 10 deletions packages/loader/src/group.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { Context } from '@cordisjs/core'
import { Entry } from './entry.ts'
import Loader from './shared.ts'

export class EntryGroup {
static inject = ['loader']

public config: Entry.Options[] = []

constructor(public loader: Loader, public ctx: Context) {
ctx.on('dispose', () => {
for (const options of this.config) {
this.loader._remove(options.id)
}
})
constructor(public ctx: Context) {}

async _create(options: Omit<Entry.Options, 'id'>) {
const id = this.ctx.loader.ensureId(options)
const entry = this.ctx.loader.entries[id] ??= new Entry(this.ctx.loader, this)
entry.parent = this
await entry.update(options as Entry.Options)
return id
}

_remove(id: string) {
const entry = this.ctx.loader.entries[id]
if (!entry) return
entry.stop()
entry.unlink()
delete this.ctx.loader.entries[id]
}

update(config: Entry.Options[]) {
Expand All @@ -21,11 +32,19 @@ export class EntryGroup {

// update inner plugins
for (const id of Reflect.ownKeys({ ...oldMap, ...newMap }) as string[]) {
if (!newMap[id]) {
this.loader._remove(id)
if (newMap[id]) {
this._create(newMap[id]).catch((error) => {
this.ctx.emit('internal/error', error)
})
} else {
this.loader._ensure(this.ctx, newMap[id])
this._remove(id)
}
}
}

dispose() {
for (const options of this.config) {
this._remove(options.id)
}
}
}
80 changes: 36 additions & 44 deletions packages/loader/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export namespace Loader {
}

export abstract class Loader<T extends Loader.Options = Loader.Options> extends Service<Entry.Options[]> {
static inject = {
optional: ['loader'],
}

// process
public baseDir = process.cwd()
public envData = process.env.CORDIS_SHARED
Expand All @@ -66,7 +70,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
env: process.env,
}

public root: Entry
public root: EntryGroup
public suspend = false
public writable = false
public mimeType!: string
Expand All @@ -83,8 +87,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends

constructor(public app: Context, public options: T) {
super(app, 'loader', true)
this.root = new Entry(this)
this.entries[''] = this.root
this.root = new EntryGroup(this.app)
this.realms['#'] = app.root[Context.isolate]

this.app.on('dispose', () => {
Expand All @@ -105,6 +108,10 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
})

this.app.on('internal/fork', (fork) => {
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
Expand Down Expand Up @@ -242,7 +249,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
return !!this.interpolate(`\${{ ${expr} }}`)
}

private ensureId(options: Partial<Entry.Options>) {
ensureId(options: Partial<Entry.Options>) {
if (!options.id) {
do {
options.id = Math.random().toString(36).slice(2, 8)
Expand All @@ -251,13 +258,6 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
return options.id!
}

async _ensure(parent: Context, options: Omit<Entry.Options, 'id'>) {
const id = this.ensureId(options)
const entry = this.entries[id] ??= new Entry(this)
await entry.update(parent, options as Entry.Options)
return id
}

async update(id: string, options: Partial<Omit<Entry.Options, 'id' | 'name'>>) {
const entry = this.entries[id]
if (!entry) throw new Error(`entry ${id} not found`)
Expand All @@ -270,23 +270,20 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
}
}
this.writeConfig()
return entry.update(entry.parent, override)
return entry.update(override)
}

async create(options: Omit<Entry.Options, 'id'>, target = '', index = Infinity) {
const targetEntry = this.entries[target]
if (!targetEntry?.fork) throw new Error(`entry ${target} not found`)
targetEntry.options.config.splice(index, 0, options)
this.writeConfig()
return this._ensure(targetEntry.fork.ctx, options)
resolveGroup(id: string | null) {
const group = id ? this.entries[id]?.children : this.root
if (!group) throw new Error(`entry ${id} not found`)
return group
}

_remove(id: string) {
const entry = this.entries[id]
if (!entry) return
entry.stop()
entry.unlink()
delete this.entries[id]
async create(options: Omit<Entry.Options, 'id'>, parent: string | null = null, position = Infinity) {
const group = this.resolveGroup(parent)
group.config.splice(position, 0, options as Entry.Options)
this.writeConfig()
return group._create(options)
}

remove(id: string) {
Expand All @@ -298,17 +295,16 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
this.writeConfig()
}

transfer(id: string, target: string, index = Infinity) {
transfer(id: string, parent: string | null, position = Infinity) {
const entry = this.entries[id]
if (!entry) throw new Error(`entry ${id} not found`)
const sourceEntry = entry.parent.scope.entry!
const targetEntry = this.entries[target]
if (!targetEntry?.fork) throw new Error(`entry ${target} not found`)
const source = entry.parent
const target = this.resolveGroup(parent)
entry.unlink()
targetEntry.options.config.splice(index, 0, entry.options)
target.config.splice(position, 0, entry.options)
this.writeConfig()
if (sourceEntry === targetEntry) return
entry.parent = targetEntry.fork.ctx
if (source === target) return
entry.parent = target
if (!entry.fork) return
const ctx = entry.createContext()
entry.patch(entry.fork.parent, ctx)
Expand All @@ -331,19 +327,9 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
return this._locate(scope.parent.scope)
}

createGroup() {
const ctx = this[Context.current]
// if (!ctx.scope.entry) throw new Error(`expected entry scope`)
return new EntryGroup(this, ctx)
}

async start() {
await this.readConfig()
this.root.update(this.app, {
id: '',
name: 'cordis/group',
config: this.config,
})
this.root.update(this.config)

while (this.tasks.size) {
await Promise.all(this.tasks)
Expand Down Expand Up @@ -387,13 +373,19 @@ export interface GroupOptions {
export function createGroup(config?: Entry.Options[], options: GroupOptions = {}) {
options.initial = config

function group(ctx: Context, config: Entry.Options[]) {
const group = ctx.get('loader')!.createGroup()
function group(ctx: Context) {
if (!ctx.scope.entry) throw new Error(`expected entry scope`)
const group = new EntryGroup(ctx)
ctx.scope.entry.children = group
ctx.on('dispose', () => {
group.dispose()
})
ctx.accept((config: Entry.Options[]) => {
group.update(config)
}, { passive: true, immediate: true })
}

defineProperty(group, 'inject', ['loader'])
defineProperty(group, 'reusable', true)
defineProperty(group, kGroup, options)
if (options.name) defineProperty(group, 'name', options.name)
Expand Down
4 changes: 2 additions & 2 deletions packages/loader/tests/isolate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,15 +530,15 @@ describe('service isolation: transfer', () => {
})

it('transfer injector out of group', async () => {
loader.transfer(injector, '')
loader.transfer(injector, null)

await new Promise((resolve) => setTimeout(resolve, 0))
expect(foo.mock.calls).to.have.length(0)
expect(dispose.mock.calls).to.have.length(1)
})

it('transfer provider out of group', async () => {
loader.transfer(provider, '')
loader.transfer(provider, null)

await new Promise((resolve) => setTimeout(resolve, 0))
expect(foo.mock.calls).to.have.length(1)
Expand Down

0 comments on commit 94bd84d

Please sign in to comment.