From 5e1455c3d6ed27e7ae0eb6063df1ba2cd84bb9b8 Mon Sep 17 00:00:00 2001 From: solarcloud7 Date: Sat, 11 Jul 2026 03:57:09 -0400 Subject: [PATCH 1/3] Document Link method binding requirements --- docs/writing-plugins.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index faf88a5f..0c1f3b01 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -455,6 +455,20 @@ async frobnicate() { } ``` +The send and handle methods depend on `this`, so call them directly on the connection object. +Extracting one into a variable, destructuring it, or passing it as an unbound callback drops the receiver and the method later throws when called with `this === undefined` (for example `Cannot read properties of undefined (reading 'sendRequest')`). +A type cast around a method that is invoked directly still retains its receiver, but storing or passing the cast result is an unsafe extraction. +When possible, cast the arguments or result instead of the method so the binding remains obvious. + +```js +// Broken: the extracted method loses its `this` binding and crashes when called. +const send = this.instance.sendTo; +send("controller", new messages.Frobnicate({ foo: "bar" })); + +// Correct: call it bound on the connection object and cast the arguments/result if needed. +this.instance.sendTo("controller", new messages.Frobnicate({ foo: "bar" })); +``` + For classes with `static type = "request"` the send method is async and returns a promise that resolves to the response data received from the target it was sent to, or rejects with an error if the request failed. The destination specification can either be the ID of a particular control, host or instance, or one of the keywords used to send to multiple targets at once. From 0e8fe54ae373ef4bb7d09c3ee2a960aba482e043 Mon Sep 17 00:00:00 2001 From: solarcloud7 Date: Sun, 12 Jul 2026 01:29:24 -0400 Subject: [PATCH 2/3] Mention .bind for methods passed as values --- docs/writing-plugins.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index 0c1f3b01..10cc1e64 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -459,6 +459,7 @@ The send and handle methods depend on `this`, so call them directly on the conne Extracting one into a variable, destructuring it, or passing it as an unbound callback drops the receiver and the method later throws when called with `this === undefined` (for example `Cannot read properties of undefined (reading 'sendRequest')`). A type cast around a method that is invoked directly still retains its receiver, but storing or passing the cast result is an unsafe extraction. When possible, cast the arguments or result instead of the method so the binding remains obvious. +When a method has to be passed as a value, use `.bind()` to fix its receiver first — most commonly when registering handlers. ```js // Broken: the extracted method loses its `this` binding and crashes when called. @@ -467,6 +468,9 @@ send("controller", new messages.Frobnicate({ foo: "bar" })); // Correct: call it bound on the connection object and cast the arguments/result if needed. this.instance.sendTo("controller", new messages.Frobnicate({ foo: "bar" })); + +// Correct: bind the receiver when passing a method as a value, such as a handler. +this.instance.handle(messages.Frobnicate, this.handleFrobnicate.bind(this)); ``` For classes with `static type = "request"` the send method is async and returns a promise that resolves to the response data received from the target it was sent to, or rejects with an error if the request failed. From d9e1829fcf1de89cd1ed981c8053bcb807b1785c Mon Sep 17 00:00:00 2001 From: Ryan Walker <758330+solarcloud7@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:46:21 -0400 Subject: [PATCH 3/3] Human prose.. :D --- docs/writing-plugins.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index 10cc1e64..fb80fef8 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -454,22 +454,23 @@ async frobnicate() { console.log(response); // { report: [...] } } ``` +Because the send and handle methods rely on this, you should always call them directly on the connection object. -The send and handle methods depend on `this`, so call them directly on the connection object. -Extracting one into a variable, destructuring it, or passing it as an unbound callback drops the receiver and the method later throws when called with `this === undefined` (for example `Cannot read properties of undefined (reading 'sendRequest')`). -A type cast around a method that is invoked directly still retains its receiver, but storing or passing the cast result is an unsafe extraction. -When possible, cast the arguments or result instead of the method so the binding remains obvious. -When a method has to be passed as a value, use `.bind()` to fix its receiver first — most commonly when registering handlers. +If you extract them into a variable, destructure them, or pass them as unbound callbacks, they lose their this context. The method will eventually crash with an error like Cannot read properties of undefined (reading 'sendRequest'). + +While it's fine to type cast a direct method invocation, casting and then storing the result causes that same dangerous loss of context. + +Here is what that looks like in practice: ```js -// Broken: the extracted method loses its `this` binding and crashes when called. +// ❌ BAD: The extracted method loses its `this` binding and crashes when called. const send = this.instance.sendTo; send("controller", new messages.Frobnicate({ foo: "bar" })); -// Correct: call it bound on the connection object and cast the arguments/result if needed. +// ✅ GOOD: Call it directly on the connection object. (Cast args/results here if needed). this.instance.sendTo("controller", new messages.Frobnicate({ foo: "bar" })); -// Correct: bind the receiver when passing a method as a value, such as a handler. +// ✅ GOOD: Explicitly bind the receiver if you need to pass a method as a value (like a handler). this.instance.handle(messages.Frobnicate, this.handleFrobnicate.bind(this)); ```