refactor(boot): read a row failure's stage from the Loader's typed error
`stageOf` parsed the stage out of the Loader's `failed to <stage> loader entry` message. The vendored wrapper is now an exported `EntryUpdateError` carrying its `stage`, logged as a local modification, and the contained group reads the field. `isJsDisabled` wrapped `isJsExpr` behind a literal `disabled === true` comparison that already excluded expression nodes, so it goes. The post-boot rejection guard, its documentation, and the external-bundle module doc now say what they do: the process keeps running with nothing stopped or attributed to a plugin, and a contained group lets a failing row's siblings mount rather than failing the bundle as a whole.
This commit is contained in:
1 parent
0f95351405
commit
d5613da2cb
13 files changed
+54
-53
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-boot-scoped-fail-loud-and-package-probe.md
|
||||
2026-09-04-boot-scoped-fail-loud-and-package-probe.md: 0d50be413af8ed5622dd7289f86a4026b0c69f45
|
||||
2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md: 8b8c574941c616419958328bec70179fba5a5331
|
||||
2026-09-04-boot-scoped-fail-loud-and-package-probe.md: c0ae61e1323de3dcbeec826363dc953c32dbdcc5
|
||||
2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md: d4e136e36dcf90ad78b499123bfe56add868484d
|
||||
+2
-2
@@ -12,7 +12,7 @@ Separately, nothing could say what an installed package was without importing it
|
||||
|
||||
## Decision
|
||||
|
||||
**Fail-loud ends when the tree is up.** The launcher keeps `installFailLoud`'s uninstaller and calls it once `boot()` returns, then installs `installRuntimeGuards`: an unhandled rejection after boot is reported to stderr as contained and the process keeps running; an uncaught exception is reported with its origin and the process exits, as Node would, because its state is unknown. Both handlers are removed on shutdown.
|
||||
**Fail-loud ends when the tree is up.** The launcher keeps `installFailLoud`'s uninstaller and calls it once `boot()` returns, then installs `installRuntimeGuards`: an unhandled rejection after boot is reported to stderr and the process keeps running — the guard neither stops the task that produced it nor attributes it to a plugin, built-in or external, so this is a process-level policy chosen over exiting, not isolation; an uncaught exception is reported with its origin and the process exits, as Node would, because its state is unknown. Both handlers are removed on shutdown.
|
||||
|
||||
**Nested failures are reported, not yet fatal.** `warnNestedFiberFailures` walks every runtime's fibers and reports a `FAILED` fiber that belongs to a built-in entry but is not that entry's root fiber — a `ctx.inject()` continuation that threw, which the Loader stamps with the entry but the activation audit never sees. It runs after boot as advisory lines; it becomes part of the fatal audit once shipped compositions are known clean.
|
||||
|
||||
@@ -20,7 +20,7 @@ Separately, nothing could say what an installed package was without importing it
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Attribute a runtime rejection to the plugin that produced it and mark that plugin failed.** The right end state, but a promise carries no fiber, and cordis's effect wrappers cover only what plugins register through them. Deferred: the guard reports and contains now; attribution needs an async-context seam.
|
||||
**Attribute a runtime rejection to the plugin that produced it and mark that plugin failed.** The right end state, but a promise carries no fiber, and cordis's effect wrappers cover only what plugins register through them. Deferred: the guard reports and lets the process continue; attribution needs an async-context seam.
|
||||
|
||||
**Import the package in the host to learn its shape.** Rejected: import runs code with the host's privileges before the user enabled anything, and a hang or a `process.exit` in a package's module scope would be the host's.
|
||||
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决定
|
||||
|
||||
**fail-loud 在树起来时结束。** launcher 保留 `installFailLoud` 的卸载函数,在 `boot()` 返回后调用它,然后安装 `installRuntimeGuards`:启动后未处理的 rejection 以"已兜住"报告到 stderr,进程继续运行;未捕获的异常连同来源一起报告,进程退出,如 Node 所做,因为其状态未知。两个处理器在关闭时移除。
|
||||
**fail-loud 在树起来时结束。** launcher 保留 `installFailLoud` 的卸载函数,在 `boot()` 返回后调用它,然后安装 `installRuntimeGuards`:启动后未处理的 rejection 报告到 stderr,进程继续运行——守卫既不停止产生它的任务,也不把它归属到任何插件,内置或外部一视同仁,这是一条相对于退出而选择的进程级策略,不是隔离;未捕获的异常连同来源一起报告,进程退出,如 Node 所做,因为其状态未知。两个处理器在关闭时移除。
|
||||
|
||||
**嵌套失败被报告,暂不致命。** `warnNestedFiberFailures` 遍历每个 runtime 的 fiber,报告属于内置条目却不是该条目根 fiber 的 `FAILED` fiber——抛错的 `ctx.inject()` 延续,Loader 给它盖了条目的章,而激活审计从未看见它。它在启动后以提示行运行;确认随附组合没有这类失败后再并入致命审计。
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: implemented
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**把运行时 rejection 归属到产生它的插件并把该插件标为失败。** 正确的终态,但 promise 不携带 fiber,cordis 的 effect 包装也只覆盖插件经由它注册的东西。延后:守卫现在只报告并兜住;归属需要一个 async-context seam。
|
||||
**把运行时 rejection 归属到产生它的插件并把该插件标为失败。** 正确的终态,但 promise 不携带 fiber,cordis 的 effect 包装也只覆盖插件经由它注册的东西。延后:守卫现在只报告并让进程继续;归属需要一个 async-context seam。
|
||||
|
||||
**在宿主里 import 包来了解其形态。** 否决:import 会在用户启用任何东西之前以宿主权限运行代码,包的模块作用域里的一次挂起或 `process.exit` 就是宿主的。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
|
||||
README.md: f162e1e189c9366c0c4ce3e7aeac4b1ea736dcb1
|
||||
README.zh.md: 21cab2fbe661ca44bf16aa326a48951f49976229
|
||||
README.md: ee322da49b92b6ad7bd201c33665a2e29e2a4cd9
|
||||
README.zh.md: 44019585d345e02dfcfcd31113a171a292eef22a
|
||||
@@ -60,7 +60,7 @@ Inserted plugin names may be absolute filesystem paths, file URLs, or package sp
|
||||
|
||||
A bundle you installed with `dsh plugin` is an **external** bundle: its rows mount under one contained group named `bundle/<package>` with the ids its patch declares, and a row that fails to start is isolated and recorded instead of stopping the process — the group and its other rows stay up, and the plugin list shows the failure. Template bundles are built in and keep failing loud. A bundle that provides a service built-in rows inject must mount like a built-in one: its author declares `dsh.bundle.stage: boot` in `package.json`, or you set `dsh.profile.stages` in the profile manifest, which wins. Even without that, an isolated failure that leaves a built-in row waiting for a service still stops the boot and names the isolated bundle. Two more profile-manifest fields shape this: `dsh.profile.firstParty` lists installed packages treated as built in (a first-party package linked in during development), and `dependencies` versus `dsh.profile.bundles` distinguishes a package that is merely installed from one whose layer is enabled. Row ids share one namespace across the stack: built-in layers own theirs first, an external bundle that declares an id another layer already owns, or declares one of its own ids twice, is left out whole and reported on stderr and in the plugin list, and a user-layer insert of a taken id is dropped and reported the same way.
|
||||
|
||||
After the tree is up the launcher provides `ctx.profileRuntime`, which holds the composition the tree runs — the profile, the layer that owns each row, and the rows the composition left out — reads which rows the user patch files disable, and is the one entry point that recomposes the tree: the patch watchers and a runtime bundle enable or install all call it, and a rejected update leaves its facts describing the tree still running. Startup's fail-loud rejection guard is uninstalled once the tree is up: an unhandled rejection after boot is reported and contained, an uncaught exception is reported and exits.
|
||||
After the tree is up the launcher provides `ctx.profileRuntime`, which holds the composition the tree runs — the profile, the layer that owns each row, and the rows the composition left out — reads which rows the user patch files disable, and is the one entry point that recomposes the tree: the patch watchers and a runtime bundle enable or install all call it, and a rejected update leaves its facts describing the tree still running. Startup's fail-loud rejection guard is uninstalled once the tree is up: an unhandled rejection after boot is reported and the process keeps running, with nothing stopped or attributed to a plugin; an uncaught exception is reported and exits.
|
||||
|
||||
### Previewing the effective configuration
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
|
||||
|
||||
用 `dsh plugin` 安装的组合包是**外部**组合包:它的行挂在一个名为 `bundle/<package>` 的受控组下,id 保持它的 patch 所声明的样子,启动失败的行被隔离并记录而不是让进程停下——组和它的其他行继续运行,插件列表显示失败。模板组合包是内置的,仍然明确失败。若某个组合包提供内置行注入的服务,它必须像内置行一样挂载:作者在 `package.json` 里声明 `dsh.bundle.stage: boot`,或者你在 profile manifest 里设置 `dsh.profile.stages`,后者优先。即使没有这些声明,隔离的失败若让某个内置行停在等待服务的状态,启动仍会失败并点名那个被隔离的组合包。profile manifest 还有两个相关字段:`dsh.profile.firstParty` 列出按内置处理的已安装包(开发期 link 进来的一方包),`dependencies` 与 `dsh.profile.bundles` 的区别则把"只是装了"的包和"层已启用"的包分开。行 id 在整叠层里共用一个命名空间:内置层先占有自己的 id,外部组合包若声明了别的层已占有的 id,或把自己的某个 id 声明了两次,就整层被排除,并在 stderr 与插件列表里报告;用户层插入已被占用的 id 时该行被丢弃,同样报告。
|
||||
|
||||
树起来之后 launcher 提供 `ctx.profileRuntime`:它持有树正在运行的组合——profile、每一行的归属层、被组合排除的行——读取用户 patch 文件停用了哪些行,并且是重组整棵树的唯一入口:patch 监视器、运行时启用或安装组合包都调用它,被拒的更新留下的事实仍然描述正在运行的树。启动期的 fail-loud rejection 守卫在树起来后卸载:启动后未处理的 rejection 会被报告并兜住,未捕获的异常会被报告并退出。
|
||||
树起来之后 launcher 提供 `ctx.profileRuntime`:它持有树正在运行的组合——profile、每一行的归属层、被组合排除的行——读取用户 patch 文件停用了哪些行,并且是重组整棵树的唯一入口:patch 监视器、运行时启用或安装组合包都调用它,被拒的更新留下的事实仍然描述正在运行的树。启动期的 fail-loud rejection 守卫在树起来后卸载:启动后未处理的 rejection 会被报告,进程继续运行,不停止任何任务也不归属到任何插件;未捕获的异常会被报告并退出。
|
||||
|
||||
### 预览生效配置
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { Group, type Entry, type EntryGroup, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { EntryUpdateError, Group, type Entry, type EntryGroup, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
|
||||
/** The lifecycle step at which a contained row failed. */
|
||||
export type ContainedFailureStage = 'import' | 'apply' | 'inject-pending' | 'unknown'
|
||||
@@ -92,15 +92,16 @@ declare module '@deepseek-ai/cordis' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the Loader's `failed to <stage> loader entry` wrapper into a stage.
|
||||
* Every row failure reaches {@link ContainedGroup.create} through that
|
||||
* wrapper, so `unknown` is the guard for a message shape this build has not
|
||||
* seen rather than a path a row can take.
|
||||
* The stage a row failure reached, from the Loader's typed update error.
|
||||
* A fresh row fails at import or apply; the wrapper's other stages replace
|
||||
* an existing entry, which a contained row does not go through, and a value
|
||||
* the Loader did not wrap has no stage.
|
||||
*/
|
||||
function stageOf(message: string): ContainedFailureStage {
|
||||
const match = /^failed to (import|apply) loader entry /.exec(message)
|
||||
/* v8 ignore next -- the Loader wraps every row failure with one of the two stages, so the `unknown` arm is unreachable */
|
||||
return match === null ? 'unknown' : match[1] === 'import' ? 'import' : 'apply'
|
||||
function stageOf(error: unknown): ContainedFailureStage {
|
||||
/* v8 ignore next -- the Loader wraps every row failure; the arm keeps the type total */
|
||||
if (!(error instanceof EntryUpdateError)) return 'unknown'
|
||||
/* v8 ignore next -- dispose and rollback replace an existing entry, which a fresh contained row does not go through */
|
||||
return error.stage === 'import' || error.stage === 'apply' ? error.stage : 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +134,7 @@ export class ContainedGroup extends Group {
|
||||
rowId,
|
||||
moduleName: options.name,
|
||||
groupId: this.groupId(),
|
||||
stage: stageOf(message),
|
||||
stage: stageOf(error),
|
||||
message,
|
||||
})
|
||||
this.ctx.logger.warn(`contained group ${this.groupId()}: row ${rowId} failed and was isolated: ${message}`)
|
||||
|
||||
@@ -5,27 +5,18 @@
|
||||
* An external (`runtime` stage) bundle never mounts its rows directly into the
|
||||
* built-in tree: its inserted rows are wrapped in one contained group per
|
||||
* bundle, under the ids its patch declares. A group is the unit the Loader
|
||||
* rolls back, so one group per bundle is what makes a bundle fail as a whole
|
||||
* rather than half-mount, and the contained variant records a failed row
|
||||
* instead of rejecting. Row ids stay as declared; entry ids are unique per
|
||||
* tree (`tree.store`), and `compose-stack.ts` owns the tree-wide ownership
|
||||
* check that shared id namespace requires.
|
||||
* updates transactionally, and the contained variant catches each row's
|
||||
* failure inside that transaction: the failing row is recorded, its siblings
|
||||
* mount, and the built-in tree never sees a rejection. Row ids stay as
|
||||
* declared; entry ids are unique per tree (`tree.store`), and
|
||||
* `compose-stack.ts` owns the tree-wide ownership check that shared id
|
||||
* namespace requires.
|
||||
* @module @deepseek-ai/dsh-app-boot/external-bundles
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import { isJsExpr } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { visitInsertedRows } from './patch-rows.ts'
|
||||
|
||||
/**
|
||||
* Whether a `disabled` node is a `!!js` expression rather than a literal.
|
||||
* @param value - the raw `disabled` node of a row or patch.
|
||||
* @returns true for an expression node.
|
||||
*/
|
||||
export function isJsDisabled(value: unknown): boolean {
|
||||
return isJsExpr(value)
|
||||
}
|
||||
import {
|
||||
readProfileManifest, resolveBundleDir, writeProfileManifest, type ProfileLayer, type ProfileManifest,
|
||||
} from './profile.ts'
|
||||
|
||||
@@ -56,7 +56,7 @@ export {
|
||||
} from './contained-group.ts'
|
||||
export {
|
||||
BUNDLE_GROUP_PREFIX, bundleGroupId, composeExternalLayer, CONTAINED_GROUP_MODULE, disableBundle,
|
||||
enableBundle, exportsBundlePatch, isContainedLayer, isJsDisabled, reconcileInstalledBundles,
|
||||
enableBundle, exportsBundlePatch, isContainedLayer, reconcileInstalledBundles,
|
||||
type BundleReconciliation, type ComposedExternalLayer, type DuplicateRow,
|
||||
} from './external-bundles.ts'
|
||||
export {
|
||||
@@ -721,9 +721,12 @@ export interface RuntimeGuardProcess {
|
||||
* The post-boot replacement for {@link installFailLoud}. Once the tree is up,
|
||||
* an unhandled rejection is no longer a load failure: it is most likely a
|
||||
* plugin's stray continuation, and exiting would take every session down for
|
||||
* it. The rejection is reported and the process keeps running. An uncaught
|
||||
* exception leaves the process in an unknown state, so it is reported and
|
||||
* the process exits, as Node would — but with the origin named.
|
||||
* it. The rejection is reported and the process keeps running; nothing is
|
||||
* stopped or attributed to a plugin, built-in or external, so this is a
|
||||
* process-level policy chosen over exiting, not an isolation of the plugin
|
||||
* that produced it. An uncaught exception leaves the process in an unknown
|
||||
* state, so it is reported and the process exits, as Node would — but with
|
||||
* the origin named.
|
||||
* @param binName - the diagnostic prefix on each report.
|
||||
* @param report - sink for the report lines.
|
||||
* @param proc - the process slice to register on; defaults to `process`.
|
||||
@@ -735,7 +738,7 @@ export function installRuntimeGuards(
|
||||
proc: RuntimeGuardProcess = process,
|
||||
): () => void {
|
||||
const onRejection = (err: unknown): void => {
|
||||
report(`${binName}: unhandled rejection after boot (contained; the process keeps running): ${formatActivationError(err)}`)
|
||||
report(`${binName}: unhandled rejection after boot (not attributed to a plugin; the process keeps running): ${formatActivationError(err)}`)
|
||||
}
|
||||
const onException = (err: unknown): void => {
|
||||
report(`${binName}: uncaught exception after boot; exiting: ${formatActivationError(err)}`)
|
||||
|
||||
@@ -18,7 +18,6 @@ import type Include from '@deepseek-ai/cordis-plugin-include'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import type { ProfilePatchReload } from '@deepseek-ai/dsh-package-manifest'
|
||||
import type { ComposedStack, RowConflict } from './compose-stack.ts'
|
||||
import { isJsDisabled } from './external-bundles.ts'
|
||||
import type { BundleTrust, Profile, ProfileLayer } from './profile.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
@@ -121,15 +120,16 @@ export class ProfileRuntime extends Service {
|
||||
|
||||
/**
|
||||
* Row ids the user patch layers disable with a literal `disabled: true`.
|
||||
* A `!!js` gate in a user file is a condition, not a user decision, and is
|
||||
* left to the composition.
|
||||
* A `!!js` gate in a user file stays an expression node when read from
|
||||
* disk, so it is a condition, not a user decision, and is left to the
|
||||
* composition.
|
||||
* @returns the ids, re-read from disk on every call.
|
||||
*/
|
||||
userDisabledRowIds(): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
for (const patch of this.options.readUserPatches()) {
|
||||
if (patch.insert !== undefined || patch.id === undefined) continue
|
||||
if (patch.disabled === true && !isJsDisabled(patch.disabled)) ids.add(patch.id)
|
||||
if (patch.disabled === true) ids.add(patch.id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import {
|
||||
bundleGroupId, composeExternalLayer, CONTAINED_GROUP_MODULE, disableBundle, enableBundle,
|
||||
exportsBundlePatch, isContainedLayer, isJsDisabled, reconcileInstalledBundles, readProfileManifest, type ProfileLayer,
|
||||
exportsBundlePatch, isContainedLayer, reconcileInstalledBundles, readProfileManifest, type ProfileLayer,
|
||||
} from '../src/index.ts'
|
||||
|
||||
const NAME = 'dsh-test-bin'
|
||||
@@ -139,11 +139,6 @@ describe('composeExternalLayer', () => {
|
||||
expect(isContainedLayer({ ...contained, stage: 'boot' })).toBe(false)
|
||||
expect(isContainedLayer({ ...contained, trust: 'builtin' })).toBe(false)
|
||||
})
|
||||
|
||||
it('tells a !!js disabled node from a literal', () => {
|
||||
expect(isJsDisabled({ __jsExpr: 'true' })).toBe(true)
|
||||
expect(isJsDisabled(true)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
/** Stage a profile directory whose node_modules holds the named packages. */
|
||||
|
||||
Vendored
+1
@@ -49,6 +49,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
|
||||
17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table's `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for('schemastery')` and Schemastery's `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table's two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).
|
||||
18. **Entry `disabled` interpolation in `loader/src/config/entry.ts`**: a `disabled: !!js` expression evaluates against the loader context at every mount decision; the raw node stays in the options, so write-back keeps the `!!js` form. `disabled` is the only interpolated metadata field. Covered by `packages/boot/app-boot/tests/user-patches.spec.ts` and `apps/cli/tests/windows-shell.spec.ts`.
|
||||
19. **`loader/src/internal.ts` runtime shape detection**: `ModuleLoader.fromInternal()` classifies the internal loader by which module-job API it owns — `getOrCreateModuleJob` for v2, `getModuleJobForImport` for v1 — instead of by Node major version. Upstream tags every major `>= 24` as v2, but the v2 interface arrived in Node 24.12.0, so 24.0–24.11.1 report major 24 while still carrying the v1 loader; consumers then called `resolveSync` with reversed parameters and every call threw. `dsh web` served an empty client graph (`__DSH_BOOT__.entries: []`) and HMR partial reload resolved no entry URL, both behind swallowed or warn-level errors. Arity cannot discriminate the two shapes, because each reports `resolveSync.length === 2`. A loader owning neither API is left unclassified rather than guessed, so consumers take their documented no-internals path. Covered on the `node-compat` Node version matrix, which pins 24.9 for the mistagged range.
|
||||
20. **`loader/src/config/entry.ts` typed update errors**: the per-row `failed to <stage> loader entry` wrapper is an exported `EntryUpdateError` carrying its `stage`, so a consumer that classifies a row failure reads the field instead of parsing the message. The message text is unchanged. Covered by `packages/boot/app-boot/tests/contained-group.spec.ts`.
|
||||
|
||||
## Sync procedure
|
||||
|
||||
|
||||
Vendored
+13
-3
@@ -21,9 +21,19 @@ export interface EntryOptions {
|
||||
inject?: Inject | null
|
||||
}
|
||||
|
||||
function updateError(stage: 'import' | 'dispose' | 'apply' | 'rollback', options: EntryOptions, cause: unknown) {
|
||||
const detail = cause instanceof Error ? cause.message : String(cause)
|
||||
return new Error(`failed to ${stage} loader entry ${options.id} (${options.name}): ${detail}`, { cause })
|
||||
/** The step of an entry update that failed. */
|
||||
export type EntryUpdateStage = 'import' | 'dispose' | 'apply' | 'rollback'
|
||||
|
||||
/** The failure of one entry update, carrying the step that failed so callers need not parse the message. */
|
||||
export class EntryUpdateError extends Error {
|
||||
constructor(readonly stage: EntryUpdateStage, options: EntryOptions, cause: unknown) {
|
||||
const detail = cause instanceof Error ? cause.message : String(cause)
|
||||
super(`failed to ${stage} loader entry ${options.id} (${options.name}): ${detail}`, { cause })
|
||||
}
|
||||
}
|
||||
|
||||
function updateError(stage: EntryUpdateStage, options: EntryOptions, cause: unknown) {
|
||||
return new EntryUpdateError(stage, options, cause)
|
||||
}
|
||||
|
||||
function takeEntries(object: {}, keys: string[]) {
|
||||
|
||||
Reference in new issue
Block a user