Skip to main content

What is patching?

Patching (also called monkey-patching) wraps an existing function with your own code, which runs before it, after it, or in place of it. Plugins use it to change behavior they don’t own, from a Discord method inside a module to a component’s render. Doing this by hand means keeping a reference to the original, restoring it later, and working alongside other plugins that patch the same function. The patcher handles all three. Unbound re-exports it unchanged from the possess package, the same patcher its own built-ins use.

Creating a patcher

Call createPatcher with your plugin’s id to get a patcher instance. Every patch you apply through it is grouped under that name, so one unpatchAll call in stop reverts all of them.
string | PatchOptions
required
A caller name, or a full PatchOptions object. A string is shorthand for { caller: name }. If you pass an object without caller, a random UUID is generated for it.
The instance has four methods:
typeof before
Adds a before patch. Your defaultOptions are merged with the options you pass per call, and per-call options win.
typeof after
Adds an after patch, with the same option merging.
typeof instead
Adds an instead patch, with the same option merging.
() => void
Removes every patch registered under this instance’s caller. Equivalent to patcher.unpatchAllByCaller(caller).
Two instances created with the same name share a caller, so calling unpatchAll on either one removes the patches of both.

The three patch types

Pick the type by when your code needs to run relative to the original:
Runs before the original. Mutate args, or return a new array, to change what the original receives. Use it to rewrite inputs.

Applying a patch

before, after, and instead all take the same arguments and return a function that removes that one patch. You can call them on an instance or directly on patcher. Patches applied directly on patcher have no caller unless you pass one in options.
PatchParent
required
The object (or function) that owns the method.
PropOf<parent>
required
The property name of the method on parent. The type only accepts keys whose values are functions or constructors.
PatchCallback
required
Called with a PatchContext each time the patched method runs. It is also called with the same this as the patched method.
PatchOptions
Per-patch options.
If parent[method] is missing or isn’t a function, the call throws The prop you provided does not exist on the parent object or is not a function.
The first patch on a method replaces parent[method] with a wrapper function. Later patches on the same method reuse that wrapper. The wrapper copies the original’s own properties (except length), and its toString() returns the original’s source.

The patch context

Every callback receives a context object. All callbacks on the same call share one context, so a before patch can store data on it for an after patch to read.
Args
The arguments the method was called with. Mutate it, or return a new array from a before patch, to change what the original receives.
Self
The this value the method was called with.
Res | null
The return value so far. It is null until the original or an instead patch produces a value, so read it in after patches.
(...args) => Res
Calls the unpatched method. It always uses the this of the current call, so original(...args) keeps the right context. When the patched function is called with new, original constructs with new too.
Calling original from a before or after patch runs the original an extra time. The wrapper still makes its own call.

The this context

ctx.this is the this value JavaScript resolved at the call site. Its value depends on how the function was called.
For a method on a class prototype, this is the instance the method was called on. Read its props, state, or fields through ctx.this.

Return values

A callback can change the call by mutating ctx or by returning a value. The wrapper treats the return value differently for each patch type: Returning undefined from instead or after leaves ctx.result as it was. To make the call return undefined, set ctx.result = undefined instead of returning it.
If no instead patch calls original and returns a value, the call returns null, the initial value of ctx.result. Only use instead when you mean to take over the call.

Execution order

On every call, the wrapper:
  1. Builds the context, with result set to null.
  2. Runs each before patch in registration order.
  3. If there are no instead patches, calls the original with ctx.args and stores its return value in ctx.result. Otherwise it runs each instead patch in registration order and does not call the original.
  4. Runs each after patch in registration order. Each one sees the result so far.
  5. Returns ctx.result.

Typing a patch

The patcher infers Args, Res, and Self from the target function. If the module is typed, args, result, and this are typed too, with no annotations needed.
If you define a callback outside the patch call, type it with PatchContext<Args, Result, Self>. All three type parameters are optional and default to any, so pass only the ones you need.

Patch options

PatchOptions can be passed to createPatcher as defaults or to a single patch call as the fourth argument.
string
The name the patch is grouped under. unpatchAllByCaller and an instance’s unpatchAll remove patches by this name.
boolean
Removes the patch after its callback runs once. Use it for targets that only exist for one call or one render, such as a child component created fresh on each parent render. See Temporary patches from a parent.
A once patch is removed only after its callback returns. If the callback throws, the patch stays in place and runs again on the next call.

Patching classes

The same three methods work on constructors. Patch the class by name on its parent module. args holds the constructor arguments, and in an after patch result is the new instance.

Errors in callbacks

Each callback runs inside its own try/catch. When one throws, the wrapper logs the error with console.error and moves on to the next patch. Other patches and the call itself still run, so one broken plugin doesn’t stop other plugins’ patches on the same function. The log line names the patch type:

Cleaning up

Call unpatchAll on your instance in your plugin’s stop. Each function goes back to its original, so the next plugin, or your plugin when re-enabled, can patch it cleanly.
You must call unpatchAll in stop. Otherwise your patches keep running after the plugin is disabled. If you patched a component’s render, also force a re-render so the original UI comes back. See cleaning up.
There are four ways to remove patches: When the last patch on a method is removed, the original function is written back to parent[method] and the method’s entry is dropped from patches.
patcher.unpatchAll() removes patches applied by every plugin and by Unbound itself. In a plugin, use your instance’s unpatchAll or unpatchAllByCaller with your own caller name.
If you always use your plugin id as the caller name, any code can clean up your patches with patcher.unpatchAllByCaller(id).

Inspecting patches

Use these exports to check what is patched, for debugging and tooling.
PatchedModule[]
One entry for every patched method. Each entry has parent (a weak reference, read it with parent.deref()), method, original, and patches: a record of Sets keyed by PatchType.
enum
Before = 0, Instead = 1, After = 2. These are the keys of PatchedModule.patches.
unique symbol
Symbol.for('$$patched$$'). Every wrapper function has this property set to true.
The patcher keeps only a weak reference to each parent, so patching an object doesn’t keep it in memory. Where WeakRef isn’t available, it falls back to a strong reference.

Types

Import types from the patcher entry point: