@mcesystems/usb-device-listener 1.0.78 → 1.0.79

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/README.md CHANGED
@@ -114,6 +114,43 @@ usb:detail
114
114
  usb:detail ================================================================================
115
115
  ```
116
116
 
117
+ ## Application ID registry (multiple configs)
118
+
119
+ When multiple parts of the same process need **separate** USB device filters and callbacks (e.g. one app for Samsung devices, another for Arduino), use `UsbDeviceListenerRegistry`. You register an application ID with a config, get a scoped listener for that ID, and only receive events that match that app's config. One native listener is shared; the registry fans out events per app.
120
+
121
+ **When to use:**
122
+ - Multiple logical "apps" or modules in one process, each with its own `ListenerConfig`
123
+ - You want each app to call `onDeviceAdd` / `onDeviceRemove` and only see devices matching its filters
124
+ - You want `listDevices()` per app to return only devices that match that app's config
125
+
126
+ **Example:** See [src/examples/example-registry.ts](src/examples/example-registry.ts). Run with `pnpm example:registry`.
127
+
128
+ ```javascript
129
+ import { UsbDeviceListenerRegistry } from "@mcesystems/usb-device-listener";
130
+
131
+ const registry = new UsbDeviceListenerRegistry();
132
+ registry.register("app-samsung", { targetDevices: [{ vid: "04E8", pid: "6860" }] });
133
+ registry.register("app-arduino", { targetDevices: [{ vid: "2341", pid: "0043" }] });
134
+
135
+ const listenerSamsung = registry.getListener("app-samsung");
136
+ const listenerArduino = registry.getListener("app-arduino");
137
+
138
+ listenerSamsung.onDeviceAdd((device) => { /* only Samsung devices */ });
139
+ listenerArduino.onDeviceAdd((device) => { /* only Arduino devices */ });
140
+
141
+ listenerSamsung.startListening({ targetDevices: [{ vid: "04E8", pid: "6860" }] });
142
+ listenerArduino.startListening({ targetDevices: [{ vid: "2341", pid: "0043" }] });
143
+
144
+ // Later: unregister and stop native listener when last app unregisters
145
+ registry.unregister("app-samsung");
146
+ registry.unregister("app-arduino");
147
+ ```
148
+
149
+ **Registry API:**
150
+ - `register(appId, config)` — Register or update config for an application ID.
151
+ - `getListener(appId)` — Return a listener (implements `UsbDeviceListenerI`) scoped to that app; events and `listDevices()` are filtered by the app's config.
152
+ - `unregister(appId)` — Remove the app and stop the native listener if it was the last one started.
153
+
117
154
  ## API Reference
118
155
 
119
156
  ### `startListening(config)`
@@ -1 +1 @@
1
- {"version":3,"file":"device-filter.d.ts","sourceRoot":"","sources":["../src/device-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AAoDxE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAuCtF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,UAAU,CA2B9F"}
1
+ {"version":3,"file":"device-filter.d.ts","sourceRoot":"","sources":["../src/device-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AAoDxE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CA6CtF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,UAAU,CA2B9F"}
@@ -1,10 +1,10 @@
1
1
  import type { DeviceInfo } from "./types.js";
2
2
  /**
3
3
  * Format device port path for display (e.g. "2/3/1" for port 2, then 3, then 1).
4
- * When portPath is available, returns slash-separated port numbers; otherwise returns locationInfo.
4
+ * Uses native portPath when available; otherwise returns locationInfo.
5
5
  *
6
6
  * @param device - Device info from the listener
7
- * @returns Port path string (e.g. "2/3/1") or locationInfo fallback
7
+ * @returns Port path string (e.g. "2/3/1") or locationInfo when portPath is missing
8
8
  */
9
9
  export declare function formatPortPath(device: DeviceInfo): string;
10
10
  //# sourceMappingURL=formatPortPath.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,11 +1,4 @@
1
- import type { DeviceAddCallback, DeviceInfo, DeviceRemoveCallback, ListenerConfig, UsbDeviceListenerI } from "./types";
2
- export interface NativeAddon {
3
- startListening(): void;
4
- stopListening(): void;
5
- onDeviceAdd(callback: DeviceAddCallback): void;
6
- onDeviceRemove(callback: DeviceRemoveCallback): void;
7
- listDevices(): DeviceInfo[];
8
- }
1
+ import type { DeviceAddCallback, DeviceInfo, DeviceRemoveCallback, ListenerConfig, NativeAddon, UsbDeviceListenerI } from "./types";
9
2
  /**
10
3
  * USB Device Listener implementation
11
4
  * Provides a type-safe wrapper around the native C++ addon
@@ -50,5 +43,6 @@ export default class UsbDeviceListener implements UsbDeviceListenerI {
50
43
  listDevices(): DeviceInfo[];
51
44
  }
52
45
  export { formatPortPath } from "./formatPortPath.js";
53
- export type { DeviceInfo, DeviceAddCallback, DeviceRemoveCallback, ListenerConfig, TargetDevice, } from "./types";
46
+ export { UsbDeviceListenerRegistry } from "./registry.js";
47
+ export type { DeviceInfo, DeviceAddCallback, DeviceRemoveCallback, ListenerConfig, NativeAddon, TargetDevice, UsbDeviceListenerRegistryI, } from "./types";
54
48
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,WAAW;IAC3B,cAAc,IAAI,IAAI,CAAC;IACvB,aAAa,IAAI,IAAI,CAAC;IACtB,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC/C,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrD,WAAW,IAAI,UAAU,EAAE,CAAC;CAC5B;AAUD;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAkB,YAAW,kBAAkB;IACnE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,kBAAkB,CAAqC;gBAEnD,KAAK,CAAC,EAAE,WAAW;IAI/B;;OAEG;IACI,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAQnD;;OAEG;IACI,aAAa,IAAI,IAAI;IAI5B;;;;OAIG;IACI,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAOjD;;;OAGG;IACI,gBAAgB,IAAI,cAAc;IAIzC;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAerD;;OAEG;IACI,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAe3D;;;OAGG;IACI,WAAW,IAAI,UAAU,EAAE;CAKlC;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EACX,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,YAAY,GACZ,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,MAAM,SAAS,CAAC;AAUjB;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAkB,YAAW,kBAAkB;IACnE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,kBAAkB,CAAqC;gBAEnD,KAAK,CAAC,EAAE,WAAW;IAI/B;;OAEG;IACI,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAQnD;;OAEG;IACI,aAAa,IAAI,IAAI;IAI5B;;;;OAIG;IACI,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAOjD;;;OAGG;IACI,gBAAgB,IAAI,cAAc;IAIzC;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAerD;;OAEG;IACI,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAe3D;;;OAGG;IACI,WAAW,IAAI,UAAU,EAAE;CAKlC;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,YAAY,EACX,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,YAAY,EACZ,0BAA0B,GAC1B,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -31,13 +31,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
+ UsbDeviceListenerRegistry: () => UsbDeviceListenerRegistry,
34
35
  default: () => UsbDeviceListener,
35
36
  formatPortPath: () => formatPortPath
36
37
  });
37
38
  module.exports = __toCommonJS(index_exports);
38
- var import_node_module = require("node:module");
39
- var import_node_path = __toESM(require("node:path"));
40
- var import_node_url = require("node:url");
39
+ var import_node_module2 = require("node:module");
40
+ var import_node_path2 = __toESM(require("node:path"));
41
+ var import_node_url2 = require("node:url");
41
42
 
42
43
  // src/device-filter.ts
43
44
  function toHexString(value) {
@@ -61,12 +62,12 @@ function isMappedInLogicalPortMapByHub(device, config) {
61
62
  if (!map || Object.keys(map).length === 0) return false;
62
63
  const vid = device.parentHubVid;
63
64
  const pid = device.parentHubPid;
64
- const path2 = device.portPath;
65
- if (vid === void 0 || pid === void 0 || !path2 || path2.length === 0) return false;
65
+ const path3 = device.portPath;
66
+ if (vid === void 0 || pid === void 0 || !path3 || path3.length === 0) return false;
66
67
  const key = hubKeyFromParent(vid, pid);
67
68
  const portMap = map[key];
68
69
  if (!portMap) return false;
69
- const physicalPort = path2[path2.length - 1];
70
+ const physicalPort = path3[path3.length - 1];
70
71
  return physicalPort in portMap;
71
72
  }
72
73
  function shouldNotifyDevice(device, config) {
@@ -130,7 +131,10 @@ function formatPortPath(device) {
130
131
  return device.locationInfo;
131
132
  }
132
133
 
133
- // src/index.ts
134
+ // src/registry.ts
135
+ var import_node_module = require("node:module");
136
+ var import_node_path = __toESM(require("node:path"));
137
+ var import_node_url = require("node:url");
134
138
  function loadDefaultAddon() {
135
139
  const require2 = (0, import_node_module.createRequire)(__importMetaUrl);
136
140
  const __filename = (0, import_node_url.fileURLToPath)(__importMetaUrl);
@@ -138,13 +142,173 @@ function loadDefaultAddon() {
138
142
  const packageRoot = import_node_path.default.join(__dirname, "..");
139
143
  return require2("node-gyp-build")(packageRoot);
140
144
  }
145
+ function ensureAddonCallbacksInstalled(addon, apps) {
146
+ addon.onDeviceAdd((device) => {
147
+ for (const entry of apps.values()) {
148
+ if (!entry.started || !entry.addCallback) continue;
149
+ if (shouldNotifyDevice(device, entry.config)) {
150
+ const mapped = applyLogicalPortMapping(device, entry.config);
151
+ entry.addCallback(mapped);
152
+ }
153
+ }
154
+ });
155
+ addon.onDeviceRemove((device) => {
156
+ for (const entry of apps.values()) {
157
+ if (!entry.started || !entry.removeCallback) continue;
158
+ if (shouldNotifyDevice(device, entry.config)) {
159
+ const mapped = applyLogicalPortMapping(device, entry.config);
160
+ entry.removeCallback(mapped);
161
+ }
162
+ }
163
+ });
164
+ }
165
+ var ScopedListener = class {
166
+ constructor(registry, appId) {
167
+ this.registry = registry;
168
+ this.appId = appId;
169
+ }
170
+ getEntry() {
171
+ const entry = this.registry.getEntry(this.appId);
172
+ if (!entry) {
173
+ throw new Error(`App not registered: ${this.appId}`);
174
+ }
175
+ return entry;
176
+ }
177
+ startListening(config) {
178
+ if (typeof config !== "object" || config === null) {
179
+ throw new TypeError("Config must be an object");
180
+ }
181
+ const entry = this.getEntry();
182
+ entry.config = config;
183
+ entry.started = true;
184
+ this.registry.ensureNativeListenerRunning();
185
+ }
186
+ stopListening() {
187
+ const entry = this.getEntry();
188
+ entry.started = false;
189
+ this.registry.maybeStopNativeListener();
190
+ }
191
+ updateConfig(config) {
192
+ if (typeof config !== "object" || config === null) {
193
+ throw new TypeError("Config must be an object");
194
+ }
195
+ this.getEntry().config = config;
196
+ }
197
+ getCurrentConfig() {
198
+ return { ...this.getEntry().config };
199
+ }
200
+ onDeviceAdd(callback) {
201
+ if (typeof callback !== "function") {
202
+ throw new TypeError("Callback must be a function");
203
+ }
204
+ this.getEntry().addCallback = callback;
205
+ this.registry.ensureAddonCallbacksInstalled();
206
+ }
207
+ onDeviceRemove(callback) {
208
+ if (typeof callback !== "function") {
209
+ throw new TypeError("Callback must be a function");
210
+ }
211
+ this.getEntry().removeCallback = callback;
212
+ this.registry.ensureAddonCallbacksInstalled();
213
+ }
214
+ listDevices() {
215
+ const config = this.getEntry().config;
216
+ const devices = this.registry.listDevicesFromAddon();
217
+ return devices.filter((device) => shouldNotifyDevice(device, config)).map((device) => applyLogicalPortMapping(device, config));
218
+ }
219
+ };
220
+ var UsbDeviceListenerRegistry = class {
221
+ apps = /* @__PURE__ */ new Map();
222
+ addon;
223
+ addonCallbacksInstalled = false;
224
+ nativeListenerRunning = false;
225
+ scopedListeners = /* @__PURE__ */ new Map();
226
+ constructor(addon) {
227
+ this.addon = addon ?? loadDefaultAddon();
228
+ }
229
+ getEntry(appId) {
230
+ return this.apps.get(appId);
231
+ }
232
+ ensureNativeListenerRunning() {
233
+ if (this.nativeListenerRunning) return;
234
+ this.nativeListenerRunning = true;
235
+ this.addon.startListening();
236
+ }
237
+ maybeStopNativeListener() {
238
+ const anyStarted = [...this.apps.values()].some((e) => e.started);
239
+ if (!anyStarted && this.nativeListenerRunning) {
240
+ this.nativeListenerRunning = false;
241
+ this.addon.stopListening();
242
+ }
243
+ }
244
+ ensureAddonCallbacksInstalled() {
245
+ if (this.addonCallbacksInstalled) return;
246
+ this.addonCallbacksInstalled = true;
247
+ ensureAddonCallbacksInstalled(this.addon, this.apps);
248
+ }
249
+ listDevicesFromAddon() {
250
+ return this.addon.listDevices();
251
+ }
252
+ register(appId, config) {
253
+ if (typeof config !== "object" || config === null) {
254
+ throw new TypeError("Config must be an object");
255
+ }
256
+ const existing = this.apps.get(appId);
257
+ if (existing) {
258
+ existing.config = config;
259
+ return;
260
+ }
261
+ this.apps.set(appId, {
262
+ config: { ...config },
263
+ addCallback: null,
264
+ removeCallback: null,
265
+ started: false
266
+ });
267
+ }
268
+ getListener(appId) {
269
+ if (!this.apps.has(appId)) {
270
+ this.apps.set(appId, {
271
+ config: {},
272
+ addCallback: null,
273
+ removeCallback: null,
274
+ started: false
275
+ });
276
+ }
277
+ let scoped = this.scopedListeners.get(appId);
278
+ if (!scoped) {
279
+ scoped = new ScopedListener(this, appId);
280
+ this.scopedListeners.set(appId, scoped);
281
+ }
282
+ return scoped;
283
+ }
284
+ unregister(appId) {
285
+ this.scopedListeners.delete(appId);
286
+ const entry = this.apps.get(appId);
287
+ if (entry) {
288
+ entry.started = false;
289
+ entry.addCallback = null;
290
+ entry.removeCallback = null;
291
+ this.apps.delete(appId);
292
+ }
293
+ this.maybeStopNativeListener();
294
+ }
295
+ };
296
+
297
+ // src/index.ts
298
+ function loadDefaultAddon2() {
299
+ const require2 = (0, import_node_module2.createRequire)(__importMetaUrl);
300
+ const __filename = (0, import_node_url2.fileURLToPath)(__importMetaUrl);
301
+ const __dirname = import_node_path2.default.dirname(__filename);
302
+ const packageRoot = import_node_path2.default.join(__dirname, "..");
303
+ return require2("node-gyp-build")(packageRoot);
304
+ }
141
305
  var UsbDeviceListener = class {
142
306
  addon;
143
307
  config = {};
144
308
  userAddCallback = null;
145
309
  userRemoveCallback = null;
146
310
  constructor(addon) {
147
- this.addon = addon ?? loadDefaultAddon();
311
+ this.addon = addon ?? loadDefaultAddon2();
148
312
  }
149
313
  /**
150
314
  * Start listening for USB device events
@@ -221,6 +385,7 @@ var UsbDeviceListener = class {
221
385
  };
222
386
  // Annotate the CommonJS export names for ESM import in node:
223
387
  0 && (module.exports = {
388
+ UsbDeviceListenerRegistry,
224
389
  formatPortPath
225
390
  });
226
391
  module.exports = Object.assign(module.exports.default, module.exports);
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.ts", "../src/device-filter.ts", "../src/formatPortPath.ts"],
4
- "sourcesContent": ["import { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tUsbDeviceListenerI,\n} from \"./types\";\n\nexport interface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\n\t}\n\n\t/**\n\t * Register callback for device connection events\n\t */\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userAddCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceAdd((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Register callback for device disconnection events\n\t */\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userRemoveCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport { formatPortPath } from \"./formatPortPath.js\";\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tTargetDevice,\n} from \"./types\";\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Build hub key \"${vid}-${pid}\" from device's parent hub (uppercase 4-char hex).\n */\nfunction hubKeyFromParent(vid: number, pid: number): string {\n\treturn `${vid.toString(16).toUpperCase().padStart(4, \"0\")}-${pid.toString(16).toUpperCase().padStart(4, \"0\")}`;\n}\n\n/**\n * True if device has parent hub data and is mapped in logicalPortMapByHub.\n */\nfunction isMappedInLogicalPortMapByHub(device: DeviceInfo, config: ListenerConfig): boolean {\n\tconst map = config.logicalPortMapByHub;\n\tif (!map || Object.keys(map).length === 0) return false;\n\tconst vid = device.parentHubVid;\n\tconst pid = device.parentHubPid;\n\tconst path = device.portPath;\n\tif (vid === undefined || pid === undefined || !path || path.length === 0) return false;\n\tconst key = hubKeyFromParent(vid, pid);\n\tconst portMap = map[key];\n\tif (!portMap) return false;\n\tconst physicalPort = path[path.length - 1];\n\treturn physicalPort in portMap;\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap / logicalPortMapByHub - if specified, device must be in the map (hub map takes precedence when device has hub data)\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap / logicalPortMapByHub (if specified, device must be mapped)\n\tconst hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;\n\tconst hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;\n\tif (hasHubMap && device.parentHubVid !== undefined && device.parentHubPid !== undefined && device.portPath && device.portPath.length > 0) {\n\t\tif (!isMappedInLogicalPortMapByHub(device, config)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasLocationMap && config.logicalPortMap) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasHubMap) {\n\t\t// logicalPortMapByHub is set but device has no hub data - cannot be in map\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured.\n * logicalPortMapByHub takes precedence when device has parent hub and port path.\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tconst mapByHub = config.logicalPortMapByHub;\n\tif (\n\t\tmapByHub &&\n\t\tObject.keys(mapByHub).length > 0 &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tconst key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);\n\t\tconst portMap = mapByHub[key];\n\t\tconst physicalPort = device.portPath[device.portPath.length - 1];\n\t\tif (portMap && physicalPort in portMap) {\n\t\t\treturn {\n\t\t\t\t...device,\n\t\t\t\tlogicalPort: portMap[physicalPort],\n\t\t\t};\n\t\t}\n\t}\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n", "import type { DeviceInfo } from \"./types.js\";\n\n/**\n * Format device port path for display (e.g. \"2/3/1\" for port 2, then 3, then 1).\n * When portPath is available, returns slash-separated port numbers; otherwise returns locationInfo.\n *\n * @param device - Device info from the listener\n * @returns Port path string (e.g. \"2/3/1\") or locationInfo fallback\n */\nexport function formatPortPath(device: DeviceInfo): string {\n\tif (device.portPath && device.portPath.length > 0) {\n\t\treturn device.portPath.join(\"/\");\n\t}\n\treturn device.locationInfo;\n}\n"],
5
- "mappings": "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",
6
- "names": ["path", "require", "path"]
3
+ "sources": ["../src/index.ts", "../src/device-filter.ts", "../src/formatPortPath.ts", "../src/registry.ts"],
4
+ "sourcesContent": ["import { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tUsbDeviceListenerI,\n} from \"./types\";\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\n\t}\n\n\t/**\n\t * Register callback for device connection events\n\t */\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userAddCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceAdd((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Register callback for device disconnection events\n\t */\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userRemoveCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport { formatPortPath } from \"./formatPortPath.js\";\nexport { UsbDeviceListenerRegistry } from \"./registry.js\";\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tTargetDevice,\n\tUsbDeviceListenerRegistryI,\n} from \"./types\";\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Build hub key \"${vid}-${pid}\" from device's parent hub (uppercase 4-char hex).\n */\nfunction hubKeyFromParent(vid: number, pid: number): string {\n\treturn `${vid.toString(16).toUpperCase().padStart(4, \"0\")}-${pid.toString(16).toUpperCase().padStart(4, \"0\")}`;\n}\n\n/**\n * True if device has parent hub data and is mapped in logicalPortMapByHub.\n */\nfunction isMappedInLogicalPortMapByHub(device: DeviceInfo, config: ListenerConfig): boolean {\n\tconst map = config.logicalPortMapByHub;\n\tif (!map || Object.keys(map).length === 0) return false;\n\tconst vid = device.parentHubVid;\n\tconst pid = device.parentHubPid;\n\tconst path = device.portPath;\n\tif (vid === undefined || pid === undefined || !path || path.length === 0) return false;\n\tconst key = hubKeyFromParent(vid, pid);\n\tconst portMap = map[key];\n\tif (!portMap) return false;\n\tconst physicalPort = path[path.length - 1];\n\treturn physicalPort in portMap;\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap / logicalPortMapByHub - if specified, device must be in the map (hub map takes precedence when device has hub data)\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap / logicalPortMapByHub (if specified, device must be mapped)\n\tconst hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;\n\tconst hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;\n\tif (\n\t\thasHubMap &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tif (!isMappedInLogicalPortMapByHub(device, config)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasLocationMap && config.logicalPortMap) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasHubMap) {\n\t\t// logicalPortMapByHub is set but device has no hub data - cannot be in map\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured.\n * logicalPortMapByHub takes precedence when device has parent hub and port path.\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tconst mapByHub = config.logicalPortMapByHub;\n\tif (\n\t\tmapByHub &&\n\t\tObject.keys(mapByHub).length > 0 &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tconst key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);\n\t\tconst portMap = mapByHub[key];\n\t\tconst physicalPort = device.portPath[device.portPath.length - 1];\n\t\tif (portMap && physicalPort in portMap) {\n\t\t\treturn {\n\t\t\t\t...device,\n\t\t\t\tlogicalPort: portMap[physicalPort],\n\t\t\t};\n\t\t}\n\t}\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n", "import type { DeviceInfo } from \"./types.js\";\n\n/**\n * Format device port path for display (e.g. \"2/3/1\" for port 2, then 3, then 1).\n * Uses native portPath when available; otherwise returns locationInfo.\n *\n * @param device - Device info from the listener\n * @returns Port path string (e.g. \"2/3/1\") or locationInfo when portPath is missing\n */\nexport function formatPortPath(device: DeviceInfo): string {\n\tif (device.portPath && device.portPath.length > 0) {\n\t\treturn device.portPath.join(\"/\");\n\t}\n\treturn device.locationInfo;\n}\n", "import { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tUsbDeviceListenerI,\n\tUsbDeviceListenerRegistryI,\n} from \"./types\";\n\ninterface AppEntry {\n\tconfig: ListenerConfig;\n\taddCallback: DeviceAddCallback | null;\n\tremoveCallback: DeviceRemoveCallback | null;\n\tstarted: boolean;\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\nfunction ensureAddonCallbacksInstalled(addon: NativeAddon, apps: Map<string, AppEntry>): void {\n\t// Install once: single fan-out to all registered apps\n\taddon.onDeviceAdd((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.addCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.addCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n\taddon.onDeviceRemove((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.removeCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.removeCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n}\n\n/**\n * Scoped listener that delegates to the registry and uses one app's config.\n */\nclass ScopedListener implements UsbDeviceListenerI {\n\tconstructor(\n\t\tprivate readonly registry: UsbDeviceListenerRegistry,\n\t\tprivate readonly appId: string\n\t) {}\n\n\tprivate getEntry(): AppEntry {\n\t\tconst entry = this.registry.getEntry(this.appId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`App not registered: ${this.appId}`);\n\t\t}\n\t\treturn entry;\n\t}\n\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tconst entry = this.getEntry();\n\t\tentry.config = config;\n\t\tentry.started = true;\n\t\tthis.registry.ensureNativeListenerRunning();\n\t}\n\n\tpublic stopListening(): void {\n\t\tconst entry = this.getEntry();\n\t\tentry.started = false;\n\t\tthis.registry.maybeStopNativeListener();\n\t}\n\n\tpublic updateConfig(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.getEntry().config = config;\n\t}\n\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.getEntry().config };\n\t}\n\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.getEntry().addCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.getEntry().removeCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst config = this.getEntry().config;\n\t\tconst devices = this.registry.listDevicesFromAddon();\n\t\treturn devices\n\t\t\t.filter((device) => shouldNotifyDevice(device, config))\n\t\t\t.map((device) => applyLogicalPortMapping(device, config));\n\t}\n}\n\n/**\n * Registry for multiple application IDs, each with its own listener config.\n * One native listener is shared; events are fanned out per app based on config.\n */\nexport class UsbDeviceListenerRegistry implements UsbDeviceListenerRegistryI {\n\tprivate readonly apps = new Map<string, AppEntry>();\n\tprivate readonly addon: NativeAddon;\n\tprivate addonCallbacksInstalled = false;\n\tprivate nativeListenerRunning = false;\n\tprivate readonly scopedListeners = new Map<string, ScopedListener>();\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\tgetEntry(appId: string): AppEntry | undefined {\n\t\treturn this.apps.get(appId);\n\t}\n\n\tensureNativeListenerRunning(): void {\n\t\tif (this.nativeListenerRunning) return;\n\t\tthis.nativeListenerRunning = true;\n\t\tthis.addon.startListening();\n\t}\n\n\tmaybeStopNativeListener(): void {\n\t\tconst anyStarted = [...this.apps.values()].some((e) => e.started);\n\t\tif (!anyStarted && this.nativeListenerRunning) {\n\t\t\tthis.nativeListenerRunning = false;\n\t\t\tthis.addon.stopListening();\n\t\t}\n\t}\n\n\tensureAddonCallbacksInstalled(): void {\n\t\tif (this.addonCallbacksInstalled) return;\n\t\tthis.addonCallbacksInstalled = true;\n\t\tensureAddonCallbacksInstalled(this.addon, this.apps);\n\t}\n\n\tlistDevicesFromAddon(): DeviceInfo[] {\n\t\treturn this.addon.listDevices();\n\t}\n\n\tpublic register(appId: string, config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tconst existing = this.apps.get(appId);\n\t\tif (existing) {\n\t\t\texisting.config = config;\n\t\t\treturn;\n\t\t}\n\t\tthis.apps.set(appId, {\n\t\t\tconfig: { ...config },\n\t\t\taddCallback: null,\n\t\t\tremoveCallback: null,\n\t\t\tstarted: false,\n\t\t});\n\t}\n\n\tpublic getListener(appId: string): UsbDeviceListenerI {\n\t\tif (!this.apps.has(appId)) {\n\t\t\tthis.apps.set(appId, {\n\t\t\t\tconfig: {},\n\t\t\t\taddCallback: null,\n\t\t\t\tremoveCallback: null,\n\t\t\t\tstarted: false,\n\t\t\t});\n\t\t}\n\t\tlet scoped = this.scopedListeners.get(appId);\n\t\tif (!scoped) {\n\t\t\tscoped = new ScopedListener(this, appId);\n\t\t\tthis.scopedListeners.set(appId, scoped);\n\t\t}\n\t\treturn scoped;\n\t}\n\n\tpublic unregister(appId: string): void {\n\t\tthis.scopedListeners.delete(appId);\n\t\tconst entry = this.apps.get(appId);\n\t\tif (entry) {\n\t\t\tentry.started = false;\n\t\t\tentry.addCallback = null;\n\t\t\tentry.removeCallback = null;\n\t\t\tthis.apps.delete(appId);\n\t\t}\n\t\tthis.maybeStopNativeListener();\n\t}\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["import_node_module", "import_node_path", "import_node_url", "path", "require", "path", "loadDefaultAddon", "require", "path"]
7
7
  }
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
- import { createRequire } from "node:module";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
2
+ import { createRequire as createRequire2 } from "node:module";
3
+ import path2 from "node:path";
4
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
5
5
 
6
6
  // src/device-filter.ts
7
7
  function toHexString(value) {
@@ -25,12 +25,12 @@ function isMappedInLogicalPortMapByHub(device, config) {
25
25
  if (!map || Object.keys(map).length === 0) return false;
26
26
  const vid = device.parentHubVid;
27
27
  const pid = device.parentHubPid;
28
- const path2 = device.portPath;
29
- if (vid === void 0 || pid === void 0 || !path2 || path2.length === 0) return false;
28
+ const path3 = device.portPath;
29
+ if (vid === void 0 || pid === void 0 || !path3 || path3.length === 0) return false;
30
30
  const key = hubKeyFromParent(vid, pid);
31
31
  const portMap = map[key];
32
32
  if (!portMap) return false;
33
- const physicalPort = path2[path2.length - 1];
33
+ const physicalPort = path3[path3.length - 1];
34
34
  return physicalPort in portMap;
35
35
  }
36
36
  function shouldNotifyDevice(device, config) {
@@ -94,7 +94,10 @@ function formatPortPath(device) {
94
94
  return device.locationInfo;
95
95
  }
96
96
 
97
- // src/index.ts
97
+ // src/registry.ts
98
+ import { createRequire } from "node:module";
99
+ import path from "node:path";
100
+ import { fileURLToPath } from "node:url";
98
101
  function loadDefaultAddon() {
99
102
  const require2 = createRequire(import.meta.url);
100
103
  const __filename = fileURLToPath(import.meta.url);
@@ -102,13 +105,173 @@ function loadDefaultAddon() {
102
105
  const packageRoot = path.join(__dirname, "..");
103
106
  return require2("node-gyp-build")(packageRoot);
104
107
  }
108
+ function ensureAddonCallbacksInstalled(addon, apps) {
109
+ addon.onDeviceAdd((device) => {
110
+ for (const entry of apps.values()) {
111
+ if (!entry.started || !entry.addCallback) continue;
112
+ if (shouldNotifyDevice(device, entry.config)) {
113
+ const mapped = applyLogicalPortMapping(device, entry.config);
114
+ entry.addCallback(mapped);
115
+ }
116
+ }
117
+ });
118
+ addon.onDeviceRemove((device) => {
119
+ for (const entry of apps.values()) {
120
+ if (!entry.started || !entry.removeCallback) continue;
121
+ if (shouldNotifyDevice(device, entry.config)) {
122
+ const mapped = applyLogicalPortMapping(device, entry.config);
123
+ entry.removeCallback(mapped);
124
+ }
125
+ }
126
+ });
127
+ }
128
+ var ScopedListener = class {
129
+ constructor(registry, appId) {
130
+ this.registry = registry;
131
+ this.appId = appId;
132
+ }
133
+ getEntry() {
134
+ const entry = this.registry.getEntry(this.appId);
135
+ if (!entry) {
136
+ throw new Error(`App not registered: ${this.appId}`);
137
+ }
138
+ return entry;
139
+ }
140
+ startListening(config) {
141
+ if (typeof config !== "object" || config === null) {
142
+ throw new TypeError("Config must be an object");
143
+ }
144
+ const entry = this.getEntry();
145
+ entry.config = config;
146
+ entry.started = true;
147
+ this.registry.ensureNativeListenerRunning();
148
+ }
149
+ stopListening() {
150
+ const entry = this.getEntry();
151
+ entry.started = false;
152
+ this.registry.maybeStopNativeListener();
153
+ }
154
+ updateConfig(config) {
155
+ if (typeof config !== "object" || config === null) {
156
+ throw new TypeError("Config must be an object");
157
+ }
158
+ this.getEntry().config = config;
159
+ }
160
+ getCurrentConfig() {
161
+ return { ...this.getEntry().config };
162
+ }
163
+ onDeviceAdd(callback) {
164
+ if (typeof callback !== "function") {
165
+ throw new TypeError("Callback must be a function");
166
+ }
167
+ this.getEntry().addCallback = callback;
168
+ this.registry.ensureAddonCallbacksInstalled();
169
+ }
170
+ onDeviceRemove(callback) {
171
+ if (typeof callback !== "function") {
172
+ throw new TypeError("Callback must be a function");
173
+ }
174
+ this.getEntry().removeCallback = callback;
175
+ this.registry.ensureAddonCallbacksInstalled();
176
+ }
177
+ listDevices() {
178
+ const config = this.getEntry().config;
179
+ const devices = this.registry.listDevicesFromAddon();
180
+ return devices.filter((device) => shouldNotifyDevice(device, config)).map((device) => applyLogicalPortMapping(device, config));
181
+ }
182
+ };
183
+ var UsbDeviceListenerRegistry = class {
184
+ apps = /* @__PURE__ */ new Map();
185
+ addon;
186
+ addonCallbacksInstalled = false;
187
+ nativeListenerRunning = false;
188
+ scopedListeners = /* @__PURE__ */ new Map();
189
+ constructor(addon) {
190
+ this.addon = addon ?? loadDefaultAddon();
191
+ }
192
+ getEntry(appId) {
193
+ return this.apps.get(appId);
194
+ }
195
+ ensureNativeListenerRunning() {
196
+ if (this.nativeListenerRunning) return;
197
+ this.nativeListenerRunning = true;
198
+ this.addon.startListening();
199
+ }
200
+ maybeStopNativeListener() {
201
+ const anyStarted = [...this.apps.values()].some((e) => e.started);
202
+ if (!anyStarted && this.nativeListenerRunning) {
203
+ this.nativeListenerRunning = false;
204
+ this.addon.stopListening();
205
+ }
206
+ }
207
+ ensureAddonCallbacksInstalled() {
208
+ if (this.addonCallbacksInstalled) return;
209
+ this.addonCallbacksInstalled = true;
210
+ ensureAddonCallbacksInstalled(this.addon, this.apps);
211
+ }
212
+ listDevicesFromAddon() {
213
+ return this.addon.listDevices();
214
+ }
215
+ register(appId, config) {
216
+ if (typeof config !== "object" || config === null) {
217
+ throw new TypeError("Config must be an object");
218
+ }
219
+ const existing = this.apps.get(appId);
220
+ if (existing) {
221
+ existing.config = config;
222
+ return;
223
+ }
224
+ this.apps.set(appId, {
225
+ config: { ...config },
226
+ addCallback: null,
227
+ removeCallback: null,
228
+ started: false
229
+ });
230
+ }
231
+ getListener(appId) {
232
+ if (!this.apps.has(appId)) {
233
+ this.apps.set(appId, {
234
+ config: {},
235
+ addCallback: null,
236
+ removeCallback: null,
237
+ started: false
238
+ });
239
+ }
240
+ let scoped = this.scopedListeners.get(appId);
241
+ if (!scoped) {
242
+ scoped = new ScopedListener(this, appId);
243
+ this.scopedListeners.set(appId, scoped);
244
+ }
245
+ return scoped;
246
+ }
247
+ unregister(appId) {
248
+ this.scopedListeners.delete(appId);
249
+ const entry = this.apps.get(appId);
250
+ if (entry) {
251
+ entry.started = false;
252
+ entry.addCallback = null;
253
+ entry.removeCallback = null;
254
+ this.apps.delete(appId);
255
+ }
256
+ this.maybeStopNativeListener();
257
+ }
258
+ };
259
+
260
+ // src/index.ts
261
+ function loadDefaultAddon2() {
262
+ const require2 = createRequire2(import.meta.url);
263
+ const __filename = fileURLToPath2(import.meta.url);
264
+ const __dirname = path2.dirname(__filename);
265
+ const packageRoot = path2.join(__dirname, "..");
266
+ return require2("node-gyp-build")(packageRoot);
267
+ }
105
268
  var UsbDeviceListener = class {
106
269
  addon;
107
270
  config = {};
108
271
  userAddCallback = null;
109
272
  userRemoveCallback = null;
110
273
  constructor(addon) {
111
- this.addon = addon ?? loadDefaultAddon();
274
+ this.addon = addon ?? loadDefaultAddon2();
112
275
  }
113
276
  /**
114
277
  * Start listening for USB device events
@@ -184,6 +347,7 @@ var UsbDeviceListener = class {
184
347
  }
185
348
  };
186
349
  export {
350
+ UsbDeviceListenerRegistry,
187
351
  UsbDeviceListener as default,
188
352
  formatPortPath
189
353
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.ts", "../src/device-filter.ts", "../src/formatPortPath.ts"],
4
- "sourcesContent": ["import { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tUsbDeviceListenerI,\n} from \"./types\";\n\nexport interface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\n\t}\n\n\t/**\n\t * Register callback for device connection events\n\t */\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userAddCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceAdd((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Register callback for device disconnection events\n\t */\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userRemoveCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport { formatPortPath } from \"./formatPortPath.js\";\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tTargetDevice,\n} from \"./types\";\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Build hub key \"${vid}-${pid}\" from device's parent hub (uppercase 4-char hex).\n */\nfunction hubKeyFromParent(vid: number, pid: number): string {\n\treturn `${vid.toString(16).toUpperCase().padStart(4, \"0\")}-${pid.toString(16).toUpperCase().padStart(4, \"0\")}`;\n}\n\n/**\n * True if device has parent hub data and is mapped in logicalPortMapByHub.\n */\nfunction isMappedInLogicalPortMapByHub(device: DeviceInfo, config: ListenerConfig): boolean {\n\tconst map = config.logicalPortMapByHub;\n\tif (!map || Object.keys(map).length === 0) return false;\n\tconst vid = device.parentHubVid;\n\tconst pid = device.parentHubPid;\n\tconst path = device.portPath;\n\tif (vid === undefined || pid === undefined || !path || path.length === 0) return false;\n\tconst key = hubKeyFromParent(vid, pid);\n\tconst portMap = map[key];\n\tif (!portMap) return false;\n\tconst physicalPort = path[path.length - 1];\n\treturn physicalPort in portMap;\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap / logicalPortMapByHub - if specified, device must be in the map (hub map takes precedence when device has hub data)\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap / logicalPortMapByHub (if specified, device must be mapped)\n\tconst hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;\n\tconst hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;\n\tif (hasHubMap && device.parentHubVid !== undefined && device.parentHubPid !== undefined && device.portPath && device.portPath.length > 0) {\n\t\tif (!isMappedInLogicalPortMapByHub(device, config)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasLocationMap && config.logicalPortMap) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasHubMap) {\n\t\t// logicalPortMapByHub is set but device has no hub data - cannot be in map\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured.\n * logicalPortMapByHub takes precedence when device has parent hub and port path.\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tconst mapByHub = config.logicalPortMapByHub;\n\tif (\n\t\tmapByHub &&\n\t\tObject.keys(mapByHub).length > 0 &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tconst key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);\n\t\tconst portMap = mapByHub[key];\n\t\tconst physicalPort = device.portPath[device.portPath.length - 1];\n\t\tif (portMap && physicalPort in portMap) {\n\t\t\treturn {\n\t\t\t\t...device,\n\t\t\t\tlogicalPort: portMap[physicalPort],\n\t\t\t};\n\t\t}\n\t}\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n", "import type { DeviceInfo } from \"./types.js\";\n\n/**\n * Format device port path for display (e.g. \"2/3/1\" for port 2, then 3, then 1).\n * When portPath is available, returns slash-separated port numbers; otherwise returns locationInfo.\n *\n * @param device - Device info from the listener\n * @returns Port path string (e.g. \"2/3/1\") or locationInfo fallback\n */\nexport function formatPortPath(device: DeviceInfo): string {\n\tif (device.portPath && device.portPath.length > 0) {\n\t\treturn device.portPath.join(\"/\");\n\t}\n\treturn device.locationInfo;\n}\n"],
5
- "mappings": "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",
6
- "names": ["path", "require"]
3
+ "sources": ["../src/index.ts", "../src/device-filter.ts", "../src/formatPortPath.ts", "../src/registry.ts"],
4
+ "sourcesContent": ["import { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tUsbDeviceListenerI,\n} from \"./types\";\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\n\t}\n\n\t/**\n\t * Register callback for device connection events\n\t */\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userAddCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceAdd((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Register callback for device disconnection events\n\t */\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userRemoveCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\tthis.addon.onDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport { formatPortPath } from \"./formatPortPath.js\";\nexport { UsbDeviceListenerRegistry } from \"./registry.js\";\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tTargetDevice,\n\tUsbDeviceListenerRegistryI,\n} from \"./types\";\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Build hub key \"${vid}-${pid}\" from device's parent hub (uppercase 4-char hex).\n */\nfunction hubKeyFromParent(vid: number, pid: number): string {\n\treturn `${vid.toString(16).toUpperCase().padStart(4, \"0\")}-${pid.toString(16).toUpperCase().padStart(4, \"0\")}`;\n}\n\n/**\n * True if device has parent hub data and is mapped in logicalPortMapByHub.\n */\nfunction isMappedInLogicalPortMapByHub(device: DeviceInfo, config: ListenerConfig): boolean {\n\tconst map = config.logicalPortMapByHub;\n\tif (!map || Object.keys(map).length === 0) return false;\n\tconst vid = device.parentHubVid;\n\tconst pid = device.parentHubPid;\n\tconst path = device.portPath;\n\tif (vid === undefined || pid === undefined || !path || path.length === 0) return false;\n\tconst key = hubKeyFromParent(vid, pid);\n\tconst portMap = map[key];\n\tif (!portMap) return false;\n\tconst physicalPort = path[path.length - 1];\n\treturn physicalPort in portMap;\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap / logicalPortMapByHub - if specified, device must be in the map (hub map takes precedence when device has hub data)\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap / logicalPortMapByHub (if specified, device must be mapped)\n\tconst hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;\n\tconst hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;\n\tif (\n\t\thasHubMap &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tif (!isMappedInLogicalPortMapByHub(device, config)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasLocationMap && config.logicalPortMap) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasHubMap) {\n\t\t// logicalPortMapByHub is set but device has no hub data - cannot be in map\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured.\n * logicalPortMapByHub takes precedence when device has parent hub and port path.\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tconst mapByHub = config.logicalPortMapByHub;\n\tif (\n\t\tmapByHub &&\n\t\tObject.keys(mapByHub).length > 0 &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tconst key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);\n\t\tconst portMap = mapByHub[key];\n\t\tconst physicalPort = device.portPath[device.portPath.length - 1];\n\t\tif (portMap && physicalPort in portMap) {\n\t\t\treturn {\n\t\t\t\t...device,\n\t\t\t\tlogicalPort: portMap[physicalPort],\n\t\t\t};\n\t\t}\n\t}\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n", "import type { DeviceInfo } from \"./types.js\";\n\n/**\n * Format device port path for display (e.g. \"2/3/1\" for port 2, then 3, then 1).\n * Uses native portPath when available; otherwise returns locationInfo.\n *\n * @param device - Device info from the listener\n * @returns Port path string (e.g. \"2/3/1\") or locationInfo when portPath is missing\n */\nexport function formatPortPath(device: DeviceInfo): string {\n\tif (device.portPath && device.portPath.length > 0) {\n\t\treturn device.portPath.join(\"/\");\n\t}\n\treturn device.locationInfo;\n}\n", "import { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tUsbDeviceListenerI,\n\tUsbDeviceListenerRegistryI,\n} from \"./types\";\n\ninterface AppEntry {\n\tconfig: ListenerConfig;\n\taddCallback: DeviceAddCallback | null;\n\tremoveCallback: DeviceRemoveCallback | null;\n\tstarted: boolean;\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\nfunction ensureAddonCallbacksInstalled(addon: NativeAddon, apps: Map<string, AppEntry>): void {\n\t// Install once: single fan-out to all registered apps\n\taddon.onDeviceAdd((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.addCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.addCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n\taddon.onDeviceRemove((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.removeCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.removeCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n}\n\n/**\n * Scoped listener that delegates to the registry and uses one app's config.\n */\nclass ScopedListener implements UsbDeviceListenerI {\n\tconstructor(\n\t\tprivate readonly registry: UsbDeviceListenerRegistry,\n\t\tprivate readonly appId: string\n\t) {}\n\n\tprivate getEntry(): AppEntry {\n\t\tconst entry = this.registry.getEntry(this.appId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`App not registered: ${this.appId}`);\n\t\t}\n\t\treturn entry;\n\t}\n\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tconst entry = this.getEntry();\n\t\tentry.config = config;\n\t\tentry.started = true;\n\t\tthis.registry.ensureNativeListenerRunning();\n\t}\n\n\tpublic stopListening(): void {\n\t\tconst entry = this.getEntry();\n\t\tentry.started = false;\n\t\tthis.registry.maybeStopNativeListener();\n\t}\n\n\tpublic updateConfig(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.getEntry().config = config;\n\t}\n\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.getEntry().config };\n\t}\n\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.getEntry().addCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.getEntry().removeCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst config = this.getEntry().config;\n\t\tconst devices = this.registry.listDevicesFromAddon();\n\t\treturn devices\n\t\t\t.filter((device) => shouldNotifyDevice(device, config))\n\t\t\t.map((device) => applyLogicalPortMapping(device, config));\n\t}\n}\n\n/**\n * Registry for multiple application IDs, each with its own listener config.\n * One native listener is shared; events are fanned out per app based on config.\n */\nexport class UsbDeviceListenerRegistry implements UsbDeviceListenerRegistryI {\n\tprivate readonly apps = new Map<string, AppEntry>();\n\tprivate readonly addon: NativeAddon;\n\tprivate addonCallbacksInstalled = false;\n\tprivate nativeListenerRunning = false;\n\tprivate readonly scopedListeners = new Map<string, ScopedListener>();\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\tgetEntry(appId: string): AppEntry | undefined {\n\t\treturn this.apps.get(appId);\n\t}\n\n\tensureNativeListenerRunning(): void {\n\t\tif (this.nativeListenerRunning) return;\n\t\tthis.nativeListenerRunning = true;\n\t\tthis.addon.startListening();\n\t}\n\n\tmaybeStopNativeListener(): void {\n\t\tconst anyStarted = [...this.apps.values()].some((e) => e.started);\n\t\tif (!anyStarted && this.nativeListenerRunning) {\n\t\t\tthis.nativeListenerRunning = false;\n\t\t\tthis.addon.stopListening();\n\t\t}\n\t}\n\n\tensureAddonCallbacksInstalled(): void {\n\t\tif (this.addonCallbacksInstalled) return;\n\t\tthis.addonCallbacksInstalled = true;\n\t\tensureAddonCallbacksInstalled(this.addon, this.apps);\n\t}\n\n\tlistDevicesFromAddon(): DeviceInfo[] {\n\t\treturn this.addon.listDevices();\n\t}\n\n\tpublic register(appId: string, config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tconst existing = this.apps.get(appId);\n\t\tif (existing) {\n\t\t\texisting.config = config;\n\t\t\treturn;\n\t\t}\n\t\tthis.apps.set(appId, {\n\t\t\tconfig: { ...config },\n\t\t\taddCallback: null,\n\t\t\tremoveCallback: null,\n\t\t\tstarted: false,\n\t\t});\n\t}\n\n\tpublic getListener(appId: string): UsbDeviceListenerI {\n\t\tif (!this.apps.has(appId)) {\n\t\t\tthis.apps.set(appId, {\n\t\t\t\tconfig: {},\n\t\t\t\taddCallback: null,\n\t\t\t\tremoveCallback: null,\n\t\t\t\tstarted: false,\n\t\t\t});\n\t\t}\n\t\tlet scoped = this.scopedListeners.get(appId);\n\t\tif (!scoped) {\n\t\t\tscoped = new ScopedListener(this, appId);\n\t\t\tthis.scopedListeners.set(appId, scoped);\n\t\t}\n\t\treturn scoped;\n\t}\n\n\tpublic unregister(appId: string): void {\n\t\tthis.scopedListeners.delete(appId);\n\t\tconst entry = this.apps.get(appId);\n\t\tif (entry) {\n\t\t\tentry.started = false;\n\t\t\tentry.addCallback = null;\n\t\t\tentry.removeCallback = null;\n\t\t\tthis.apps.delete(appId);\n\t\t}\n\t\tthis.maybeStopNativeListener();\n\t}\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["createRequire", "path", "fileURLToPath", "path", "require", "loadDefaultAddon", "require", "createRequire", "fileURLToPath", "path"]
7
7
  }
@@ -0,0 +1,29 @@
1
+ import type { DeviceAddCallback, DeviceInfo, DeviceRemoveCallback, ListenerConfig, NativeAddon, UsbDeviceListenerI, UsbDeviceListenerRegistryI } from "./types";
2
+ interface AppEntry {
3
+ config: ListenerConfig;
4
+ addCallback: DeviceAddCallback | null;
5
+ removeCallback: DeviceRemoveCallback | null;
6
+ started: boolean;
7
+ }
8
+ /**
9
+ * Registry for multiple application IDs, each with its own listener config.
10
+ * One native listener is shared; events are fanned out per app based on config.
11
+ */
12
+ export declare class UsbDeviceListenerRegistry implements UsbDeviceListenerRegistryI {
13
+ private readonly apps;
14
+ private readonly addon;
15
+ private addonCallbacksInstalled;
16
+ private nativeListenerRunning;
17
+ private readonly scopedListeners;
18
+ constructor(addon?: NativeAddon);
19
+ getEntry(appId: string): AppEntry | undefined;
20
+ ensureNativeListenerRunning(): void;
21
+ maybeStopNativeListener(): void;
22
+ ensureAddonCallbacksInstalled(): void;
23
+ listDevicesFromAddon(): DeviceInfo[];
24
+ register(appId: string, config: ListenerConfig): void;
25
+ getListener(appId: string): UsbDeviceListenerI;
26
+ unregister(appId: string): void;
27
+ }
28
+ export {};
29
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,0BAA0B,EAC1B,MAAM,SAAS,CAAC;AAEjB,UAAU,QAAQ;IACjB,MAAM,EAAE,cAAc,CAAC;IACvB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,cAAc,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC5C,OAAO,EAAE,OAAO,CAAC;CACjB;AAqGD;;;GAGG;AACH,qBAAa,yBAA0B,YAAW,0BAA0B;IAC3E,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;gBAEzD,KAAK,CAAC,EAAE,WAAW;IAI/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAI7C,2BAA2B,IAAI,IAAI;IAMnC,uBAAuB,IAAI,IAAI;IAQ/B,6BAA6B,IAAI,IAAI;IAMrC,oBAAoB,IAAI,UAAU,EAAE;IAI7B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI;IAiBrD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB;IAiB9C,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAWtC"}
package/dist/types.d.ts CHANGED
@@ -146,6 +146,17 @@ export interface ListenerConfig {
146
146
  * Callback type for device add events
147
147
  */
148
148
  export type DeviceAddCallback = (deviceInfo: DeviceInfo) => void;
149
+ /**
150
+ * Native addon interface (implemented by the node-gyp addon).
151
+ * Used for dependency injection in tests.
152
+ */
153
+ export interface NativeAddon {
154
+ startListening(): void;
155
+ stopListening(): void;
156
+ onDeviceAdd(callback: DeviceAddCallback): void;
157
+ onDeviceRemove(callback: DeviceRemoveCallback): void;
158
+ listDevices(): DeviceInfo[];
159
+ }
149
160
  /**
150
161
  * Callback type for device remove events
151
162
  */
@@ -256,4 +267,46 @@ export interface UsbDeviceListenerI {
256
267
  */
257
268
  listDevices(): DeviceInfo[];
258
269
  }
270
+ /**
271
+ * Registry for multiple application IDs, each with its own listener config.
272
+ * Use when multiple parts of the same process need separate USB device filters and callbacks.
273
+ */
274
+ export interface UsbDeviceListenerRegistryI {
275
+ /**
276
+ * Register or update config for an application ID.
277
+ * Call before or after getListener(); the scoped listener's startListening(config) also updates config.
278
+ *
279
+ * @param appId - Application identifier
280
+ * @param config - Listener configuration for this app
281
+ * @throws {TypeError} If config is not an object
282
+ *
283
+ * @example
284
+ * registry.register("my-app", { targetDevices: [{ vid: "04E8", pid: "6860" }] });
285
+ */
286
+ register(appId: string, config: ListenerConfig): void;
287
+ /**
288
+ * Return a listener scoped to the given application ID.
289
+ * Events and listDevices() are filtered and mapped using that app's config.
290
+ * Creates an internal entry with empty config if the app was not yet registered.
291
+ *
292
+ * @param appId - Application identifier
293
+ * @returns Listener implementing UsbDeviceListenerI for this app
294
+ *
295
+ * @example
296
+ * const listener = registry.getListener("my-app");
297
+ * listener.startListening(config);
298
+ * listener.onDeviceAdd((device) => { ... });
299
+ */
300
+ getListener(appId: string): UsbDeviceListenerI;
301
+ /**
302
+ * Remove an application from the registry and stop native listener if it was the last.
303
+ * Safe to call multiple times.
304
+ *
305
+ * @param appId - Application identifier
306
+ *
307
+ * @example
308
+ * registry.unregister("my-app");
309
+ */
310
+ unregister(appId: string): void;
311
+ }
259
312
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE7D;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAE/B;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,YAAY,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAE7C;;;;;;;;OAQG;IACH,aAAa,IAAI,IAAI,CAAC;IAEtB;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAE3C;;;;;;;;;;;OAWG;IACH,gBAAgB,IAAI,cAAc,CAAC;IAEnC;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAE/C;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAErD;;;;;;;;;;;;;;OAcG;IACH,WAAW,IAAI,UAAU,EAAE,CAAC;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcesystems/usb-device-listener",
3
- "version": "1.0.78",
3
+ "version": "1.0.79",
4
4
  "description": "Native cross-platform USB device listener for Windows and macOS",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -83,6 +83,7 @@
83
83
  "test": "vitest run",
84
84
  "test:watch": "vitest",
85
85
  "listDevices": "cross-env DEBUG=* tsx ./src/examples/list-devices.ts",
86
- "example": "cross-env DEBUG=* tsx ./src/examples/example.ts"
86
+ "example": "cross-env DEBUG=* tsx ./src/examples/example.ts",
87
+ "example:registry": "cross-env DEBUG=* tsx ./src/examples/example-registry.ts"
87
88
  }
88
89
  }