Spaces:
Sleeping
Sleeping
| import { invariant } from 'outvariant' | |
| class PatchesRegistry { | |
| #replacements = new Map<object, Map<PropertyKey, () => void>>() | |
| public applyPatch<Owner extends object, K extends keyof Owner>( | |
| owner: Owner, | |
| key: K, | |
| getNextValue: (realValue: Owner[K]) => Owner[K] | |
| ): () => void { | |
| const ownerReplacements = this.#replacements.get(owner) | |
| invariant( | |
| !ownerReplacements?.has(key), | |
| `Failed to replace a global value at "${String(key)}": already replaced.` | |
| ) | |
| const match = getDeepPropertyDescriptor(owner, key) | |
| if (typeof match === 'undefined') { | |
| console.warn( | |
| `Failed to replace a global value at "${String(key)}": not a global value.` | |
| ) | |
| return () => {} | |
| } | |
| if (match.descriptor.configurable) { | |
| Object.defineProperty(owner, key, { | |
| value: getNextValue(owner[key]), | |
| enumerable: true, | |
| configurable: true, | |
| }) | |
| } else if (match.descriptor.writable) { | |
| owner[key] = getNextValue(owner[key]) | |
| } else { | |
| throw new Error( | |
| `Failed to patch a non-configurable non-writable property "${key.toString()}"` | |
| ) | |
| } | |
| const restorePatch = () => { | |
| const currentReplacements = this.#replacements.get(owner) | |
| if (!currentReplacements?.has(key)) { | |
| return | |
| } | |
| if (match.owner === owner) { | |
| /** | |
| * @note Restoring non-configurable properties works as long as "writable: true" | |
| * and none of the other descriptor properties except for "value" have changed. | |
| */ | |
| Object.defineProperty(match.owner, key, match.descriptor) | |
| } else { | |
| /** | |
| * @todo Delete the proxy property set by the registry. | |
| * If the match's owner isn't the original owner, the property is likely nested in the prototype. | |
| * The registry does not meddle with those, they are left intact. | |
| */ | |
| Reflect.deleteProperty(owner, key) | |
| } | |
| currentReplacements.delete(key) | |
| if (currentReplacements.size === 0) { | |
| this.#replacements.delete(owner) | |
| } | |
| } | |
| if (ownerReplacements) { | |
| ownerReplacements.set(key, restorePatch) | |
| } else { | |
| this.#replacements.set(owner, new Map([[key, restorePatch]])) | |
| } | |
| return restorePatch | |
| } | |
| public restoreAllPatches(): void { | |
| const errors: Array<Error> = [] | |
| for (const [, ownerReplacements] of this.#replacements) { | |
| for (const [, restorePatch] of ownerReplacements) { | |
| try { | |
| restorePatch() | |
| } catch (error) { | |
| if (error instanceof Error) { | |
| errors.push(error) | |
| } else { | |
| throw error | |
| } | |
| } | |
| } | |
| } | |
| if (errors.length > 0) { | |
| throw new AggregateError(errors, 'FOO!') | |
| } | |
| } | |
| } | |
| export const patchesRegistry = new PatchesRegistry() | |
| interface DeepDescriptorMatch { | |
| owner: object | |
| descriptor: PropertyDescriptor | |
| } | |
| /** | |
| * Returns a property descriptor for the given property on the owner. | |
| * Walks down the prototype chain if the property does not exist on the owner. | |
| * Handy for getting a global property descriptor where `globalThis` is | |
| * replaced with a controlled class (e.g. ServiceWorkerGlobalScope). | |
| */ | |
| export function getDeepPropertyDescriptor<Owner extends object>( | |
| owner: Owner, | |
| key: keyof Owner | |
| ): DeepDescriptorMatch | undefined { | |
| let currentOwner: Owner | null = owner | |
| let descriptor: PropertyDescriptor | undefined | |
| while (currentOwner) { | |
| descriptor = Object.getOwnPropertyDescriptor(currentOwner, key) | |
| if (descriptor) { | |
| return { | |
| owner: currentOwner, | |
| descriptor, | |
| } | |
| } | |
| currentOwner = Object.getPrototypeOf(currentOwner) | |
| } | |
| } | |