@matter-server/ble-proxy 0.7.2-alpha.0-20260601-e778038 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/BleProxyConnection.d.ts +34 -0
- package/dist/esm/BleProxyConnection.d.ts.map +1 -0
- package/dist/esm/BleProxyConnection.js +210 -0
- package/dist/esm/BleProxyConnection.js.map +6 -0
- package/dist/esm/BleProxyHandler.d.ts +15 -20
- package/dist/esm/BleProxyHandler.d.ts.map +1 -1
- package/dist/esm/BleProxyHandler.js +114 -183
- package/dist/esm/BleProxyHandler.js.map +1 -1
- package/dist/esm/ProxyBleChannel.d.ts.map +1 -1
- package/dist/esm/ProxyBleChannel.js +70 -40
- package/dist/esm/ProxyBleChannel.js.map +1 -1
- package/dist/esm/ProxyBleClient.d.ts.map +1 -1
- package/dist/esm/ProxyBleClient.js +16 -24
- package/dist/esm/ProxyBleClient.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +21 -20
- package/src/BleProxyConnection.ts +282 -0
- package/src/BleProxyHandler.ts +121 -246
- package/src/ProxyBleChannel.ts +92 -56
- package/src/ProxyBleClient.ts +22 -26
- package/src/index.ts +1 -0
package/src/BleProxyHandler.ts
CHANGED
|
@@ -5,79 +5,54 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { WebServerHandler } from "@matter-server/ws-controller";
|
|
8
|
-
import {
|
|
8
|
+
import { Logger, Observable } from "@matter/main";
|
|
9
9
|
import type { createServer } from "node:http";
|
|
10
10
|
import { WebSocket, WebSocketServer } from "ws";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
decodeBinaryFrame,
|
|
14
|
-
encodeBinaryFrame,
|
|
15
|
-
type BinaryFrame,
|
|
16
|
-
type BleProxyCommandMap,
|
|
17
|
-
type BleProxyCommandName,
|
|
18
|
-
type BleProxyEventName,
|
|
19
|
-
type CommandMessage,
|
|
20
|
-
type EventMessage,
|
|
21
|
-
type HelloMessage,
|
|
22
|
-
type ResponseMessage,
|
|
23
|
-
} from "./BleProxyProtocol.js";
|
|
11
|
+
import { BleProxyConnection } from "./BleProxyConnection.js";
|
|
12
|
+
import { BleProxyCommand, BleProxyEvent, type DeviceDiscoveredData, type StartScanArgs } from "./BleProxyProtocol.js";
|
|
24
13
|
|
|
25
14
|
type HttpServer = ReturnType<typeof createServer>;
|
|
26
15
|
|
|
27
16
|
const logger = Logger.get("BleProxyHandler");
|
|
28
17
|
|
|
29
|
-
const HANDSHAKE_TIMEOUT = Seconds(10);
|
|
30
18
|
/**
|
|
31
|
-
*
|
|
32
|
-
* commissioning ops (peripheral connect ≤ 30 s, service discovery on
|
|
33
|
-
* Android-side adapters can take several seconds), short enough that an
|
|
34
|
-
* unresponsive-but-still-connected proxy client doesn't hang commissioning
|
|
35
|
-
* indefinitely.
|
|
36
|
-
*/
|
|
37
|
-
const COMMAND_TIMEOUT = Seconds(60);
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* WebSocket server handler for the /ble BLE proxy endpoint.
|
|
19
|
+
* WebSocket server handler (hub) for the `/ble` BLE proxy endpoint.
|
|
41
20
|
*
|
|
42
|
-
* Accepts
|
|
43
|
-
* and
|
|
44
|
-
*
|
|
21
|
+
* Accepts any number of proxy client connections, broadcasts scan commands to
|
|
22
|
+
* all of them, and tracks which client owns each discovered peripheral so that
|
|
23
|
+
* per-peripheral traffic routes to a single client.
|
|
45
24
|
*/
|
|
46
25
|
export class BleProxyHandler implements WebServerHandler {
|
|
47
26
|
#wss?: WebSocketServer;
|
|
48
27
|
/** Upgrade listener removers, one per HTTP server `register()` call (multi-bind). */
|
|
49
28
|
#removeUpgradeListeners: (() => void)[] = [];
|
|
50
|
-
#
|
|
51
|
-
#handshakeComplete = false;
|
|
52
|
-
#pendingCommands = new Map<
|
|
53
|
-
number,
|
|
54
|
-
{ resolver: (result: Record<string, unknown> | undefined) => void; rejecter: (reason?: unknown) => void }
|
|
55
|
-
>();
|
|
56
|
-
/** Command ID counter. Rolls over at 0xFFFF to stay in safe integer range. */
|
|
57
|
-
#nextCommandId = 0;
|
|
29
|
+
#connections = new Set<BleProxyConnection>();
|
|
58
30
|
#closed = false;
|
|
59
31
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
32
|
+
/** Active scan intent + args, so clients joining mid-scan can be synced. */
|
|
33
|
+
#scanActive = false;
|
|
34
|
+
#scanArgs?: StartScanArgs;
|
|
35
|
+
/** Connections currently told to scan, for aggregate `scanStopped`. */
|
|
36
|
+
#scanning = new Set<BleProxyConnection>();
|
|
62
37
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
38
|
+
/** address -> owning connection + every connection that has seen it. */
|
|
39
|
+
#owners = new Map<string, { owner: BleProxyConnection; seers: Set<BleProxyConnection> }>();
|
|
65
40
|
|
|
66
|
-
/** Emitted
|
|
41
|
+
/** Emitted whenever a connection completes its handshake. */
|
|
67
42
|
readonly connectionEstablished = new Observable<[]>();
|
|
43
|
+
/** Emitted for each `device_discovered`, after ownership is updated. */
|
|
44
|
+
readonly deviceDiscovered = new Observable<[data: DeviceDiscoveredData, connection: BleProxyConnection]>();
|
|
45
|
+
/** Emitted once no connection is scanning anymore. */
|
|
46
|
+
readonly scanStopped = new Observable<[reason: string]>();
|
|
68
47
|
|
|
69
48
|
get connected(): boolean {
|
|
70
|
-
|
|
49
|
+
for (const c of this.#connections) {
|
|
50
|
+
if (c.connected) return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
71
53
|
}
|
|
72
54
|
|
|
73
55
|
async register(server: HttpServer): Promise<void> {
|
|
74
|
-
// Use noServer mode with a path-filtered upgrade listener.
|
|
75
|
-
// ws 8.x calls handleUpgrade unconditionally from its own upgrade listener, which
|
|
76
|
-
// sends HTTP 400 for non-matching paths and destroys the socket — breaking other
|
|
77
|
-
// WebSocket endpoints on the same server.
|
|
78
|
-
//
|
|
79
|
-
// Reuse a single WSS across all bound HTTP servers (multi-listen-address): create
|
|
80
|
-
// it once, then attach a per-server upgrade listener that forwards into it.
|
|
81
56
|
const isFirstBind = this.#wss === undefined;
|
|
82
57
|
const wss = (this.#wss ??= new WebSocketServer({ noServer: true }));
|
|
83
58
|
const upgradeHandler = (
|
|
@@ -111,43 +86,12 @@ export class BleProxyHandler implements WebServerHandler {
|
|
|
111
86
|
return;
|
|
112
87
|
}
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
ws.close(4000, "Only one BLE proxy client allowed");
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
logger.info("BLE proxy client connected, waiting for handshake");
|
|
121
|
-
this.#client = ws;
|
|
122
|
-
this.#handshakeComplete = false;
|
|
123
|
-
|
|
124
|
-
const handshakeTimer = Time.getTimer("BLE proxy handshake timeout", HANDSHAKE_TIMEOUT, () => {
|
|
125
|
-
if (!this.#handshakeComplete) {
|
|
126
|
-
logger.warn("BLE proxy handshake timeout - closing connection");
|
|
127
|
-
ws.close(4001, "Handshake timeout");
|
|
128
|
-
this.#cleanupClient();
|
|
129
|
-
}
|
|
130
|
-
}).start();
|
|
89
|
+
const connection = new BleProxyConnection(ws);
|
|
90
|
+
this.#connections.add(connection);
|
|
131
91
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
} else {
|
|
136
|
-
this.#handleTextMessage(data.toString(), handshakeTimer);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
ws.on("close", () => {
|
|
141
|
-
handshakeTimer.stop();
|
|
142
|
-
logger.info("BLE proxy client disconnected");
|
|
143
|
-
this.#cleanupClient();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
ws.on("error", err => {
|
|
147
|
-
logger.error("BLE proxy WebSocket error:", err);
|
|
148
|
-
handshakeTimer.stop();
|
|
149
|
-
this.#cleanupClient();
|
|
150
|
-
});
|
|
92
|
+
connection.handshakeCompleted.on(() => this.#onHandshakeCompleted(connection));
|
|
93
|
+
connection.eventReceived.on((event, data) => this.#onConnectionEvent(connection, event, data));
|
|
94
|
+
connection.closed.on(() => this.#onConnectionClosed(connection));
|
|
151
95
|
});
|
|
152
96
|
}
|
|
153
97
|
|
|
@@ -160,204 +104,135 @@ export class BleProxyHandler implements WebServerHandler {
|
|
|
160
104
|
for (const remove of this.#removeUpgradeListeners) remove();
|
|
161
105
|
this.#removeUpgradeListeners = [];
|
|
162
106
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.#client.close();
|
|
107
|
+
for (const connection of this.#connections) {
|
|
108
|
+
connection.close();
|
|
166
109
|
}
|
|
167
|
-
this.#
|
|
110
|
+
this.#connections.clear();
|
|
111
|
+
this.#scanning.clear();
|
|
112
|
+
this.#owners.clear();
|
|
168
113
|
|
|
169
114
|
const wss = this.#wss;
|
|
170
115
|
this.#wss = undefined;
|
|
171
116
|
|
|
172
|
-
// Close all clients connected to the WSS (safety net)
|
|
173
117
|
wss.clients.forEach(client => {
|
|
174
118
|
if (client.readyState === WebSocket.OPEN) {
|
|
175
119
|
client.close();
|
|
176
120
|
}
|
|
177
121
|
});
|
|
178
122
|
|
|
179
|
-
// Wait for the WebSocket server to close properly
|
|
180
123
|
return new Promise<void>((resolve, reject) => {
|
|
181
|
-
wss.close(err =>
|
|
182
|
-
if (err) {
|
|
183
|
-
reject(err);
|
|
184
|
-
} else {
|
|
185
|
-
resolve();
|
|
186
|
-
}
|
|
187
|
-
});
|
|
124
|
+
wss.close(err => (err ? reject(err) : resolve()));
|
|
188
125
|
});
|
|
189
126
|
}
|
|
190
127
|
|
|
191
|
-
/**
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
async sendCommand<C extends BleProxyCommandName>(
|
|
197
|
-
command: C,
|
|
198
|
-
...rest: BleProxyCommandMap[C]["args"] extends undefined ? [] : [args: BleProxyCommandMap[C]["args"]]
|
|
199
|
-
): Promise<BleProxyCommandMap[C]["result"]> {
|
|
200
|
-
if (!this.connected || !this.#client) {
|
|
201
|
-
throw new Error("BLE proxy client not connected");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const args = rest[0];
|
|
205
|
-
const id = this.#nextCommandId;
|
|
206
|
-
this.#nextCommandId = (this.#nextCommandId + 1) & 0xffff;
|
|
207
|
-
const message: CommandMessage = { id, command };
|
|
208
|
-
if (args !== undefined) {
|
|
209
|
-
message.args = args as Record<string, unknown>;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const { promise, resolver, rejecter } = createPromise<Record<string, unknown> | undefined>();
|
|
213
|
-
this.#pendingCommands.set(id, { resolver, rejecter });
|
|
214
|
-
|
|
215
|
-
this.#client.send(JSON.stringify(message), err => {
|
|
216
|
-
if (err) {
|
|
217
|
-
this.#pendingCommands.delete(id);
|
|
218
|
-
rejecter(new Error(`Failed to send command ${command}: ${err.message}`));
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
return withTimeout(COMMAND_TIMEOUT, promise, () => this.#pendingCommands.delete(id)) as Promise<
|
|
223
|
-
BleProxyCommandMap[C]["result"]
|
|
224
|
-
>;
|
|
128
|
+
/** Returns the connection that owns `address`, if it is still connected. */
|
|
129
|
+
getOwner(address: string): BleProxyConnection | undefined {
|
|
130
|
+
const entry = this.#owners.get(address);
|
|
131
|
+
if (entry && entry.owner.connected) return entry.owner;
|
|
132
|
+
return undefined;
|
|
225
133
|
}
|
|
226
134
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
135
|
+
async startScan(args: StartScanArgs): Promise<void> {
|
|
136
|
+
this.#scanActive = true;
|
|
137
|
+
this.#scanArgs = args;
|
|
138
|
+
const sends = new Array<Promise<unknown>>();
|
|
139
|
+
for (const connection of this.#connections) {
|
|
140
|
+
if (connection.connected) {
|
|
141
|
+
this.#scanning.add(connection);
|
|
142
|
+
sends.push(
|
|
143
|
+
connection.sendCommand(BleProxyCommand.StartScan, args).catch(err => {
|
|
144
|
+
logger.warn(`[${connection.id}] Failed to start scan:`, err);
|
|
145
|
+
this.#markNotScanning(connection, "start scan failed");
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
233
149
|
}
|
|
234
|
-
|
|
235
|
-
this.#client.send(frame);
|
|
150
|
+
await Promise.all(sends);
|
|
236
151
|
}
|
|
237
152
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Handle handshake
|
|
250
|
-
if (!this.#handshakeComplete) {
|
|
251
|
-
this.#handleHandshake(msg, handshakeTimer);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Handle response to a pending command
|
|
256
|
-
if ("id" in msg && "success" in msg) {
|
|
257
|
-
this.#handleResponse(msg as unknown as ResponseMessage);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Handle event
|
|
262
|
-
if ("event" in msg && "data" in msg) {
|
|
263
|
-
this.#handleEvent(msg as unknown as EventMessage);
|
|
264
|
-
return;
|
|
153
|
+
async stopScan(): Promise<void> {
|
|
154
|
+
this.#scanActive = false;
|
|
155
|
+
const sends = new Array<Promise<unknown>>();
|
|
156
|
+
for (const connection of this.#connections) {
|
|
157
|
+
if (connection.connected) {
|
|
158
|
+
sends.push(
|
|
159
|
+
connection
|
|
160
|
+
.sendCommand(BleProxyCommand.StopScan)
|
|
161
|
+
.catch(err => logger.warn(`[${connection.id}] Failed to stop scan:`, err)),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
265
164
|
}
|
|
266
|
-
|
|
267
|
-
|
|
165
|
+
this.#scanning.clear();
|
|
166
|
+
await Promise.all(sends);
|
|
268
167
|
}
|
|
269
168
|
|
|
270
|
-
#
|
|
271
|
-
if (msg.type !== "hello") {
|
|
272
|
-
logger.warn(`Expected hello message, got: ${JSON.stringify(msg)}`);
|
|
273
|
-
this.#client?.close(4002, "Expected hello message");
|
|
274
|
-
this.#cleanupClient();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const hello = msg as unknown as HelloMessage;
|
|
279
|
-
handshakeTimer.stop();
|
|
280
|
-
|
|
281
|
-
if (hello.version !== BLE_PROXY_PROTOCOL_VERSION) {
|
|
282
|
-
logger.warn(
|
|
283
|
-
`BLE proxy client version ${hello.version} not supported (server supports ${BLE_PROXY_PROTOCOL_VERSION})`,
|
|
284
|
-
);
|
|
285
|
-
this.#client?.send(
|
|
286
|
-
JSON.stringify({
|
|
287
|
-
type: "hello_response",
|
|
288
|
-
version: BLE_PROXY_PROTOCOL_VERSION,
|
|
289
|
-
error: "unsupported_version",
|
|
290
|
-
message: `Server supports protocol version ${BLE_PROXY_PROTOCOL_VERSION}, client sent version ${hello.version}`,
|
|
291
|
-
}),
|
|
292
|
-
);
|
|
293
|
-
this.#client?.close(4003, "Unsupported protocol version");
|
|
294
|
-
this.#cleanupClient();
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
this.#handshakeComplete = true;
|
|
299
|
-
this.#client?.send(
|
|
300
|
-
JSON.stringify({
|
|
301
|
-
type: "hello_response",
|
|
302
|
-
version: BLE_PROXY_PROTOCOL_VERSION,
|
|
303
|
-
}),
|
|
304
|
-
);
|
|
305
|
-
logger.info(`BLE proxy handshake complete (version ${BLE_PROXY_PROTOCOL_VERSION})`);
|
|
169
|
+
#onHandshakeCompleted(connection: BleProxyConnection): void {
|
|
306
170
|
this.connectionEstablished.emit();
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
this.#pendingCommands.delete(msg.id);
|
|
316
|
-
|
|
317
|
-
if (msg.success) {
|
|
318
|
-
pending.resolver(msg.result);
|
|
319
|
-
} else {
|
|
320
|
-
pending.rejecter(new Error(`${msg.error}: ${msg.message}`));
|
|
171
|
+
if (this.#scanActive && this.#scanArgs) {
|
|
172
|
+
this.#scanning.add(connection);
|
|
173
|
+
connection.sendCommand(BleProxyCommand.StartScan, this.#scanArgs).catch(err => {
|
|
174
|
+
logger.warn(`[${connection.id}] Failed to sync scan to joining client:`, err);
|
|
175
|
+
this.#markNotScanning(connection, "start scan failed");
|
|
176
|
+
});
|
|
321
177
|
}
|
|
322
178
|
}
|
|
323
179
|
|
|
324
|
-
#
|
|
325
|
-
|
|
180
|
+
#onConnectionEvent(connection: BleProxyConnection, event: string, data: Record<string, unknown>): void {
|
|
181
|
+
if (event === BleProxyEvent.DeviceDiscovered) {
|
|
182
|
+
this.#onDeviceDiscovered(connection, data as unknown as DeviceDiscoveredData);
|
|
183
|
+
} else if (event === BleProxyEvent.ScanStopped) {
|
|
184
|
+
this.#markNotScanning(connection, (data as { reason?: string }).reason ?? "unknown");
|
|
185
|
+
}
|
|
326
186
|
}
|
|
327
187
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
188
|
+
/** Drop a connection from the scanning set; emit aggregate scanStopped once none remain. */
|
|
189
|
+
#markNotScanning(connection: BleProxyConnection, reason: string): void {
|
|
190
|
+
if (!this.#scanning.delete(connection)) {
|
|
331
191
|
return;
|
|
332
192
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const frame = decodeBinaryFrame(new Uint8Array(data));
|
|
336
|
-
const head = Array.from(frame.payload.subarray(0, 8))
|
|
337
|
-
.map(b => b.toString(16).padStart(2, "0"))
|
|
338
|
-
.join("");
|
|
339
|
-
logger.debug(
|
|
340
|
-
`[FRAME] opcode=${frame.opcode} handle=${frame.connectionHandle} len=${frame.payload.length} head=${head}`,
|
|
341
|
-
);
|
|
342
|
-
this.binaryFrameReceived.emit(frame);
|
|
343
|
-
} catch (err) {
|
|
344
|
-
logger.error("Failed to decode binary frame:", err);
|
|
193
|
+
if (this.#scanning.size === 0) {
|
|
194
|
+
this.scanStopped.emit(reason);
|
|
345
195
|
}
|
|
346
196
|
}
|
|
347
197
|
|
|
348
|
-
#
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
198
|
+
#onDeviceDiscovered(connection: BleProxyConnection, data: DeviceDiscoveredData): void {
|
|
199
|
+
let entry = this.#owners.get(data.address);
|
|
200
|
+
if (!entry) {
|
|
201
|
+
entry = { owner: connection, seers: new Set([connection]) };
|
|
202
|
+
this.#owners.set(data.address, entry);
|
|
203
|
+
} else {
|
|
204
|
+
entry.seers.add(connection);
|
|
205
|
+
if (!entry.owner.connected) {
|
|
206
|
+
entry.owner = connection;
|
|
352
207
|
}
|
|
353
|
-
this.#client = undefined;
|
|
354
208
|
}
|
|
355
|
-
|
|
209
|
+
logger.debug(
|
|
210
|
+
`[${connection.id}] device_discovered ${data.address} rssi=${data.rssi ?? "n/a"} seers=${entry.seers.size} isOwner=${entry.owner === connection}`,
|
|
211
|
+
);
|
|
212
|
+
this.deviceDiscovered.emit(data, connection);
|
|
213
|
+
}
|
|
356
214
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
215
|
+
#onConnectionClosed(connection: BleProxyConnection): void {
|
|
216
|
+
this.#connections.delete(connection);
|
|
217
|
+
this.#markNotScanning(connection, "client disconnected");
|
|
218
|
+
|
|
219
|
+
for (const [address, entry] of this.#owners) {
|
|
220
|
+
entry.seers.delete(connection);
|
|
221
|
+
if (entry.owner === connection) {
|
|
222
|
+
let next: BleProxyConnection | undefined;
|
|
223
|
+
for (const seer of entry.seers) {
|
|
224
|
+
if (seer.connected) {
|
|
225
|
+
next = seer;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (next) {
|
|
230
|
+
entry.owner = next;
|
|
231
|
+
logger.info(`Reassigned ownership of ${address} to [${next.id}]`);
|
|
232
|
+
} else {
|
|
233
|
+
this.#owners.delete(address);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
361
236
|
}
|
|
362
237
|
}
|
|
363
238
|
}
|