Skip to content

Commit

Permalink
feat: support experimental loader.wait()
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Nov 9, 2024
1 parent f37d78b commit c5abc3e
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 54 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ export class EffectScope<C extends Context = Context> {
})
}

async wait() {
while (this.pending) {
await this.pending
}
if (this._error) throw this._error
}

async restart() {
this.setActive(false)
this.setActive(true)
Expand Down
53 changes: 27 additions & 26 deletions packages/loader/src/config/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export class Entry<C extends Context = Context> {
public subgroup?: EntryGroup
public subtree?: EntryTree<C>

_initTask?: Promise<void>

constructor(public loader: Loader<C>) {
this.ctx = loader.ctx.extend({ [Entry.key]: this })
this.context.emit('loader/entry-init', this)
Expand Down Expand Up @@ -71,11 +73,6 @@ export class Entry<C extends Context = Context> {
return false
}

_check() {
if (this.disabled) return false
return !this.parent.ctx.bail('loader/entry-check', this)
}

evaluate(expr: string) {
return evaluate(this.ctx, expr)
}
Expand Down Expand Up @@ -110,13 +107,14 @@ export class Entry<C extends Context = Context> {
this.context.emit(meta, 'loader/after-patch', this)
}

check() {
return !this.disabled && !this.parent.ctx.bail('loader/entry-check', this)
}

async refresh() {
const ready = this._check()
if (ready && !this.scope) {
await this.start()
} else if (!ready && this.scope) {
await this.stop()
}
if (this.scope) return
if (!this.check()) return
await (this._initTask ??= this._init())
}

async update(options: Partial<EntryOptions>, override = false) {
Expand All @@ -137,31 +135,34 @@ export class Entry<C extends Context = Context> {
sortKeys(this.options)

// step 2: execute
if (!this._check()) {
await this.stop()
} else if (this.scope) {
// this._check() is only a init-time optimization
if (this.disabled) {
this.scope?.dispose()
return
}

if (this.scope?.uid) {
this.context.emit('loader/partial-dispose', this, legacy, true)
this.patch(options)
} else {
await this.start()
// FIXME: lock init task
await (this._initTask = this._init())
}
}

async start() {
const exports = await this.parent.tree.import(this.options.name).catch((error: any) => {
private async _init() {
let exports: any
try {
exports = await this.parent.tree.import(this.options.name)
} catch (error) {
this.context.emit(this.ctx, 'internal/error', new Error(`Cannot find package "${this.options.name}"`))
this.context.emit(this.ctx, 'internal/error', error)
})
if (!exports) return
return
}
const plugin = this.loader.unwrapExports(exports)
this.patch()
this.ctx[Entry.key] = this
this.scope = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin))
this.scope = this.ctx.plugin(plugin, this._resolveConfig(plugin))
this.context.emit('loader/entry-scope', this, 'apply')
}

async stop() {
this.scope?.dispose()
this.scope = undefined
this._initTask = undefined
}
}
20 changes: 12 additions & 8 deletions packages/loader/src/config/group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context } from '@cordisjs/core'
import { Context, Service } from '@cordisjs/core'
import { Entry, EntryOptions } from './entry.ts'
import { EntryTree } from './tree.ts'

Expand Down Expand Up @@ -31,28 +31,29 @@ export class EntryGroup {
remove(id: string) {
const entry = this.tree.store[id]
if (!entry) return
entry.stop()
entry.scope?.dispose()
this.unlink(entry.options)
delete this.tree.store[id]
this.ctx.emit('loader/partial-dispose', entry, entry.options, false)
}

update(config: EntryOptions[]) {
async update(config: EntryOptions[]) {
const oldConfig = this.data as EntryOptions[]
this.data = config
const oldMap = Object.fromEntries(oldConfig.map(options => [options.id, options]))
const newMap = Object.fromEntries(config.map(options => [options.id ?? Symbol('anonymous'), options]))

// update inner plugins
for (const id of Reflect.ownKeys({ ...oldMap, ...newMap }) as string[]) {
const ids = Reflect.ownKeys({ ...oldMap, ...newMap }) as string[]
await Promise.all(ids.map(async (id) => {
if (newMap[id]) {
this.create(newMap[id]).catch((error) => {
await this.create(newMap[id]).catch((error) => {
this.ctx.emit(this.ctx, 'internal/error', error)
})
} else {
this.remove(id)
}
}
}))
}

stop() {
Expand All @@ -66,13 +67,16 @@ export class Group extends EntryGroup {
static initial: Omit<EntryOptions, 'id'>[] = []
static readonly [EntryGroup.key] = true

constructor(public ctx: Context, config: EntryOptions[]) {
constructor(public ctx: Context, public config: EntryOptions[]) {
super(ctx, ctx.scope.entry!.parent.tree)
ctx.on('dispose', () => this.stop())
ctx.on('internal/update', (_, config) => {
this.update(config)
return true
})
this.update(config)
}

async [Service.setup]() {
await this.update(this.config)
}
}
15 changes: 8 additions & 7 deletions packages/loader/src/config/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,10 @@ export class ImportTree<C extends Context = Context> extends EntryTree<C> {
ctx.on('dispose', () => this.stop())
}

async [Service.setup]() {
await this.refresh()
async start() {
const data = await this.file.read()
await this.file.checkAccess()
}

async refresh() {
this.root.update(await this.file.read())
await this.root.update(data)
}

stop() {
Expand Down Expand Up @@ -80,6 +77,10 @@ export class ImportTree<C extends Context = Context> extends EntryTree<C> {
}
throw new Error('config file not found')
}

async [Service.setup]() {
await this.start()
}
}

export namespace Import {
Expand All @@ -102,6 +103,6 @@ export class Import extends ImportTree {
}
this.file = new LoaderFile(filename, LoaderFile.writable[ext])
this.file.ref(this)
await super[Service.setup]()
await super.start()
}
}
15 changes: 13 additions & 2 deletions packages/loader/src/config/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export abstract class EntryTree<C extends Context = Context> {
}
}

async wait() {
while (1) {
await new Promise(resolve => setTimeout(resolve, 100))
const pendings = [...this.entries()]
.map(entry => entry._initTask || entry.scope?.pending!)
.filter(Boolean)
if (!pendings.length) return
await Promise.all(pendings)
}
}

ensureId(options: Partial<EntryOptions>) {
if (!options.id) {
do {
Expand Down Expand Up @@ -86,9 +97,9 @@ export abstract class EntryTree<C extends Context = Context> {

async import(name: string) {
if (this.ctx.loader.internal) {
return this.ctx.loader.internal.import(name, this.url, {})
return await this.ctx.loader.internal.import(name, this.url, {})
} else {
return import(name)
return await import(name)
}
}

Expand Down
3 changes: 1 addition & 2 deletions packages/loader/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,11 @@ export abstract class Loader<C extends Context = Context> extends ImportTree<C>
// plugin hmr: delete(plugin) -> runtime dispose -> scope dispose
if (!ctx.registry.has(scope.runtime?.plugin!)) return

scope.entry.scope = undefined
scope.parent.emit('loader/entry-scope', scope.entry, 'unload')

// case 4: scope is disposed by loader behavior
// such as inject checker, config file update, ancestor group disable
if (!scope.entry._check()) return
if (!scope.entry.check()) return

scope.entry.options.disabled = true
scope.entry.parent.tree.write()
Expand Down
2 changes: 1 addition & 1 deletion packages/loader/tests/group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect } from 'chai'
import { Context } from '@cordisjs/core'
import MockLoader from './utils'

describe('group management: basic support', () => {
describe.only('group management: basic support', () => {
const root = new Context()
root.plugin(MockLoader)
const loader = root.loader as unknown as MockLoader
Expand Down
6 changes: 2 additions & 4 deletions packages/loader/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('loader: basic support', () => {
before(() => loader.start())

it('loader initiate', async () => {
loader.file.write([{
await loader.read([{
id: '1',
name: 'foo',
}, {
Expand All @@ -32,7 +32,6 @@ describe('loader: basic support', () => {
disabled: true,
}],
}])
await loader.start()

loader.expectEnable(foo)
loader.expectEnable(bar)
Expand All @@ -45,14 +44,13 @@ describe('loader: basic support', () => {
it('loader update', async () => {
foo.mock.resetCalls()
bar.mock.resetCalls()
loader.file.write([{
await loader.read([{
id: '1',
name: 'foo',
}, {
id: '4',
name: 'qux',
}])
await loader.start()

loader.expectEnable(foo)
loader.expectDisable(bar)
Expand Down
13 changes: 9 additions & 4 deletions packages/loader/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,19 @@ export default class MockLoader<C extends Context = Context> extends Loader<C> {

constructor(ctx: C) {
super(ctx, { name: 'cordis' })
this.file = new MockLoaderFile('config-1.yml')
this.file.ref(this)
this.mock('cordis/group', Group)
}

async start() {
await this.refresh()
await new Promise((resolve) => setTimeout(resolve, 0))
this.file = new MockLoaderFile('config-1.yml')
this.file.ref(this)
await super.start()
}

async read(data: any) {
this.file.write(data)
await this.root.update(data)
await this.wait()
}

async import(name: string) {
Expand Down

0 comments on commit c5abc3e

Please sign in to comment.