Skip to content

Commit

Permalink
feat: new runtime api supports hmr
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Nov 10, 2024
1 parent 4d2fa2d commit eff476d
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 29 deletions.
4 changes: 3 additions & 1 deletion packages/core/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ class Registry<C extends Context = Context> {
const runtime = key && this._internal.get(key)
if (!runtime) return
this._internal.delete(key)
runtime.scopes.popAll().forEach(scope => scope.dispose())
for (const scope of runtime.scopes) {
scope.dispose()
}
return runtime
}

Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,17 @@ export class EffectScope<C extends Context = Context> {
this.uid = parent.registry.counter
this.ctx = this.context = parent.extend({ scope: this })
this.dispose = parent.scope.effect(() => {
const remove = this.runtime?.scopes.push(this)
const remove = runtime!.scopes.push(this)
this.context.emit('internal/plugin', this)
this.setActive(true)
return async () => {
remove?.()
this.uid = null
this.context.emit('internal/plugin', this)
if (this.runtime && !this.runtime.scopes.length) {
this.ctx.registry.delete(this.runtime.plugin)
if (this.ctx.registry.has(runtime!.plugin)) {
remove()
if (!runtime!.scopes.length) {
this.ctx.registry.delete(runtime!.plugin)
}
}
this.setActive(false)
await this._pending
Expand Down Expand Up @@ -165,7 +167,7 @@ export class EffectScope<C extends Context = Context> {
}

private async _unload() {
await Promise.all(this.disposables.popAll().map(async (dispose) => {
await Promise.all(this.disposables.clear().map(async (dispose) => {
try {
await dispose()
} catch (reason) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class DisposableList<T> {
this.map.delete(this.sn)
}

popAll() {
clear() {
const values = [...this.map.values()]
this.map.clear()
return values.reverse()
Expand Down
45 changes: 24 additions & 21 deletions packages/hmr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function loadDependencies(job: ModuleJob, ignored = new Set<string>()) {

interface Reload {
filename: string
children: EffectScope[]
runtime?: Plugin.Runtime
}

class Watcher extends Service {
Expand Down Expand Up @@ -95,26 +95,30 @@ class Watcher extends Service {
// files independent from any plugins will trigger a full reload
const mainJob = await loader.internal!.getModuleJob('cordis/worker', import.meta.url, {})!
this.externals = await loadDependencies(mainJob)
const triggerLocalReload = this.ctx.debounce(() => this.triggerLocalReload(), this.config.debounce)
const partialReload = this.ctx.debounce(() => this.partialReload(), this.config.debounce)

this.watcher.on('change', async (path) => {
this.ctx.logger.debug('change detected:', path)
const url = pathToFileURL(resolve(this.base, path)).href

// full reload
if (this.externals.has(url)) return loader.exit()

// partial reload
if (loader.internal!.loadCache.has(url)) {
this.stashed.add(url)
return triggerLocalReload()
return partialReload()
}

// config reload
const file = this.ctx.loader.files[url]
if (!file) return
if (file.suspend) {
file.suspend = false
return
}
for (const tree of file.trees) {
tree.refresh()
tree.start()
}
})
}
Expand Down Expand Up @@ -191,11 +195,11 @@ class Watcher extends Service {
}
}

private async triggerLocalReload() {
private async partialReload() {
await this.analyzeChanges()

/** plugins pending classification */
const pending = new Map<ModuleJob, [Plugin, Plugin.Runtime | undefined]>()
const pending = new Map<ModuleJob, Plugin>()

/** plugins that should be reloaded */
const reloads = new Map<Plugin, Reload>()
Expand All @@ -213,17 +217,16 @@ class Watcher extends Service {
if (this.declined.has(url)) continue
const job = this.internal.loadCache.get(url)
const plugin = this.ctx.loader.unwrapExports(job?.module?.getNamespace())
const runtime = this.ctx.registry.get(plugin)
if (!job || !plugin) continue
pending.set(job, [plugin, runtime])
pending.set(job, plugin)
this.declined.add(url)
} catch (err) {
this.ctx.logger.warn(err)
}
}
}

for (const [job, [plugin, runtime]] of pending) {
for (const [job, plugin] of pending) {
// check if it is a dependent of the changed file
this.declined.delete(job.url)
const dependencies = [...await loadDependencies(job, this.declined)]
Expand All @@ -235,11 +238,10 @@ class Watcher extends Service {
dependencies.forEach(dep => this.accepted.add(dep))

// prepare for reload
if (runtime) {
reloads.set(plugin, { filename: job.url, children: runtime.scopes })
} else {
reloads.set(plugin, { filename: job.url, children: [] })
}
reloads.set(plugin, {
filename: job.url,
runtime: this.ctx.registry.get(plugin),
})
}

// save cache for rollback
Expand Down Expand Up @@ -269,16 +271,17 @@ class Watcher extends Service {
return rollback()
}

const reload = (plugin: any, children: EffectScope[]) => {
for (const oldFiber of children) {
const reload = (plugin: any, runtime?: Plugin.Runtime) => {
if (!runtime) return
for (const oldFiber of runtime.scopes) {
const scope = oldFiber.parent.plugin(plugin, oldFiber.config)
scope.entry = oldFiber.entry
if (scope.entry) scope.entry.fork = scope
if (scope.entry) scope.entry.scope = scope
}
}

try {
for (const [plugin, { filename, children }] of reloads) {
for (const [plugin, { filename, runtime }] of reloads) {
const path = this.relative(fileURLToPath(filename))

try {
Expand All @@ -289,7 +292,7 @@ class Watcher extends Service {
}

try {
reload(attempts[filename], children)
reload(attempts[filename], runtime)
this.ctx.logger.info('reload plugin at %c', path)
} catch (err) {
this.ctx.logger.warn('failed to reload plugin at %c', path)
Expand All @@ -300,10 +303,10 @@ class Watcher extends Service {
} catch {
// rollback cache and plugin states
rollback()
for (const [plugin, { filename, children }] of reloads) {
for (const [plugin, { filename, runtime }] of reloads) {
try {
this.ctx.registry.delete(attempts[filename])
reload(plugin, children)
reload(plugin, runtime)
} catch (err) {
this.ctx.logger.warn(err)
}
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.only('group management: basic support', () => {
describe('group management: basic support', () => {
const root = new Context()
root.plugin(MockLoader)
const loader = root.loader as unknown as MockLoader
Expand Down

0 comments on commit eff476d

Please sign in to comment.