@neurodevs/node-lsl 14.1.5 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/__tests__/AbstractPackageTest.d.ts +1 -0
- package/build/__tests__/AbstractPackageTest.js +7 -0
- package/build/__tests__/AbstractPackageTest.js.map +1 -1
- package/build/__tests__/impl/LslStreamInfo.test.d.ts +1 -0
- package/build/__tests__/impl/LslStreamInfo.test.js +33 -6
- package/build/__tests__/impl/LslStreamInfo.test.js.map +1 -1
- package/build/__tests__/impl/LslWebSocketBridge.test.d.ts +10 -2
- package/build/__tests__/impl/LslWebSocketBridge.test.js +78 -12
- package/build/__tests__/impl/LslWebSocketBridge.test.js.map +1 -1
- package/build/impl/LslStreamInfo.d.ts +17 -8
- package/build/impl/LslStreamInfo.js +12 -6
- package/build/impl/LslStreamInfo.js.map +1 -1
- package/build/impl/LslStreamInlet.js +2 -1
- package/build/impl/LslStreamInlet.js.map +1 -1
- package/build/impl/LslStreamOutlet.d.ts +3 -2
- package/build/impl/LslStreamOutlet.js +4 -4
- package/build/impl/LslStreamOutlet.js.map +1 -1
- package/build/impl/LslWebSocketBridge.d.ts +18 -6
- package/build/impl/LslWebSocketBridge.js +69 -27
- package/build/impl/LslWebSocketBridge.js.map +1 -1
- package/build/testDoubles/StreamInfo/FakeStreamInfo.d.ts +8 -0
- package/build/testDoubles/StreamInfo/FakeStreamInfo.js +11 -0
- package/build/testDoubles/StreamInfo/FakeStreamInfo.js.map +1 -1
- package/build/testDoubles/StreamInfo/SpyStreamInfo.d.ts +2 -0
- package/build/testDoubles/StreamInfo/SpyStreamInfo.js +5 -0
- package/build/testDoubles/StreamInfo/SpyStreamInfo.js.map +1 -1
- package/build/testDoubles/WebSocketBridge/FakeWebSocketBridge.d.ts +2 -0
- package/build/testDoubles/WebSocketBridge/FakeWebSocketBridge.js +18 -4
- package/build/testDoubles/WebSocketBridge/FakeWebSocketBridge.js.map +1 -1
- package/build/testDoubles/WebSockets/FakeWebSocket.d.ts +4 -0
- package/build/testDoubles/WebSockets/FakeWebSocket.js +12 -1
- package/build/testDoubles/WebSockets/FakeWebSocket.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/AbstractPackageTest.ts +10 -0
- package/src/__tests__/impl/LslStreamInfo.test.ts +40 -6
- package/src/__tests__/impl/LslWebSocketBridge.test.ts +111 -13
- package/src/impl/LslStreamInfo.ts +28 -12
- package/src/impl/LslStreamInlet.ts +4 -1
- package/src/impl/LslStreamOutlet.ts +6 -5
- package/src/impl/LslWebSocketBridge.ts +104 -38
- package/src/testDoubles/StreamInfo/FakeStreamInfo.ts +14 -0
- package/src/testDoubles/StreamInfo/SpyStreamInfo.ts +7 -0
- package/src/testDoubles/WebSocketBridge/FakeWebSocketBridge.ts +24 -5
- package/src/testDoubles/WebSockets/FakeWebSocket.ts +14 -1
|
@@ -18,6 +18,7 @@ export default class LslWebSocketBridgeTest extends AbstractPackageTest {
|
|
|
18
18
|
await super.beforeEach()
|
|
19
19
|
|
|
20
20
|
this.setFakeStreamInlet()
|
|
21
|
+
this.setFakeWebSocket()
|
|
21
22
|
this.setFakeWebSocketServer()
|
|
22
23
|
this.setSpyLslWebSocketBridge()
|
|
23
24
|
|
|
@@ -51,7 +52,7 @@ export default class LslWebSocketBridgeTest extends AbstractPackageTest {
|
|
|
51
52
|
type,
|
|
52
53
|
sourceId,
|
|
53
54
|
},
|
|
54
|
-
this.
|
|
55
|
+
this.inletOptions
|
|
55
56
|
)
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -93,7 +94,7 @@ export default class LslWebSocketBridgeTest extends AbstractPackageTest {
|
|
|
93
94
|
protected static async createsWebSocketServer() {
|
|
94
95
|
assert.isEqualDeep(
|
|
95
96
|
FakeWebSocketServer.callsToConstructor[0],
|
|
96
|
-
{ port: this.
|
|
97
|
+
{ port: this.listenPort },
|
|
97
98
|
'Did not create WebSocketServer!'
|
|
98
99
|
)
|
|
99
100
|
}
|
|
@@ -129,8 +130,14 @@ export default class LslWebSocketBridgeTest extends AbstractPackageTest {
|
|
|
129
130
|
this.activate()
|
|
130
131
|
this.simulateOnDataCallback()
|
|
131
132
|
|
|
133
|
+
const calls = FakeWebSocket.callsToSend.filter((call) =>
|
|
134
|
+
Array.from(FakeWebSocketServer.clients).some(
|
|
135
|
+
(client) => client.id === call.id
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
132
139
|
assert.isEqual(
|
|
133
|
-
|
|
140
|
+
calls.length,
|
|
134
141
|
0,
|
|
135
142
|
'Sent data to clients that are not ready!'
|
|
136
143
|
)
|
|
@@ -153,22 +160,105 @@ export default class LslWebSocketBridgeTest extends AbstractPackageTest {
|
|
|
153
160
|
|
|
154
161
|
assert.doesThrow(() => {
|
|
155
162
|
this.activate()
|
|
156
|
-
}, `\n\n Cannot
|
|
163
|
+
}, `\n\n Cannot activate bridge after destroying it! \n\n Please create and activate a new instance. \n`)
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
@test()
|
|
160
|
-
protected static async
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const wssPort = randomInt(1000, 9999)
|
|
167
|
+
protected static async throwsIfDeactivateIsCalledAfterDestroy() {
|
|
168
|
+
this.destroy()
|
|
164
169
|
|
|
165
|
-
|
|
170
|
+
assert.doesThrow(() => {
|
|
171
|
+
this.deactivate()
|
|
172
|
+
}, `\n\n Cannot deactivate bridge after destroying it! \n\n Please create and activate a new instance. \n`)
|
|
173
|
+
}
|
|
166
174
|
|
|
175
|
+
@test()
|
|
176
|
+
protected static async createsWebSocketServerFromPort() {
|
|
167
177
|
assert.isEqual(
|
|
168
178
|
FakeWebSocketServer.callsToConstructor[0]?.port,
|
|
169
|
-
|
|
170
|
-
'Did not
|
|
179
|
+
this.listenPort,
|
|
180
|
+
'Did not create WebSocketServer with port!'
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@test()
|
|
185
|
+
protected static async createsSocketsFromRemoteUrls() {
|
|
186
|
+
assert.isEqualDeep(
|
|
187
|
+
FakeWebSocket.callsToConstructor,
|
|
188
|
+
this.connectUrls,
|
|
189
|
+
'Did not create WebSockets for remote URLs!'
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@test()
|
|
194
|
+
protected static async activateSendsDataToRemoteSockets() {
|
|
195
|
+
this.activate()
|
|
196
|
+
|
|
197
|
+
const { samples, timestamps } = this.simulateOnDataCallback()
|
|
198
|
+
|
|
199
|
+
const expectedPayload = JSON.stringify({
|
|
200
|
+
samples,
|
|
201
|
+
timestamps,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
assert.isAbove(
|
|
205
|
+
FakeWebSocket.callsToSend.length,
|
|
206
|
+
0,
|
|
207
|
+
'Did not send data to remote sockets!'
|
|
171
208
|
)
|
|
209
|
+
|
|
210
|
+
for (const call of FakeWebSocket.callsToSend) {
|
|
211
|
+
assert.isEqualDeep(
|
|
212
|
+
call.data,
|
|
213
|
+
expectedPayload,
|
|
214
|
+
'Did not send data to remote sockets!'
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@test()
|
|
220
|
+
protected static async doesNotSendToRemoteSocketsThatAreNotReady() {
|
|
221
|
+
const sockets = this.instance[
|
|
222
|
+
'remoteSockets'
|
|
223
|
+
]! as unknown as FakeWebSocket[]
|
|
224
|
+
|
|
225
|
+
for (const socket of sockets) {
|
|
226
|
+
socket.readyState = WebSocket.CONNECTING
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.activate()
|
|
230
|
+
this.simulateOnDataCallback()
|
|
231
|
+
|
|
232
|
+
const calls = FakeWebSocket.callsToSend.filter((call) =>
|
|
233
|
+
sockets.some((socket) => socket.id === call.id)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
assert.isEqual(
|
|
237
|
+
calls.length,
|
|
238
|
+
0,
|
|
239
|
+
'Sent data to sockets that are not ready!'
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@test()
|
|
244
|
+
protected static async destroyClosesRemoteWebSockets() {
|
|
245
|
+
this.destroy()
|
|
246
|
+
|
|
247
|
+
assert.isEqual(
|
|
248
|
+
FakeWebSocket.numCallsToClose,
|
|
249
|
+
this.connectUrls.length,
|
|
250
|
+
'Did not close remote WebSockets!'
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@test()
|
|
255
|
+
protected static async throwsIfNeitherPortNorRemoteUrlsPassed() {
|
|
256
|
+
assert.doesThrow(() => {
|
|
257
|
+
this.LslWebSocketBridge({
|
|
258
|
+
listenPort: undefined,
|
|
259
|
+
connectUrls: undefined,
|
|
260
|
+
})
|
|
261
|
+
}, `At least one of listenPort or connectUrls must be provided!`)
|
|
172
262
|
}
|
|
173
263
|
|
|
174
264
|
private static activate() {
|
|
@@ -191,9 +281,11 @@ export default class LslWebSocketBridgeTest extends AbstractPackageTest {
|
|
|
191
281
|
return { samples, timestamps }
|
|
192
282
|
}
|
|
193
283
|
|
|
194
|
-
private static readonly
|
|
284
|
+
private static readonly listenPort = randomInt(1000, 5000)
|
|
195
285
|
|
|
196
|
-
private static readonly
|
|
286
|
+
private static readonly connectUrls = [this.generateId(), this.generateId()]
|
|
287
|
+
|
|
288
|
+
private static readonly inletOptions = {
|
|
197
289
|
channelNames: this.channelNames,
|
|
198
290
|
channelFormat: 'float32' as const,
|
|
199
291
|
sampleRateHz: 100 * Math.random(),
|
|
@@ -203,6 +295,12 @@ export default class LslWebSocketBridgeTest extends AbstractPackageTest {
|
|
|
203
295
|
sourceId: this.generateId(),
|
|
204
296
|
}
|
|
205
297
|
|
|
298
|
+
private static readonly baseOptions = {
|
|
299
|
+
...this.inletOptions,
|
|
300
|
+
listenPort: this.listenPort,
|
|
301
|
+
connectUrls: this.connectUrls,
|
|
302
|
+
}
|
|
303
|
+
|
|
206
304
|
private static LslWebSocketBridge(
|
|
207
305
|
options?: Partial<WebSocketBridgeOptions>
|
|
208
306
|
) {
|
|
@@ -7,14 +7,18 @@ import LiblslAdapter from './LiblslAdapter.js'
|
|
|
7
7
|
export default class LslStreamInfo implements StreamInfo {
|
|
8
8
|
public static Class?: StreamInfoConstructor
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
private static instanceCache: Record<string, StreamInfo> = {}
|
|
11
|
+
|
|
12
|
+
public readonly name: string
|
|
13
|
+
public readonly type: string
|
|
14
|
+
public readonly sourceId: string
|
|
15
|
+
public readonly channelNames: string[]
|
|
16
|
+
public readonly channelCount: number
|
|
17
|
+
public readonly channelFormat: ChannelFormat
|
|
18
|
+
public readonly sampleRateHz: number
|
|
19
|
+
public readonly units: string
|
|
20
|
+
|
|
14
21
|
protected boundInfo!: BoundStreamInfo
|
|
15
|
-
private channelNames: string[]
|
|
16
|
-
private channelFormat: ChannelFormat
|
|
17
|
-
private sampleRateHz: number
|
|
18
22
|
|
|
19
23
|
private readonly defaultName = `lsl-stream-info-${generateId()}`
|
|
20
24
|
private readonly defaultType = generateId()
|
|
@@ -38,6 +42,7 @@ export default class LslStreamInfo implements StreamInfo {
|
|
|
38
42
|
this.type = type
|
|
39
43
|
this.sourceId = sourceId
|
|
40
44
|
this.channelNames = channelNames
|
|
45
|
+
this.channelCount = channelNames.length
|
|
41
46
|
this.channelFormat = channelFormat
|
|
42
47
|
this.sampleRateHz = sampleRateHz
|
|
43
48
|
this.units = units
|
|
@@ -47,7 +52,14 @@ export default class LslStreamInfo implements StreamInfo {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
public static Create(options: StreamInfoOptions) {
|
|
50
|
-
|
|
55
|
+
const key = JSON.stringify(options)
|
|
56
|
+
let instance = this.instanceCache[key]
|
|
57
|
+
|
|
58
|
+
if (instance === undefined) {
|
|
59
|
+
instance = new (this.Class ?? this)(options)
|
|
60
|
+
this.instanceCache[key] = instance
|
|
61
|
+
}
|
|
62
|
+
return instance
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
private createStreamInfo() {
|
|
@@ -61,10 +73,6 @@ export default class LslStreamInfo implements StreamInfo {
|
|
|
61
73
|
})
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
private get channelCount() {
|
|
65
|
-
return this.channelNames.length
|
|
66
|
-
}
|
|
67
|
-
|
|
68
76
|
private get lslChannelFormat() {
|
|
69
77
|
return CHANNEL_FORMATS_MAP[this.channelFormat]
|
|
70
78
|
}
|
|
@@ -91,6 +99,14 @@ export default class LslStreamInfo implements StreamInfo {
|
|
|
91
99
|
|
|
92
100
|
export interface StreamInfo {
|
|
93
101
|
destroy(): void
|
|
102
|
+
readonly name: string
|
|
103
|
+
readonly type: string
|
|
104
|
+
readonly sourceId: string
|
|
105
|
+
readonly units: string
|
|
106
|
+
readonly channelNames: string[]
|
|
107
|
+
readonly channelCount: number
|
|
108
|
+
readonly channelFormat: ChannelFormat
|
|
109
|
+
readonly sampleRateHz: number
|
|
94
110
|
readonly boundStreamInfo: BoundStreamInfo
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -73,8 +73,11 @@ export default class LslStreamInlet implements StreamInlet {
|
|
|
73
73
|
|
|
74
74
|
public static Create(options: StreamInletOptions, onData: OnDataCallback) {
|
|
75
75
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
76
|
-
const { maxBufferedMs, chunkSize, ...
|
|
76
|
+
const { maxBufferedMs, chunkSize, ...rest } = options
|
|
77
|
+
const infoOptions: StreamInfoOptions = rest
|
|
78
|
+
|
|
77
79
|
const info = this.LslStreamInfo(infoOptions)
|
|
80
|
+
|
|
78
81
|
return new (this.Class ?? this)(info, options, onData)
|
|
79
82
|
}
|
|
80
83
|
|
|
@@ -34,7 +34,7 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
34
34
|
public readonly units: string = 'N/A'
|
|
35
35
|
|
|
36
36
|
private info: StreamInfo
|
|
37
|
-
private
|
|
37
|
+
private boundOutlet!: BoundOutlet
|
|
38
38
|
private pushSampleMethod!: (options: unknown) => LslErrorCode
|
|
39
39
|
|
|
40
40
|
private lsl = LiblslAdapter.getInstance()
|
|
@@ -108,7 +108,7 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
private createStreamOutlet() {
|
|
111
|
-
this.
|
|
111
|
+
this.boundOutlet = this.lsl.createOutlet({
|
|
112
112
|
info: this.boundStreamInfo,
|
|
113
113
|
chunkSize: this.chunkSize,
|
|
114
114
|
maxBufferedMs: this.maxBufferedMs,
|
|
@@ -138,7 +138,7 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
138
138
|
const timestamp = this.lsl.localClock()
|
|
139
139
|
|
|
140
140
|
const err = this.pushSampleMethod({
|
|
141
|
-
outlet: this.
|
|
141
|
+
outlet: this.boundOutlet,
|
|
142
142
|
sample,
|
|
143
143
|
timestamp,
|
|
144
144
|
})
|
|
@@ -155,7 +155,7 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
private destroyBoundOutlet() {
|
|
158
|
-
this.lsl.destroyOutlet({ outlet: this.
|
|
158
|
+
this.lsl.destroyOutlet({ outlet: this.boundOutlet })
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
private static async waitToAllowSetup(waitAfterConstructionMs: number) {
|
|
@@ -194,12 +194,13 @@ export interface StreamOutlet {
|
|
|
194
194
|
readonly type: string
|
|
195
195
|
readonly sourceId: string
|
|
196
196
|
readonly channelNames: string[]
|
|
197
|
+
readonly channelCount: number
|
|
197
198
|
readonly channelFormat: ChannelFormat
|
|
198
199
|
readonly sampleRateHz: number
|
|
200
|
+
readonly units: string
|
|
199
201
|
readonly chunkSize: number
|
|
200
202
|
readonly maxBufferedMs: number
|
|
201
203
|
readonly manufacturer: string
|
|
202
|
-
readonly units: string
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
export type StreamOutletConstructor = new (
|
|
@@ -1,58 +1,92 @@
|
|
|
1
|
-
import { WebSocketServer } from 'ws'
|
|
1
|
+
import WebSocket, { WebSocketServer } from 'ws'
|
|
2
2
|
import { ChannelFormat } from './LiblslAdapter.js'
|
|
3
|
-
import LslStreamInlet, {
|
|
3
|
+
import LslStreamInlet, {
|
|
4
|
+
StreamInlet,
|
|
5
|
+
StreamInletOptions,
|
|
6
|
+
} from './LslStreamInlet.js'
|
|
4
7
|
|
|
5
8
|
export default class LslWebSocketBridge implements WebSocketBridge {
|
|
6
9
|
public static Class?: WebSocketBridgeConstructor
|
|
10
|
+
public static WS = WebSocket
|
|
7
11
|
public static WSS = WebSocketServer
|
|
8
12
|
|
|
9
13
|
private inlet: StreamInlet
|
|
10
|
-
private
|
|
14
|
+
private localServer?: WebSocketServer
|
|
15
|
+
private remoteSockets?: WebSocket[]
|
|
16
|
+
|
|
11
17
|
private isDestroyed = false
|
|
12
18
|
|
|
13
19
|
protected constructor(options: WebSocketBridgeConstructorOptions) {
|
|
14
|
-
const { inlet,
|
|
20
|
+
const { inlet, localServer, remoteSockets } = options
|
|
15
21
|
|
|
16
22
|
this.inlet = inlet
|
|
17
|
-
this.
|
|
23
|
+
this.localServer = localServer
|
|
24
|
+
this.remoteSockets = remoteSockets
|
|
25
|
+
|
|
26
|
+
this.throwIfNoLocalServerOrRemoteSockets()
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
public static Create(options: WebSocketBridgeOptions) {
|
|
21
|
-
const {
|
|
22
|
-
const
|
|
23
|
-
|
|
30
|
+
const { listenPort, connectUrls, ...rest } = options
|
|
31
|
+
const inletOptions: StreamInletOptions = rest
|
|
32
|
+
|
|
33
|
+
const localServer = this.WebSocketServer(listenPort)
|
|
34
|
+
const remoteSockets = this.createSocketsFrom(connectUrls)
|
|
35
|
+
|
|
36
|
+
const inlet = this.LslStreamInlet(
|
|
37
|
+
inletOptions,
|
|
38
|
+
localServer,
|
|
39
|
+
remoteSockets
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return new (this.Class ?? this)({
|
|
43
|
+
...options,
|
|
44
|
+
inlet,
|
|
45
|
+
localServer,
|
|
46
|
+
remoteSockets,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
24
49
|
|
|
25
|
-
|
|
50
|
+
private throwIfNoLocalServerOrRemoteSockets() {
|
|
51
|
+
if (!(this.localServer || this.remoteSockets)) {
|
|
52
|
+
throw new Error(this.insufficientOptionsError)
|
|
53
|
+
}
|
|
26
54
|
}
|
|
27
55
|
|
|
56
|
+
private readonly insufficientOptionsError = `At least one of listenPort or connectUrls must be provided!`
|
|
57
|
+
|
|
28
58
|
public activate() {
|
|
29
|
-
this.throwIfBridgeIsDestroyed()
|
|
59
|
+
this.throwIfBridgeIsDestroyed(this.cannotActivateMessage)
|
|
30
60
|
this.startPullingData()
|
|
31
61
|
}
|
|
32
62
|
|
|
33
|
-
private throwIfBridgeIsDestroyed() {
|
|
63
|
+
private throwIfBridgeIsDestroyed(err: string) {
|
|
34
64
|
if (this.isDestroyed) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`\n\n Cannot re-activate bridge after destroying it! \n\n Please create and activate a new instance. \n`
|
|
37
|
-
)
|
|
65
|
+
throw new Error(err)
|
|
38
66
|
}
|
|
39
67
|
}
|
|
40
68
|
|
|
69
|
+
private readonly cannotActivateMessage = `\n\n Cannot activate bridge after destroying it! \n\n Please create and activate a new instance. \n`
|
|
70
|
+
|
|
41
71
|
private startPullingData() {
|
|
42
72
|
this.inlet.startPulling()
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
public deactivate() {
|
|
76
|
+
this.throwIfBridgeIsDestroyed(this.cannotDeactivateMessage)
|
|
46
77
|
this.stopPullingData()
|
|
47
78
|
}
|
|
48
79
|
|
|
80
|
+
private readonly cannotDeactivateMessage = `\n\n Cannot deactivate bridge after destroying it! \n\n Please create and activate a new instance. \n`
|
|
81
|
+
|
|
49
82
|
private stopPullingData() {
|
|
50
83
|
this.inlet.stopPulling()
|
|
51
84
|
}
|
|
52
85
|
|
|
53
86
|
public destroy() {
|
|
54
87
|
this.destroyBoundInlet()
|
|
55
|
-
this.
|
|
88
|
+
this.closeLocalServerIfExists()
|
|
89
|
+
this.closeRemoteSocketsIfExists()
|
|
56
90
|
this.isDestroyed = true
|
|
57
91
|
}
|
|
58
92
|
|
|
@@ -60,44 +94,74 @@ export default class LslWebSocketBridge implements WebSocketBridge {
|
|
|
60
94
|
this.inlet.destroy()
|
|
61
95
|
}
|
|
62
96
|
|
|
63
|
-
private
|
|
64
|
-
this.
|
|
97
|
+
private closeLocalServerIfExists() {
|
|
98
|
+
this.localServer?.close()
|
|
65
99
|
}
|
|
66
100
|
|
|
67
|
-
private
|
|
101
|
+
private closeRemoteSocketsIfExists() {
|
|
102
|
+
this.remoteSockets?.forEach((socket) => socket.close())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static createOnDataCallback(
|
|
106
|
+
localServer?: WebSocketServer,
|
|
107
|
+
remoteSockets?: WebSocket[]
|
|
108
|
+
) {
|
|
68
109
|
return (samples: Float32Array, timestamps: Float64Array) => {
|
|
69
|
-
|
|
110
|
+
const payload = JSON.stringify({ samples, timestamps })
|
|
111
|
+
|
|
112
|
+
if (localServer) {
|
|
113
|
+
this.broadcastToClients(localServer, payload)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (remoteSockets) {
|
|
117
|
+
this.sendToSockets(remoteSockets, payload)
|
|
118
|
+
}
|
|
70
119
|
}
|
|
71
120
|
}
|
|
72
121
|
|
|
73
122
|
private static broadcastToClients(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
timestamps: Float64Array<ArrayBufferLike>
|
|
123
|
+
localServer: WebSocketServer,
|
|
124
|
+
payload: string
|
|
77
125
|
) {
|
|
78
|
-
for (const client of
|
|
126
|
+
for (const client of localServer.clients) {
|
|
79
127
|
if (client.readyState === WebSocket.OPEN) {
|
|
80
|
-
client.send(
|
|
81
|
-
JSON.stringify({
|
|
82
|
-
samples,
|
|
83
|
-
timestamps,
|
|
84
|
-
})
|
|
85
|
-
)
|
|
128
|
+
client.send(payload)
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
131
|
}
|
|
89
132
|
|
|
133
|
+
private static sendToSockets(remoteSockets: WebSocket[], payload: string) {
|
|
134
|
+
for (const socket of remoteSockets) {
|
|
135
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
136
|
+
socket.send(payload)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private static createSocketsFrom(connectUrls?: string | string[]) {
|
|
142
|
+
return connectUrls
|
|
143
|
+
? (Array.isArray(connectUrls) ? connectUrls : [connectUrls]).map(
|
|
144
|
+
(url) => this.WebSocket(url)
|
|
145
|
+
)
|
|
146
|
+
: undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private static WebSocket(url: string) {
|
|
150
|
+
return new this.WS(url)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private static WebSocketServer(listenPort?: number) {
|
|
154
|
+
return listenPort ? new this.WSS({ port: listenPort }) : undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
90
157
|
private static LslStreamInlet(
|
|
91
|
-
options:
|
|
92
|
-
|
|
158
|
+
options: StreamInletOptions,
|
|
159
|
+
localServer?: WebSocketServer,
|
|
160
|
+
remoteSockets?: WebSocket[]
|
|
93
161
|
) {
|
|
94
|
-
const onData = this.createOnDataCallback(
|
|
162
|
+
const onData = this.createOnDataCallback(localServer, remoteSockets)
|
|
95
163
|
return LslStreamInlet.Create(options, onData)
|
|
96
164
|
}
|
|
97
|
-
|
|
98
|
-
private static WebSocketServer(wssPort: number) {
|
|
99
|
-
return new this.WSS({ port: wssPort })
|
|
100
|
-
}
|
|
101
165
|
}
|
|
102
166
|
|
|
103
167
|
export interface WebSocketBridge {
|
|
@@ -118,11 +182,13 @@ export interface WebSocketBridgeOptions {
|
|
|
118
182
|
name?: string
|
|
119
183
|
type?: string
|
|
120
184
|
sourceId?: string
|
|
121
|
-
|
|
185
|
+
listenPort?: number
|
|
186
|
+
connectUrls?: string | string[]
|
|
122
187
|
}
|
|
123
188
|
|
|
124
189
|
export interface WebSocketBridgeConstructorOptions
|
|
125
190
|
extends WebSocketBridgeOptions {
|
|
126
191
|
inlet: StreamInlet
|
|
127
|
-
|
|
192
|
+
localServer?: WebSocketServer
|
|
193
|
+
remoteSockets?: WebSocket[]
|
|
128
194
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import generateId from '@neurodevs/generate-id'
|
|
2
|
+
|
|
1
3
|
import { BoundStreamInfo } from 'impl/LiblslAdapter.js'
|
|
2
4
|
import { StreamInfo, StreamInfoOptions } from '../../impl/LslStreamInfo.js'
|
|
3
5
|
|
|
@@ -7,6 +9,14 @@ export default class FakeStreamInfo implements StreamInfo {
|
|
|
7
9
|
|
|
8
10
|
public static boundStreamInfo = {} as BoundStreamInfo
|
|
9
11
|
|
|
12
|
+
public name = generateId()
|
|
13
|
+
public type = generateId()
|
|
14
|
+
public sourceId = generateId()
|
|
15
|
+
public channelNames: string[] = [generateId(), generateId()]
|
|
16
|
+
public channelFormat = 'float32' as const
|
|
17
|
+
public sampleRateHz = 0
|
|
18
|
+
public units = 'N/A'
|
|
19
|
+
|
|
10
20
|
public constructor(options: StreamInfoOptions) {
|
|
11
21
|
FakeStreamInfo.callsToConstructor.push(options)
|
|
12
22
|
}
|
|
@@ -15,6 +25,10 @@ export default class FakeStreamInfo implements StreamInfo {
|
|
|
15
25
|
FakeStreamInfo.numCallsToDestroy++
|
|
16
26
|
}
|
|
17
27
|
|
|
28
|
+
public get channelCount() {
|
|
29
|
+
return this.channelNames.length
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
public get boundStreamInfo() {
|
|
19
33
|
return FakeStreamInfo.boundStreamInfo
|
|
20
34
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import LslStreamInfo, { StreamInfoOptions } from '../../impl/LslStreamInfo.js'
|
|
2
2
|
|
|
3
3
|
export default class SpyStreamInfo extends LslStreamInfo {
|
|
4
|
+
public static numCallsToConstructor = 0
|
|
5
|
+
|
|
4
6
|
public constructor(options: StreamInfoOptions) {
|
|
5
7
|
super(options)
|
|
8
|
+
SpyStreamInfo.numCallsToConstructor++
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
public getBoundInfo() {
|
|
@@ -24,4 +27,8 @@ export default class SpyStreamInfo extends LslStreamInfo {
|
|
|
24
27
|
public getUnits() {
|
|
25
28
|
return this.units
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
public static resetTestDouble() {
|
|
32
|
+
SpyStreamInfo.numCallsToConstructor = 0
|
|
33
|
+
}
|
|
27
34
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import WebSocket, { WebSocketServer } from 'ws'
|
|
2
|
+
import LslWebSocketBridge, {
|
|
3
|
+
WebSocketBridge,
|
|
4
|
+
} from '../../impl/LslWebSocketBridge.js'
|
|
5
|
+
import FakeWebSocket from '../WebSockets/FakeWebSocket.js'
|
|
6
|
+
import FakeWebSocketServer from '../WebSockets/FakeWebSocketServer.js'
|
|
2
7
|
|
|
3
8
|
export default class FakeWebSocketBridge implements WebSocketBridge {
|
|
4
9
|
public static numCallsToConstructor = 0
|
|
@@ -23,9 +28,23 @@ export default class FakeWebSocketBridge implements WebSocketBridge {
|
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
public static resetTestDouble() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
this.numCallsToConstructor = 0
|
|
32
|
+
this.numCallsToActivate = 0
|
|
33
|
+
this.numCallsToDeactivate = 0
|
|
34
|
+
this.numCallsToDestroy = 0
|
|
35
|
+
|
|
36
|
+
this.setFakeWebSocket()
|
|
37
|
+
this.setFakeWebSocketServer()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private static setFakeWebSocket() {
|
|
41
|
+
LslWebSocketBridge.WS = FakeWebSocket as unknown as typeof WebSocket
|
|
42
|
+
FakeWebSocket.resetTestDouble()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static setFakeWebSocketServer() {
|
|
46
|
+
LslWebSocketBridge.WSS =
|
|
47
|
+
FakeWebSocketServer as unknown as typeof WebSocketServer
|
|
48
|
+
FakeWebSocketServer.resetTestDouble()
|
|
30
49
|
}
|
|
31
50
|
}
|
|
@@ -2,16 +2,29 @@ import generateId from '@neurodevs/generate-id'
|
|
|
2
2
|
import WebSocket from 'ws'
|
|
3
3
|
|
|
4
4
|
export default class FakeWebSocket {
|
|
5
|
+
public static callsToConstructor: (string | undefined)[] = []
|
|
5
6
|
public static callsToSend: { id: string; data: unknown }[] = []
|
|
7
|
+
public static numCallsToClose = 0
|
|
6
8
|
|
|
7
9
|
public readyState: number = WebSocket.OPEN
|
|
8
10
|
public id = generateId()
|
|
9
11
|
|
|
12
|
+
public constructor(url?: string) {
|
|
13
|
+
FakeWebSocket.callsToConstructor.push(url)
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
public send(data: unknown) {
|
|
11
17
|
FakeWebSocket.callsToSend.push({ id: this.id, data })
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
public close() {
|
|
21
|
+
FakeWebSocket.numCallsToClose++
|
|
22
|
+
this.readyState = WebSocket.CLOSED
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
public static resetTestDouble() {
|
|
15
|
-
|
|
26
|
+
this.callsToConstructor = []
|
|
27
|
+
this.callsToSend = []
|
|
28
|
+
this.numCallsToClose = 0
|
|
16
29
|
}
|
|
17
30
|
}
|