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:
1 parent
6d99a8a53a
commit
e07f41d5fd
22 files changed
+206
-1126
No files matched your search
Vendored
+14
-162
@@ -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>)',
|
||||
|
||||
Reference in new issue
Block a user