Revert #932 transactional Cordis reload changes

Apply real git revert --no-commit operations in reverse order:

acb0797a1421eab128a4bd03355e930c1a221fb0
 af97714f2413a3e55f0f15ab1b283beb133b3f2d
2018b0b7e64b444bff82b62c9466f13adc4603bb
43170c027cfbf9155dad7a43c01ca39364d4aef2
a621e927d6b3763798c3929e28912d86c9576f2d

Resolve the app-boot package move, preserve deleted files and frozen archived
notes, retain the modern lockfile graph, and preserve independent later
lifecycle and lazy-config changes. Do not revert 6ac7d72d: that merge also
contains eight unrelated commits from the #936 dependency chain.

This is the mechanical intermediate result. Its focused check exposed
consumer and packaging incompatibilities. The following compatibility
commit supplies those adaptations and reconciles the vendor ledger.
This commit is contained in:
turtle1999 committed 2026-09-09 17:32:09 +08:00
1 parent 6d99a8a53a
commit e07f41d5fd
22 files changed
+206 -1126

No files matched your search

+14 -162
View File
@@ -3,8 +3,8 @@ import type { Dict } from '@deepseek-ai/cosmokit'
import { ModuleLoader, type ModuleJob, type ResolveResult } from '@deepseek-ai/cordis-plugin-loader'
import type { Include } from '@deepseek-ai/cordis-plugin-include'
import { FSWatcher, watch, type ChokidarOptions } from 'chokidar'
import { dirname, relative, resolve } from 'node:path'
import { realpath, stat } from 'node:fs/promises'
import { relative, resolve } from 'node:path'
import { realpath } from 'node:fs/promises'
import { handleError } from './error.ts'
import type {} from '@deepseek-ai/cordis-plugin-timer'
import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -20,13 +20,7 @@ declare module '@deepseek-ai/cordis' {
interface Events {
'hmr/change'(url: string): void
'hmr/reload'(reloads: Map<Plugin, Reload>): void
/**
* A watched config-file refresh failed.
* @param filename - Absolute path observed by HMR.
* @param error - Normalized refresh failure.
* @mode parallel
*/
'hmr/config-update-failed'(filename: string, error: Error): Promise<void> | void
}
}
@@ -52,37 +46,6 @@ interface Reload {
runtime?: Plugin.Runtime
}
interface ConfigRefresh {
dirty: boolean
running?: Promise<void>
}
interface ConfigRegistration {
watcher: FSWatcher
}
async function findWatchRoot(filename: string): Promise<{ filename: string; root: string; depth: number }> {
let root = dirname(filename)
let depth = 0
while (true) {
try {
if (!(await stat(root)).isDirectory()) throw new Error(`config watch parent is not a directory: ${root}`)
const canonicalRoot = await realpath(root)
return {
filename: resolve(canonicalRoot, relative(root, filename)),
root: canonicalRoot,
depth,
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const parent = dirname(root)
if (parent === root) throw error
root = parent
depth += 1
}
}
}
class Hmr extends Service {
static inject = ['loader', 'timer']
@@ -90,9 +53,6 @@ class Hmr extends Service {
private internal: ModuleLoader
private watcher!: FSWatcher
private readonly configs = new Map<string, ConfigRegistration>()
private readonly configRefreshes = new WeakMap<object, ConfigRefresh>()
private readonly refreshTasks = new Set<Promise<void>>()
/**
* Changes from externals will always trigger a full reload.
@@ -124,68 +84,6 @@ class Hmr extends Service {
this.baseDir = fileURLToPath(new URL(config.base || '.', ctx.baseUrl))
}
/**
* Watch one exact config path outside the configured module roots.
* @param filename - Config path, resolved against the HMR base directory.
* @param refresh - Refresh callback run serially on add, change, or unlink.
* @returns an asynchronous disposer once the exact watch is ready.
* @throws when HMR is inactive, the path is already registered, or watcher startup fails.
*/
async registerConfig(filename: string, refresh: () => Promise<void> | void): Promise<() => Promise<void>> {
if (!this.watcher) throw new Error('HMR is not active')
filename = resolve(this.baseDir, filename)
const target = await findWatchRoot(filename)
const watchFilename = target.filename
if (this.configs.has(watchFilename)) throw new Error(`config path already registered: ${filename}`)
const { root, depth } = target
const watcher = watch(root, {
...this.config,
cwd: undefined,
depth,
ignored: undefined,
ignoreInitial: false,
})
const registration = { watcher }
this.configs.set(watchFilename, registration)
const onChange = (path: string) => {
const observed = resolve(path)
if (observed !== filename && observed !== watchFilename) return
this.refreshConfig(registration, filename, refresh)
}
watcher.on('add', onChange)
watcher.on('change', onChange)
watcher.on('unlink', onChange)
const ready = Promise.withResolvers<void>()
let readyState: 'pending' | 'resolved' | 'rejected' = 'pending'
watcher.once('ready', () => {
readyState = 'resolved'
ready.resolve()
})
watcher.on('error', (error) => {
if (readyState === 'pending') {
readyState = 'rejected'
ready.reject(error)
} else {
this.ctx.logger.warn(error)
}
})
try {
await ready.promise
return this.ctx.effect(() => async () => {
if (this.configs.get(watchFilename) === registration) this.configs.delete(watchFilename)
await watcher.close()
await this.configRefreshes.get(registration)?.running
}, 'hmr.registerConfig()')
} catch (error) {
this.configs.delete(watchFilename)
await watcher.close()
throw error
}
}
/**
* Resolve a module specifier to a URL, compatible with Node 22-24.
*/
@@ -197,12 +95,7 @@ class Hmr extends Service {
}
async* [Service.init]() {
yield async () => {
await this.watcher?.close()
await Promise.allSettled([...this.configs.values()].map(registration => registration.watcher.close()))
this.configs.clear()
await Promise.allSettled([...this.refreshTasks])
}
yield () => this.watcher?.close()
const { loader } = this.ctx
const { root, ignored } = this.config
@@ -229,31 +122,15 @@ class Hmr extends Service {
...this.config,
cwd: watchBaseDir,
ignored: path => match(relative(watchBaseDir, path)),
// The initial scan re-announces files the boot just consumed: an `add`
// for a config file refreshes an include whose initial apply may still
// be in flight, and a failing apply then rolls this plugin back while
// the scan-triggered refresh waits on that apply — a teardown deadlock
// that strands boot without a diagnostic. Only events after the scan
// matter here; `registerConfig` keeps its own initial scan because a
// user patch layer present at registration must apply once.
ignoreInitial: true,
})
const partialReload = this.ctx.debounce(() => this.partialReload(), this.config.debounce)
const onChange = (kind: 'add' | 'change' | 'unlink', path: string) => {
this.ctx.logger.debug('%s detected at %C', kind, path)
this.watcher.on('change', async (path) => {
this.ctx.logger.debug('change detected at %C', path)
const filename = resolve(watchBaseDir, path)
const configuredFilename = resolve(this.baseDir, path)
// Config reload: the file is a loader config file (e.g. cordis.yml).
for (const entry of loader.entries()) {
const include = entry.subtree as Include | undefined
if (include?.filename !== filename && include?.filename !== configuredFilename) continue
this.refreshConfig(include, include.filename, () => include.refresh())
return
}
if (kind !== 'change') return
const url = pathToFileURL(filename).href
// Full reload: the changed file is part of the framework
@@ -267,11 +144,15 @@ class Hmr extends Service {
return partialReload()
}
for (const entry of loader.entries()) {
const include = entry.subtree as Include | undefined
if (include?.filename !== filename && include?.filename !== configuredFilename) continue
await include.refresh()
return
}
this.ctx.emit('hmr/change', url)
}
this.watcher.on('add', path => onChange('add', path))
this.watcher.on('change', path => onChange('change', path))
this.watcher.on('unlink', path => onChange('unlink', path))
})
const ready = Promise.withResolvers<void>()
let readyState: 'pending' | 'resolved' | 'rejected' = root.length === 0 ? 'resolved' : 'pending'
@@ -294,35 +175,6 @@ class Hmr extends Service {
await ready.promise
}
private refreshConfig(key: object, filename: string, refresh: () => Promise<void> | void) {
const state = this.configRefreshes.get(key) ?? { dirty: false }
this.configRefreshes.set(key, state)
state.dirty = true
if (state.running) return
const task = (async () => {
do {
state.dirty = false
try {
await refresh()
} catch (reason) {
const error = reason instanceof Error ? reason : new Error(String(reason), { cause: reason })
this.ctx.logger.warn('config reload at %C failed', filename)
this.ctx.logger.warn(error)
try {
await this.ctx.parallel('hmr/config-update-failed', filename, error)
} catch (rejection) {
this.ctx.logger.warn(rejection)
}
}
} while (state.dirty)
})().finally(() => {
state.running = undefined
this.refreshTasks.delete(task)
})
state.running = task
this.refreshTasks.add(task)
}
// hide stack trace from HMR
getOuterStack = (): string[] => [
// ' at HMR.partialReload (<anonymous>)',