@companion-surface/base 0.5.0 → 0.6.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.1](https://github.com/bitfocus/companion-surface-api/compare/companion-surface-base-v0.6.0...companion-surface-base-v0.6.1) (2025-11-24)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * make serialnumber always defined, and add helper for generating unique serials ([2cfc41e](https://github.com/bitfocus/companion-surface-api/commit/2cfc41ed58487a5d0bab7b6c6aa110774b1bf907))
9
+
10
+ ## [0.6.0](https://github.com/bitfocus/companion-surface-api/compare/companion-surface-base-v0.5.0...companion-surface-base-v0.6.0) (2025-11-23)
11
+
12
+
13
+ ### Features
14
+
15
+ * add allowMultipleInstances field to manifest ([94bba80](https://github.com/bitfocus/companion-surface-api/commit/94bba800143308a1b8fd6ed1d56249b8fc8a432b))
16
+
3
17
  ## [0.5.0](https://github.com/bitfocus/companion-surface-api/compare/companion-surface-base-v0.4.2...companion-surface-base-v0.5.0) (2025-11-16)
4
18
 
5
19
 
@@ -130,6 +130,10 @@
130
130
  "required": ["vendorId", "productIds"],
131
131
  "additionalProperties": false
132
132
  }
133
+ },
134
+ "allowMultipleInstances": {
135
+ "type": "boolean",
136
+ "description": "Whether multiple instances of this module can be run simultaneously"
133
137
  }
134
138
  },
135
139
  "required": [
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=util.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/util.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,115 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { StableDeviceIdGenerator } from '../util.js';
3
+ describe('StableDeviceIdGenerator', () => {
4
+ test('generates different IDs for different uniqueness keys', () => {
5
+ const generator = new StableDeviceIdGenerator();
6
+ const id1 = generator.generateId('1234:5678', '/dev/hidraw0');
7
+ const id2 = generator.generateId('1234:9999', '/dev/hidraw1');
8
+ expect(id1).not.toBe(id2);
9
+ expect(id1).toHaveLength(40); // SHA1 hex digest length
10
+ expect(id2).toHaveLength(40);
11
+ });
12
+ test('generates different IDs for same uniqueness key with different paths', () => {
13
+ const generator = new StableDeviceIdGenerator();
14
+ const id1 = generator.generateId('1234:5678', '/dev/hidraw0');
15
+ const id2 = generator.generateId('1234:5678', '/dev/hidraw1');
16
+ expect(id1).not.toBe(id2);
17
+ });
18
+ test('returns same ID for same device path on repeated calls', () => {
19
+ const generator = new StableDeviceIdGenerator();
20
+ const id1 = generator.generateId('1234:5678', '/dev/hidraw0');
21
+ const id2 = generator.generateId('1234:5678', '/dev/hidraw0');
22
+ expect(id1).toBe(id2);
23
+ });
24
+ test('handles missing device path parameter', () => {
25
+ const generator = new StableDeviceIdGenerator();
26
+ const id1 = generator.generateId('1234:5678');
27
+ const id2 = generator.generateId('1234:5678');
28
+ // Without path, each call should generate a new ID
29
+ expect(id1).not.toBe(id2);
30
+ expect(id1).toHaveLength(40);
31
+ expect(id2).toHaveLength(40);
32
+ });
33
+ test('generates stable IDs with counter increment', () => {
34
+ const generator = new StableDeviceIdGenerator();
35
+ const id1 = generator.generateId('1234:5678', '/dev/hidraw0');
36
+ const id2 = generator.generateId('1234:5678', '/dev/hidraw1');
37
+ const id3 = generator.generateId('1234:5678', '/dev/hidraw2');
38
+ // All should be unique
39
+ expect(new Set([id1, id2, id3]).size).toBe(3);
40
+ });
41
+ test('separate instances have independent state', () => {
42
+ const generator1 = new StableDeviceIdGenerator();
43
+ const generator2 = new StableDeviceIdGenerator();
44
+ const id1 = generator1.generateId('1234:5678', '/dev/hidraw0');
45
+ const id2 = generator2.generateId('1234:5678', '/dev/hidraw1');
46
+ // Same inputs in different instances should produce same ID
47
+ expect(id1).toBe(id2);
48
+ });
49
+ test('handles multiple devices with same uniqueness key in batch', () => {
50
+ const generator = new StableDeviceIdGenerator();
51
+ // Simulate 3 identical devices (same vendor/product) with different paths
52
+ const ids = [
53
+ generator.generateId('1234:5678', '/dev/hidraw0'),
54
+ generator.generateId('1234:5678', '/dev/hidraw1'),
55
+ generator.generateId('1234:5678', '/dev/hidraw2'),
56
+ ];
57
+ // All should be unique
58
+ expect(new Set(ids).size).toBe(3);
59
+ // Requesting same path again should return cached ID
60
+ expect(generator.generateId('1234:5678', '/dev/hidraw1')).toBe(ids[1]);
61
+ });
62
+ test('handles mixed scenarios with and without device paths', () => {
63
+ const generator = new StableDeviceIdGenerator();
64
+ const withPath1 = generator.generateId('1234:5678', '/dev/hidraw0');
65
+ const withoutPath1 = generator.generateId('1234:5678');
66
+ const withPath2 = generator.generateId('1234:5678', '/dev/hidraw1');
67
+ const withoutPath2 = generator.generateId('1234:5678');
68
+ // All should be unique
69
+ expect(new Set([withPath1, withoutPath1, withPath2, withoutPath2]).size).toBe(4);
70
+ // Cached path should return same ID
71
+ expect(generator.generateId('1234:5678', '/dev/hidraw0')).toBe(withPath1);
72
+ });
73
+ test('handles undefined device path (same as omitted)', () => {
74
+ const generator = new StableDeviceIdGenerator();
75
+ const id1 = generator.generateId('1234:5678', undefined);
76
+ const id2 = generator.generateId('1234:5678', undefined);
77
+ // Should generate different IDs when path is undefined
78
+ expect(id1).not.toBe(id2);
79
+ });
80
+ test('generates valid hex SHA1 hashes', () => {
81
+ const generator = new StableDeviceIdGenerator();
82
+ const id = generator.generateId('1234:5678', '/dev/hidraw0');
83
+ // Should be 40 character hex string
84
+ expect(id).toMatch(/^[0-9a-f]{40}$/);
85
+ });
86
+ test('deduplicates multiple endpoints of same device', () => {
87
+ const generator = new StableDeviceIdGenerator();
88
+ // Simulate same device with multiple HID endpoints (same path)
89
+ const id1 = generator.generateId('1234:5678', '/dev/hidraw0');
90
+ const id2 = generator.generateId('1234:5678', '/dev/hidraw0');
91
+ const id3 = generator.generateId('1234:5678', '/dev/hidraw0');
92
+ expect(id1).toBe(id2);
93
+ expect(id2).toBe(id3);
94
+ });
95
+ test('handles empty uniqueness key', () => {
96
+ const generator = new StableDeviceIdGenerator();
97
+ const id1 = generator.generateId('', '/dev/hidraw0');
98
+ const id2 = generator.generateId('', '/dev/hidraw1');
99
+ expect(id1).not.toBe(id2);
100
+ expect(id1).toHaveLength(40);
101
+ });
102
+ test('real-world scenario: 2 identical Stream Decks', () => {
103
+ const generator = new StableDeviceIdGenerator();
104
+ // Same vendor:product, different USB paths
105
+ const deck1 = generator.generateId('4057:96', '/dev/hidraw0');
106
+ const deck2 = generator.generateId('4057:96', '/dev/hidraw1');
107
+ expect(deck1).not.toBe(deck2);
108
+ expect(deck1).toHaveLength(40);
109
+ expect(deck2).toHaveLength(40);
110
+ // Re-enumerating same devices should return same IDs
111
+ expect(generator.generateId('4057:96', '/dev/hidraw0')).toBe(deck1);
112
+ expect(generator.generateId('4057:96', '/dev/hidraw1')).toBe(deck2);
113
+ });
114
+ });
115
+ //# sourceMappingURL=util.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.test.js","sourceRoot":"","sources":["../../src/__tests__/util.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAEpD,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACxC,IAAI,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAClE,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAE7D,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACzB,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA,CAAC,yBAAyB;QACtD,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,sEAAsE,EAAE,GAAG,EAAE;QACjF,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAE7D,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,wDAAwD,EAAE,GAAG,EAAE;QACnE,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAE7D,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAClD,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QAC7C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QAE7C,mDAAmD;QACnD,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACzB,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;QAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACxD,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAE7D,uBAAuB;QACvB,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC9C,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACtD,MAAM,UAAU,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAChD,MAAM,UAAU,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAEhD,MAAM,GAAG,GAAG,UAAU,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC9D,MAAM,GAAG,GAAG,UAAU,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAE9D,4DAA4D;QAC5D,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACvE,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,0EAA0E;QAC1E,MAAM,GAAG,GAAG;YACX,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC;YACjD,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC;YACjD,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC;SACjD,CAAA;QAED,uBAAuB;QACvB,MAAM,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAEjC,qDAAqD;QACrD,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAClE,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,SAAS,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QACnE,MAAM,YAAY,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACtD,MAAM,SAAS,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QACnE,MAAM,YAAY,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QAEtD,uBAAuB;QACvB,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAEhF,oCAAoC;QACpC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC1E,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,iDAAiD,EAAE,GAAG,EAAE;QAC5D,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;QACxD,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;QAExD,uDAAuD;QACvD,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,iCAAiC,EAAE,GAAG,EAAE;QAC5C,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,EAAE,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAE5D,oCAAoC;QACpC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACrC,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,gDAAgD,EAAE,GAAG,EAAE;QAC3D,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,+DAA+D;QAC/D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QAE7D,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACrB,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACzC,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,CAAA;QACpD,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,CAAA;QAEpD,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACzB,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;QAC1D,MAAM,SAAS,GAAG,IAAI,uBAAuB,EAAE,CAAA;QAE/C,2CAA2C;QAC3C,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;QAC7D,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;QAE7D,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;QAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;QAE9B,qDAAqD;QACrD,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnE,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -10,7 +10,11 @@ export interface HIDDevice {
10
10
  vendorId: number;
11
11
  productId: number;
12
12
  path: string;
13
- serialNumber: string | undefined;
13
+ /**
14
+ * The serial number of the device.
15
+ * This is either the serial number provided by the device, or something generated by Companion to give this device a unique ID
16
+ */
17
+ serialNumber: string;
14
18
  manufacturer: string | undefined;
15
19
  product: string | undefined;
16
20
  release: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/surface-api/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAA;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAExE;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CACzB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,CAAA;AAC9B,MAAM,MAAM,SAAS,GAAG,MAAM,CAAA;AAE9B,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AACD,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC;;OAEG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;OAEG;IACH,aAAa,EAAE,6BAA6B,CAAA;IAC5C;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,oBAAoB,GAAG,qBAAqB,CAAC,CAAA;IACvE;;;OAGG;IACH,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAEpC;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAEvB;;OAEG;IACH,YAAY,EAAE,uBAAuB,EAAE,GAAG,IAAI,CAAA;CAC9C;AAED,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,eAAe,CAAA;IACxB,aAAa,EAAE,oBAAoB,CAAA;CACnC;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,2BAA2B;IAC3C,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,aAAa,CAAA;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/surface-api/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAA;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAExE;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CACzB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,CAAA;AAC9B,MAAM,MAAM,SAAS,GAAG,MAAM,CAAA;AAE9B,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AACD,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC;;OAEG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;OAEG;IACH,aAAa,EAAE,6BAA6B,CAAA;IAC5C;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,oBAAoB,GAAG,qBAAqB,CAAC,CAAA;IACvE;;;OAGG;IACH,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAEpC;;OAEG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAEvB;;OAEG;IACH,YAAY,EAAE,uBAAuB,EAAE,GAAG,IAAI,CAAA;CAC9C;AAED,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,eAAe,CAAA;IACxB,aAAa,EAAE,oBAAoB,CAAA;CACnC;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,2BAA2B;IAC3C,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,aAAa,CAAA;CACrB"}
package/dist/util.d.ts CHANGED
@@ -5,4 +5,27 @@ export interface RgbColor {
5
5
  b: number;
6
6
  }
7
7
  export declare function parseColor(color: string | undefined): RgbColor;
8
+ /**
9
+ * Helper class to generate stable, unique device IDs for devices that lack serial numbers.
10
+ * Each instance is scoped to a single batch of devices, ensuring uniqueness within that batch.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const generator = new StableDeviceIdGenerator()
15
+ * const serial1 = generator.generateId('1234:5678', '/dev/hidraw0')
16
+ * const serial2 = generator.generateId('1234:5678', '/dev/hidraw1')
17
+ * // serial1 and serial2 will be unique even for the same uniquenessKey
18
+ * ```
19
+ */
20
+ export declare class StableDeviceIdGenerator {
21
+ #private;
22
+ /**
23
+ * Generate a stable unique ID for a device within the current batch.
24
+ *
25
+ * @param uniquenessKey - A string containing device identifiers (e.g., "vendorId:productId")
26
+ * @param devicePath - A unique identifier for the device. Typically the device path. This is to ensure that multiple endpoints of the same hid device get the same id.
27
+ * @returns A stable unique identifier for the device
28
+ */
29
+ generateId(uniquenessKey: string, devicePath?: string): string;
30
+ }
8
31
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAE3C;AAED,MAAM,WAAW,QAAQ;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACT;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAM9D"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAEA,wBAAgB,WAAW,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAE3C;AAED,MAAM,WAAW,QAAQ;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACT;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAM9D;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,uBAAuB;;IAInC;;;;;;OAMG;IACH,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM;CAsB9D"}
package/dist/util.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  export function assertNever(_v) {
2
3
  // Nothing to do
3
4
  }
@@ -7,4 +8,47 @@ export function parseColor(color) {
7
8
  const b = color ? parseInt(color.substr(5, 2), 16) : 0;
8
9
  return { r, g, b };
9
10
  }
11
+ /**
12
+ * Helper class to generate stable, unique device IDs for devices that lack serial numbers.
13
+ * Each instance is scoped to a single batch of devices, ensuring uniqueness within that batch.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const generator = new StableDeviceIdGenerator()
18
+ * const serial1 = generator.generateId('1234:5678', '/dev/hidraw0')
19
+ * const serial2 = generator.generateId('1234:5678', '/dev/hidraw1')
20
+ * // serial1 and serial2 will be unique even for the same uniquenessKey
21
+ * ```
22
+ */
23
+ export class StableDeviceIdGenerator {
24
+ #previousForDevicePath = new Map();
25
+ #returnedIds = new Map();
26
+ /**
27
+ * Generate a stable unique ID for a device within the current batch.
28
+ *
29
+ * @param uniquenessKey - A string containing device identifiers (e.g., "vendorId:productId")
30
+ * @param devicePath - A unique identifier for the device. Typically the device path. This is to ensure that multiple endpoints of the same hid device get the same id.
31
+ * @returns A stable unique identifier for the device
32
+ */
33
+ generateId(uniquenessKey, devicePath) {
34
+ // Generate a complete key
35
+ // If there is something cached against the devicePath, use that
36
+ const pathCacheKey = `${uniquenessKey}||${devicePath}`;
37
+ if (devicePath) {
38
+ const cached = this.#previousForDevicePath.get(pathCacheKey);
39
+ if (cached)
40
+ return cached;
41
+ }
42
+ // Loop until we find a non-colliding ID
43
+ for (let i = 0;; i++) {
44
+ const id = `${uniquenessKey}||${i}`;
45
+ if (!this.#returnedIds.has(id)) {
46
+ const fakeSerial = createHash('sha1').update(id).digest('hex');
47
+ this.#returnedIds.set(id, fakeSerial);
48
+ this.#previousForDevicePath.set(pathCacheKey, fakeSerial);
49
+ return fakeSerial;
50
+ }
51
+ }
52
+ }
53
+ }
10
54
  //# sourceMappingURL=util.js.map
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,EAAS;IACpC,gBAAgB;AACjB,CAAC;AAQD,MAAM,UAAU,UAAU,CAAC,KAAyB;IACnD,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEtD,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;AACnB,CAAC"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,UAAU,WAAW,CAAC,EAAS;IACpC,gBAAgB;AACjB,CAAC;AAQD,MAAM,UAAU,UAAU,CAAC,KAAyB;IACnD,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEtD,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;AACnB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,uBAAuB;IAC1B,sBAAsB,GAAG,IAAI,GAAG,EAAkB,CAAA;IAClD,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAA;IAEjD;;;;;;OAMG;IACH,UAAU,CAAC,aAAqB,EAAE,UAAmB;QACpD,0BAA0B;QAE1B,gEAAgE;QAChE,MAAM,YAAY,GAAG,GAAG,aAAa,KAAK,UAAU,EAAE,CAAA;QACtD,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,MAAM,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YAC5D,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAA;QAC1B,CAAC;QAED,wCAAwC;QACxC,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;YACvB,MAAM,EAAE,GAAG,GAAG,aAAa,KAAK,CAAC,EAAE,CAAA;YACnC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChC,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBAE9D,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;gBACrC,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;gBACzD,OAAO,UAAU,CAAA;YAClB,CAAC;QACF,CAAC;IACF,CAAC;CACD"}
@@ -58,6 +58,10 @@ export interface SurfaceModuleManifest {
58
58
  * List of USB vendor and product IDs that the module supports. Your module will only be notified of devices matching these IDs.
59
59
  */
60
60
  usbIds: SurfaceModuleManifestUsbIds[];
61
+ /**
62
+ * Whether multiple instances of this module can be run simultaneously
63
+ */
64
+ allowMultipleInstances?: boolean;
61
65
  }
62
66
  export interface SurfaceModuleManifestMaintainer {
63
67
  name: string;
@@ -52,7 +52,7 @@ var require_equal = __commonJS({
52
52
  // generated/validate_manifest.js
53
53
  var validate = validate20;
54
54
  var validate_manifest_default = validate20;
55
- var schema31 = { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "title": "SurfaceModuleManifest", "properties": { "$schema": { "type": "string" }, "type": { "type": "string", "enum": ["surface"], "description": "Type of module. Must be: surface" }, "id": { "type": "string", "description": "Unique identifier for the module" }, "name": { "type": "string", "description": "Name of the module" }, "shortname": { "type": "string" }, "description": { "type": "string", "description": "Description of the module " }, "version": { "type": "string", "description": "Current version of the module" }, "isPrerelease": { "type": "boolean", "description": "Is this a pre-release version" }, "license": { "type": "string", "description": "SPDX identifier for license of the module" }, "repository": { "type": "string", "description": "URL to the source repository" }, "bugs": { "type": "string", "description": "URL to bug tracker" }, "maintainers": { "type": "array", "description": "List of active maintiners", "uniqueItems": true, "items": { "type": "object", "title": "SurfaceModuleManifestMaintainer", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "github": { "type": "string" }, "url": { "type": "string" } }, "required": ["name"], "additionalProperties": false } }, "runtime": { "type": "object", "title": "SurfaceModuleManifestRuntime", "description": "Information on how to execute the module", "properties": { "type": { "type": "string", "description": "Type of the module. Must be: node18 or node22", "enum": ["node22"] }, "apiVersion": { "type": "string", "description": "The version of the host-api used" }, "entrypoint": { "type": "string", "description": "Entrypoint to pass to the runtime. eg index.js" } }, "required": ["type", "apiVersion", "entrypoint"] }, "products": { "type": "array", "uniqueItems": true, "items": { "type": "string" }, "minItems": 1 }, "keywords": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, "usbIds": { "type": "array", "description": "List of USB vendor and product IDs that the module supports. Your module will only be notified of devices matching these IDs.", "uniqueItems": true, "items": { "type": "object", "title": "SurfaceModuleManifestUsbIds", "properties": { "vendorId": { "type": "integer" }, "productIds": { "type": "array", "uniqueItems": true, "items": { "type": "integer" }, "minItems": 1 } }, "required": ["vendorId", "productIds"], "additionalProperties": false } } }, "required": ["type", "id", "name", "shortname", "description", "version", "license", "repository", "bugs", "maintainers", "runtime", "products", "keywords", "usbIds"] };
55
+ var schema31 = { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "title": "SurfaceModuleManifest", "properties": { "$schema": { "type": "string" }, "type": { "type": "string", "enum": ["surface"], "description": "Type of module. Must be: surface" }, "id": { "type": "string", "description": "Unique identifier for the module" }, "name": { "type": "string", "description": "Name of the module" }, "shortname": { "type": "string" }, "description": { "type": "string", "description": "Description of the module " }, "version": { "type": "string", "description": "Current version of the module" }, "isPrerelease": { "type": "boolean", "description": "Is this a pre-release version" }, "license": { "type": "string", "description": "SPDX identifier for license of the module" }, "repository": { "type": "string", "description": "URL to the source repository" }, "bugs": { "type": "string", "description": "URL to bug tracker" }, "maintainers": { "type": "array", "description": "List of active maintiners", "uniqueItems": true, "items": { "type": "object", "title": "SurfaceModuleManifestMaintainer", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "github": { "type": "string" }, "url": { "type": "string" } }, "required": ["name"], "additionalProperties": false } }, "runtime": { "type": "object", "title": "SurfaceModuleManifestRuntime", "description": "Information on how to execute the module", "properties": { "type": { "type": "string", "description": "Type of the module. Must be: node18 or node22", "enum": ["node22"] }, "apiVersion": { "type": "string", "description": "The version of the host-api used" }, "entrypoint": { "type": "string", "description": "Entrypoint to pass to the runtime. eg index.js" } }, "required": ["type", "apiVersion", "entrypoint"] }, "products": { "type": "array", "uniqueItems": true, "items": { "type": "string" }, "minItems": 1 }, "keywords": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, "usbIds": { "type": "array", "description": "List of USB vendor and product IDs that the module supports. Your module will only be notified of devices matching these IDs.", "uniqueItems": true, "items": { "type": "object", "title": "SurfaceModuleManifestUsbIds", "properties": { "vendorId": { "type": "integer" }, "productIds": { "type": "array", "uniqueItems": true, "items": { "type": "integer" }, "minItems": 1 } }, "required": ["vendorId", "productIds"], "additionalProperties": false } }, "allowMultipleInstances": { "type": "boolean", "description": "Whether multiple instances of this module can be run simultaneously" } }, "required": ["type", "id", "name", "shortname", "description", "version", "license", "repository", "bugs", "maintainers", "runtime", "products", "keywords", "usbIds"] };
56
56
  var func0 = require_equal().default;
57
57
  function validate20(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) {
58
58
  let vErrors = null;
@@ -595,6 +595,18 @@ function validate20(data, { instancePath = "", parentData, parentDataProperty, r
595
595
  } else {
596
596
  var valid0 = true;
597
597
  }
598
+ if (valid0) {
599
+ if (data.allowMultipleInstances !== void 0) {
600
+ const _errs63 = errors;
601
+ if (typeof data.allowMultipleInstances !== "boolean") {
602
+ validate20.errors = [{ instancePath: instancePath + "/allowMultipleInstances", schemaPath: "#/properties/allowMultipleInstances/type", keyword: "type", params: { type: "boolean" }, message: "must be boolean" }];
603
+ return false;
604
+ }
605
+ var valid0 = _errs63 === errors;
606
+ } else {
607
+ var valid0 = true;
608
+ }
609
+ }
598
610
  }
599
611
  }
600
612
  }
@@ -619,7 +631,7 @@ function validate20(data, { instancePath = "", parentData, parentDataProperty, r
619
631
  validate20.errors = vErrors;
620
632
  return errors === 0;
621
633
  }
622
- validate20.evaluated = { "props": { "$schema": true, "type": true, "id": true, "name": true, "shortname": true, "description": true, "version": true, "isPrerelease": true, "license": true, "repository": true, "bugs": true, "maintainers": true, "runtime": true, "products": true, "keywords": true, "usbIds": true }, "dynamicProps": false, "dynamicItems": false };
634
+ validate20.evaluated = { "props": { "$schema": true, "type": true, "id": true, "name": true, "shortname": true, "description": true, "version": true, "isPrerelease": true, "license": true, "repository": true, "bugs": true, "maintainers": true, "runtime": true, "products": true, "keywords": true, "usbIds": true, "allowMultipleInstances": true }, "dynamicProps": false, "dynamicItems": false };
623
635
  export {
624
636
  validate_manifest_default as default,
625
637
  validate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-surface/base",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/bitfocus/companion-surface-api",