Skip to content

Commit 94bd84d

Browse files
committed
feat(loader): use entry group for loader root
1 parent 92df648 commit 94bd84d

File tree

4 files changed

+80
-68
lines changed

4 files changed

+80
-68
lines changed

packages/loader/src/entry.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Context, ForkScope, Inject } from '@cordisjs/core'
22
import { Dict } from 'cosmokit'
33
import Loader from './shared.ts'
4+
import { EntryGroup } from './group.ts'
45

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

4445
export class Entry {
46+
static key = Symbol('cordis.entry')
47+
4548
public fork?: ForkScope
4649
public isUpdate = false
47-
public parent!: Context
4850
public options!: Entry.Options
49-
public group: Entry.Options[] | null = null
51+
public children?: EntryGroup
5052

51-
constructor(public loader: Loader) {}
53+
constructor(public loader: Loader, public parent: EntryGroup) {}
5254

5355
unlink() {
54-
const config = this.parent.config as Entry.Options[]
56+
const config = this.parent.config
5557
const index = config.indexOf(this.options)
5658
if (index >= 0) config.splice(index, 1)
5759
}
@@ -86,7 +88,7 @@ export class Entry {
8688
if (!(value instanceof Object)) continue
8789
const source = Reflect.getOwnPropertyDescriptor(value, Context.origin)?.value
8890
if (!source) {
89-
this.parent.emit('internal/warning', new Error(`expected service ${key} to be implemented`))
91+
ctx.emit('internal/warning', new Error(`expected service ${key} to be implemented`))
9092
continue
9193
}
9294
diff.push([key, oldMap[key], newMap[key], ctx[delim], source[delim]])
@@ -142,15 +144,14 @@ export class Entry {
142144
}
143145

144146
createContext() {
145-
return this.parent.extend({
146-
[Context.intercept]: Object.create(this.parent[Context.intercept]),
147-
[Context.isolate]: Object.create(this.parent[Context.isolate]),
147+
return this.parent.ctx.extend({
148+
[Context.intercept]: Object.create(this.parent.ctx[Context.intercept]),
149+
[Context.isolate]: Object.create(this.parent.ctx[Context.isolate]),
148150
})
149151
}
150152

151-
async update(parent: Context, options: Entry.Options) {
153+
async update(options: Entry.Options) {
152154
const legacy = this.options
153-
this.parent = parent
154155
this.options = sortKeys(options)
155156
if (!this.loader.isTruthyLike(options.when) || options.disabled) {
156157
this.stop()
@@ -167,9 +168,9 @@ export class Entry {
167168
if (!plugin) return
168169
const ctx = this.createContext()
169170
this.patch(ctx)
171+
ctx[Entry.key] = this
170172
this.fork = ctx.plugin(plugin, this.options.config)
171-
this.fork.entry = this
172-
this.parent.emit('loader/entry', 'apply', this)
173+
ctx.emit('loader/entry', 'apply', this)
173174
}
174175
}
175176

packages/loader/src/group.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import { Context } from '@cordisjs/core'
22
import { Entry } from './entry.ts'
3-
import Loader from './shared.ts'
43

54
export class EntryGroup {
5+
static inject = ['loader']
6+
67
public config: Entry.Options[] = []
78

8-
constructor(public loader: Loader, public ctx: Context) {
9-
ctx.on('dispose', () => {
10-
for (const options of this.config) {
11-
this.loader._remove(options.id)
12-
}
13-
})
9+
constructor(public ctx: Context) {}
10+
11+
async _create(options: Omit<Entry.Options, 'id'>) {
12+
const id = this.ctx.loader.ensureId(options)
13+
const entry = this.ctx.loader.entries[id] ??= new Entry(this.ctx.loader, this)
14+
entry.parent = this
15+
await entry.update(options as Entry.Options)
16+
return id
17+
}
18+
19+
_remove(id: string) {
20+
const entry = this.ctx.loader.entries[id]
21+
if (!entry) return
22+
entry.stop()
23+
entry.unlink()
24+
delete this.ctx.loader.entries[id]
1425
}
1526

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

2233
// update inner plugins
2334
for (const id of Reflect.ownKeys({ ...oldMap, ...newMap }) as string[]) {
24-
if (!newMap[id]) {
25-
this.loader._remove(id)
35+
if (newMap[id]) {
36+
this._create(newMap[id]).catch((error) => {
37+
this.ctx.emit('internal/error', error)
38+
})
2639
} else {
27-
this.loader._ensure(this.ctx, newMap[id])
40+
this._remove(id)
2841
}
2942
}
3043
}
44+
45+
dispose() {
46+
for (const options of this.config) {
47+
this._remove(options.id)
48+
}
49+
}
3150
}

packages/loader/src/shared.ts

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export namespace Loader {
5656
}
5757

5858
export abstract class Loader<T extends Loader.Options = Loader.Options> extends Service<Entry.Options[]> {
59+
static inject = {
60+
optional: ['loader'],
61+
}
62+
5963
// process
6064
public baseDir = process.cwd()
6165
public envData = process.env.CORDIS_SHARED
@@ -66,7 +70,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
6670
env: process.env,
6771
}
6872

69-
public root: Entry
73+
public root: EntryGroup
7074
public suspend = false
7175
public writable = false
7276
public mimeType!: string
@@ -83,8 +87,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
8387

8488
constructor(public app: Context, public options: T) {
8589
super(app, 'loader', true)
86-
this.root = new Entry(this)
87-
this.entries[''] = this.root
90+
this.root = new EntryGroup(this.app)
8891
this.realms['#'] = app.root[Context.isolate]
8992

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

107110
this.app.on('internal/fork', (fork) => {
111+
if (fork.parent[Entry.key]) {
112+
fork.entry = fork.parent[Entry.key]
113+
delete fork.parent[Entry.key]
114+
}
108115
// fork.uid: fork is created (we only care about fork dispose event)
109116
// fork.parent.runtime.plugin !== group: fork is not tracked by loader
110117
if (fork.uid || !fork.entry) return
@@ -242,7 +249,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
242249
return !!this.interpolate(`\${{ ${expr} }}`)
243250
}
244251

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

254-
async _ensure(parent: Context, options: Omit<Entry.Options, 'id'>) {
255-
const id = this.ensureId(options)
256-
const entry = this.entries[id] ??= new Entry(this)
257-
await entry.update(parent, options as Entry.Options)
258-
return id
259-
}
260-
261261
async update(id: string, options: Partial<Omit<Entry.Options, 'id' | 'name'>>) {
262262
const entry = this.entries[id]
263263
if (!entry) throw new Error(`entry ${id} not found`)
@@ -270,23 +270,20 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
270270
}
271271
}
272272
this.writeConfig()
273-
return entry.update(entry.parent, override)
273+
return entry.update(override)
274274
}
275275

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

284-
_remove(id: string) {
285-
const entry = this.entries[id]
286-
if (!entry) return
287-
entry.stop()
288-
entry.unlink()
289-
delete this.entries[id]
282+
async create(options: Omit<Entry.Options, 'id'>, parent: string | null = null, position = Infinity) {
283+
const group = this.resolveGroup(parent)
284+
group.config.splice(position, 0, options as Entry.Options)
285+
this.writeConfig()
286+
return group._create(options)
290287
}
291288

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

301-
transfer(id: string, target: string, index = Infinity) {
298+
transfer(id: string, parent: string | null, position = Infinity) {
302299
const entry = this.entries[id]
303300
if (!entry) throw new Error(`entry ${id} not found`)
304-
const sourceEntry = entry.parent.scope.entry!
305-
const targetEntry = this.entries[target]
306-
if (!targetEntry?.fork) throw new Error(`entry ${target} not found`)
301+
const source = entry.parent
302+
const target = this.resolveGroup(parent)
307303
entry.unlink()
308-
targetEntry.options.config.splice(index, 0, entry.options)
304+
target.config.splice(position, 0, entry.options)
309305
this.writeConfig()
310-
if (sourceEntry === targetEntry) return
311-
entry.parent = targetEntry.fork.ctx
306+
if (source === target) return
307+
entry.parent = target
312308
if (!entry.fork) return
313309
const ctx = entry.createContext()
314310
entry.patch(entry.fork.parent, ctx)
@@ -331,19 +327,9 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
331327
return this._locate(scope.parent.scope)
332328
}
333329

334-
createGroup() {
335-
const ctx = this[Context.current]
336-
// if (!ctx.scope.entry) throw new Error(`expected entry scope`)
337-
return new EntryGroup(this, ctx)
338-
}
339-
340330
async start() {
341331
await this.readConfig()
342-
this.root.update(this.app, {
343-
id: '',
344-
name: 'cordis/group',
345-
config: this.config,
346-
})
332+
this.root.update(this.config)
347333

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

390-
function group(ctx: Context, config: Entry.Options[]) {
391-
const group = ctx.get('loader')!.createGroup()
376+
function group(ctx: Context) {
377+
if (!ctx.scope.entry) throw new Error(`expected entry scope`)
378+
const group = new EntryGroup(ctx)
379+
ctx.scope.entry.children = group
380+
ctx.on('dispose', () => {
381+
group.dispose()
382+
})
392383
ctx.accept((config: Entry.Options[]) => {
393384
group.update(config)
394385
}, { passive: true, immediate: true })
395386
}
396387

388+
defineProperty(group, 'inject', ['loader'])
397389
defineProperty(group, 'reusable', true)
398390
defineProperty(group, kGroup, options)
399391
if (options.name) defineProperty(group, 'name', options.name)

packages/loader/tests/isolate.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,15 +530,15 @@ describe('service isolation: transfer', () => {
530530
})
531531

532532
it('transfer injector out of group', async () => {
533-
loader.transfer(injector, '')
533+
loader.transfer(injector, null)
534534

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

540540
it('transfer provider out of group', async () => {
541-
loader.transfer(provider, '')
541+
loader.transfer(provider, null)
542542

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

0 commit comments

Comments
 (0)