/** * The dshloader PATCH protocol (`ctx.dshLoader.patch` / `__dshLoader__.patch`). * * Monkey-patching a host method or global is the only way to intervene in * behaviour dsh exposes no extension point for. Three plugins in this family * each hand-rolled the protocol and they did NOT agree: * * - dsh-loader's own `Module._resolveFilename` hook → identity-checked restore * - dsh-network-settings' `globalThis.fetch` wrapper → identity-checked restore * - dsh-better-sidebar's `workspaces.openPath` wrap → UNCONDITIONAL restore * * The last one silently destroys a later plugin's wrapper when it disposes * first, even though its own comment promised the opposite. This module is the * single correct implementation; every patch site delegates here. * * The five guarantees: * * 1. RAW original — each patch captures EXACTLY the value present when it * installed (another plugin's wrapper when one is already there) and * restores that value verbatim, never a bound copy. This is what lets a * chain of wrappers survive disposal in any order. * 2. Identity-checked restore — disposal only reverts the slot when it still * holds OUR wrapper. If somebody patched on top afterwards, we leave the * chain alone (reverting would delete their wrapper). * 3. Re-apply safety — re-applying the same `id` recovers the true original * first, so HMR / repeated `apply()` can never nest a wrapper in itself. * This holds while our patch is the OUTERMOST one; if a foreign patch was * layered on top in between, re-applying chains instead (un-nesting a * middle wrapper is not possible without rebuilding the whole chain). * 4. Cross-instance durability — slot bookkeeping lives in a WeakMap parked on * `globalThis` under a `Symbol.for` key, so a reloaded module instance still * sees the original captured by its predecessor. Targets are never mutated. * 5. Loud misuse — patching a non-function method, or a missing target, throws * instead of silently no-op'ing. * * Pure and environment-agnostic: no DOM, no Node builtins, so the same module * serves the host half and the browser half. * * @module @dsh-plugin/dsh-loader/patch */ /** A live patch, disposed through {@link PatchHandle.dispose}. */ export interface PatchHandle { /** Remove this patch when the slot still carries its wrapper (idempotent). */ dispose(): void; /** Whether the target slot still carries this patch's wrapper. */ readonly active: boolean; /** The raw value captured before any dshloader patch touched the slot. */ readonly original: unknown; } /** Options shared by every patch entry point. */ export interface PatchOptions { /** * Stable patch id. Re-applying the same id on the same slot recovers the * true original before re-wrapping (guarantee 3), so HMR cannot nest * wrappers. Defaults to `'anonymous'` — pass a real id (`':'`) * for any patch that can be re-applied. */ id?: string; } /** * Patch one slot of `target`. The low-level primitive behind * {@link PatchAPI.method} and {@link PatchAPI.global}. * * @param target - object owning the slot (a service instance, `globalThis`, …). * @param key - property key to wrap. * @param wrap - receives the value currently in the slot (the previous wrapper * when another patch is already installed — chaining is intended) and returns * the replacement. * @param options - see {@link PatchOptions}. * @returns the handle whose `dispose()` reverts this patch. */ export declare function patchSlot(target: object, key: PropertyKey, wrap: (original: T) => T, options?: PatchOptions): PatchHandle; /** Whether a dshloader patch is currently installed on `target[key]`. */ export declare function isPatched(target: object, key: PropertyKey): boolean; /** The patch id currently installed on `target[key]`, or `undefined`. */ export declare function patchIdOf(target: object, key: PropertyKey): string | undefined; /** The stable `patch` facade exposed on the host and browser APIs. */ export interface PatchAPI { /** * Wrap a method of a service / object. * * ```ts * const handle = ctx.dshLoader.patch.method( * workspaces, 'openPath', * original => (path: string) => takeover(path) ?? original.call(workspaces, path), * { id: 'better-sidebar:openPath' }, * ) * ``` */ method(target: T, key: K, wrap: (original: T[K]) => T[K], options?: PatchOptions): PatchHandle; /** * Wrap a global function such as `fetch`. Defaults to `globalThis`; pass * `scope` for `window` or a test double. * * ```ts * ctx.dshLoader.patch.global('fetch', * original => (input, init) => original(rewrite(input), init), * { id: 'network-settings:fetch' }) * ``` */ global(key: PropertyKey, wrap: (original: T) => T, options?: PatchOptions & { scope?: object; }): PatchHandle; /** Low-level primitive; `method`/`global` are thin wrappers over it. */ slot(target: object, key: PropertyKey, wrap: (original: T) => T, options?: PatchOptions): PatchHandle; /** Whether a dshloader patch currently occupies `target[key]`. */ isPatched(target: object, key: PropertyKey): boolean; /** The patch id currently occupying `target[key]`, if any. */ patchIdOf(target: object, key: PropertyKey): string | undefined; } /** Build the `patch` facade. Stateless — all bookkeeping is in the global registry. */ export declare function createPatchAPI(): PatchAPI;