Skip to content

Commit

Permalink
feat(loader): extract realm class, enhance realm access
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 30, 2024
1 parent 1641e2e commit f970d3c
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 48 deletions.
105 changes: 76 additions & 29 deletions packages/loader/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,68 @@ function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['
return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
}

export abstract class Realm {
protected store: Dict<symbol> = Object.create(null)

abstract get suffix(): string

access(key: string, create = false) {
if (create) {
return this.store[key] ??= Symbol(`${key}${this.suffix}`)
} else {
return this.store[key] ?? Symbol(`${key}${this.suffix}`)
}
}

delete(key: string) {
delete this.store[key]
}
}

export class LocalRealm extends Realm {
constructor(private entry: Entry) {
super()
}

get suffix() {
return '#' + this.entry.options.id
}
}

export class GlobalRealm extends Realm {
constructor(private loader: Loader, private label: string) {
super()
}

get suffix() {
return '@' + this.label
}

gc(key: string) {
// realm garbage collection
for (const entry of this.loader.entries()) {
// has reference to this realm
if (entry.options.isolate?.[key] === this.label) return
}
this.delete(key)
if (!Object.keys(this.store).length) {
delete this.loader.realms[this.suffix]
}
}
}

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

public fork?: ForkScope
public suspend = false
public parent!: EntryGroup
public options!: Entry.Options
public subgroup?: EntryGroup
public subtree?: EntryTree
public realm = new LocalRealm(this)

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

get id() {
let id = this.options.id
Expand Down Expand Up @@ -91,17 +143,28 @@ export class Entry {
_check() {
if (this.disabled) return false
for (const name of this.requiredDeps) {
let key = this.parent.ctx[Context.isolate][name]
let key: symbol | undefined = 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 (label) key = this.access(name, label)
if (!key || isNullable(this.parent.ctx[key])) return false
}
return true
}

access(key: string, label: string | true, create: true): symbol
access(key: string, label: string | true, create?: boolean): symbol | undefined
access(key: string, label: string | true, create = false) {
let realm: Realm | undefined
if (label === true) {
realm = this.realm
} else if (create) {
realm = this.loader.realms[label] ??= new GlobalRealm(this.loader, label)
} else {
realm = this.loader.realms[label]
}
return realm?.access(key, create)
}

async checkService(name: string) {
if (!this.requiredDeps.includes(name)) return
const ready = this._check()
Expand All @@ -112,21 +175,6 @@ export class Entry {
}
}

resolveRealm(label: string | true) {
if (label === true) {
return '#' + this.id
} else {
return '@' + label
}
}

hasIsolate(key: string, realm: string) {
if (!this.fork) return false
const label = this.options.isolate?.[key]
if (!label) return false
return realm === this.resolveRealm(label)
}

patch(options: Partial<Entry.Options> = {}) {
// step 1: prepare isolate map
const ctx = this.fork?.parent ?? this.parent.ctx.extend({
Expand All @@ -135,8 +183,7 @@ export class Entry {
})
const newMap: Dict<symbol> = Object.create(this.parent.ctx[Context.isolate])
for (const [key, label] of Object.entries(this.options.isolate ?? {})) {
const realm = this.resolveRealm(label)
newMap[key] = (this.loader.realms[realm] ??= Object.create(null))[key] ??= Symbol(`${key}${realm}`)
newMap[key] = this.access(key, label, true)
}

// step 2: generate service diff
Expand Down Expand Up @@ -240,9 +287,8 @@ export class Entry {
await this.stop()
} else if (this.fork) {
for (const [key, label] of Object.entries(legacy.isolate ?? {})) {
if (this.options.isolate?.[key] === label) continue
const name = this.resolveRealm(label)
this.loader._clearRealm(key, name)
if (this.options.isolate?.[key] === label || label === true) continue
this.loader.realms[label]?.gc(key)
}
this.patch(options)
} else {
Expand All @@ -266,11 +312,12 @@ export class Entry {
async stop() {
this.fork?.dispose()
this.fork = undefined
}

// realm garbage collection
dispose() {
for (const [key, label] of Object.entries(this.options.isolate ?? {})) {
const name = this.resolveRealm(label)
this.loader._clearRealm(key, name)
if (label === true) continue
this.loader.realms[label]?.gc(key)
}
}
}
7 changes: 5 additions & 2 deletions packages/loader/src/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Entry } from './entry.ts'
import { EntryTree } from './tree.ts'

export class EntryGroup {
static readonly key = Symbol.for('cordis.group')

public data: Entry.Options[] = []

constructor(public ctx: Context, public tree: EntryTree) {
Expand All @@ -12,7 +14,7 @@ export class EntryGroup {

async create(options: Omit<Entry.Options, 'id'>) {
const id = this.tree.ensureId(options)
const entry = this.tree.store[id] ??= new Entry(this.ctx.loader, this)
const entry = this.tree.store[id] ??= new Entry(this.ctx.loader)
// Entry may be moved from another group,
// so we need to update the parent reference.
entry.parent = this
Expand All @@ -30,6 +32,7 @@ export class EntryGroup {
const entry = this.tree.store[id]
if (!entry) return
entry.stop()
entry.dispose()
this.unlink(entry.options)
delete this.tree.store[id]
}
Expand Down Expand Up @@ -60,9 +63,9 @@ export class EntryGroup {
}

export class Group extends EntryGroup {
static key = Symbol('cordis.group')
static reusable = true
static initial: Omit<Entry.Options, 'id'>[] = []
static readonly [EntryGroup.key] = true

// TODO support options
constructor(public ctx: Context) {
Expand Down
23 changes: 6 additions & 17 deletions packages/loader/src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context, EffectScope } from '@cordisjs/core'
import { Dict, isNullable } from 'cosmokit'
import { ModuleLoader } from './internal.ts'
import { Entry } from './entry.ts'
import { Entry, GlobalRealm } from './entry.ts'
import { ImportTree, LoaderFile } from './file.ts'

export * from './entry.ts'
Expand Down Expand Up @@ -57,15 +57,14 @@ export abstract class Loader extends ImportTree {
}

public files: Dict<LoaderFile> = Object.create(null)
public realms: Dict<Dict<symbol>> = Object.create(null)
public realms: Dict<GlobalRealm> = Object.create(null)
public delims: Dict<symbol> = Object.create(null)
public internal?: ModuleLoader

constructor(public ctx: Context, public config: Loader.Config) {
super(ctx)

this.ctx.set('loader', this)
this.realms['#'] = ctx.root[Context.isolate]

this.ctx.on('internal/update', (fork) => {
if (!fork.entry) return
Expand Down Expand Up @@ -96,18 +95,18 @@ export abstract class Loader extends ImportTree {
// case 2: fork is not tracked by loader
if (!fork.entry) return

// case 3: fork is disposed outside of plugin
// case 3: fork is disposed on behalf of plugin deletion (such as plugin hmr)
// self-dispose: ctx.scope.dispose() -> fork / runtime dispose -> delete(plugin)
// hmr: delete(plugin) -> runtime dispose -> fork dispose
// 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
// case 4: fork is disposed by loader behavior
// such as inject checker, config file update, ancestor group disable
if (!fork.entry._check()) return

fork.parent.emit('loader/entry', 'unload', fork.entry)
fork.entry.options.disabled = true
fork.entry.fork = undefined
fork.entry.stop()
fork.entry.parent.tree.write()
})

Expand Down Expand Up @@ -169,16 +168,6 @@ export abstract class Loader extends ImportTree {
if (!exports.__esModule) return exports
return exports.default ?? exports
}

_clearRealm(key: string, realm: string) {
for (const entry of this.entries()) {
if (entry.hasIsolate(key, realm)) return
}
delete this.realms[realm][key]
if (!Object.keys(this.realms[realm]).length) {
delete this.realms[realm]
}
}
}

export default Loader
1 change: 1 addition & 0 deletions packages/loader/src/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { EntryGroup } from './group.ts'

export abstract class EntryTree {
static readonly sep = ':'
static readonly [EntryGroup.key] = true

public url!: string
public root: EntryGroup
Expand Down

0 comments on commit f970d3c

Please sign in to comment.