@neurodevs/ndx-native 0.0.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.
Files changed (67) hide show
  1. package/.nvmrc +1 -0
  2. package/.vscode/launch.json +58 -0
  3. package/.vscode/settings.json +67 -0
  4. package/.vscode/tasks.json +130 -0
  5. package/LICENSE +21 -0
  6. package/README.md +2 -0
  7. package/build/.spruce/settings.json +11 -0
  8. package/build/__tests__/AbstractPackageTest.d.ts +7 -0
  9. package/build/__tests__/AbstractPackageTest.js +14 -0
  10. package/build/__tests__/AbstractPackageTest.js.map +1 -0
  11. package/build/__tests__/impl/LabrecorderAdapter.test.d.ts +30 -0
  12. package/build/__tests__/impl/LabrecorderAdapter.test.js +164 -0
  13. package/build/__tests__/impl/LabrecorderAdapter.test.js.map +1 -0
  14. package/build/__tests__/impl/LiblslAdapter.test.d.ts +84 -0
  15. package/build/__tests__/impl/LiblslAdapter.test.js +750 -0
  16. package/build/__tests__/impl/LiblslAdapter.test.js.map +1 -0
  17. package/build/__tests__/impl/LibxdfAdapter.test.d.ts +45 -0
  18. package/build/__tests__/impl/LibxdfAdapter.test.js +243 -0
  19. package/build/__tests__/impl/LibxdfAdapter.test.js.map +1 -0
  20. package/build/consts.d.ts +11 -0
  21. package/build/consts.js +21 -0
  22. package/build/consts.js.map +1 -0
  23. package/build/impl/LabrecorderAdapter.d.ts +30 -0
  24. package/build/impl/LabrecorderAdapter.js +69 -0
  25. package/build/impl/LabrecorderAdapter.js.map +1 -0
  26. package/build/impl/LiblslAdapter.d.ts +172 -0
  27. package/build/impl/LiblslAdapter.js +360 -0
  28. package/build/impl/LiblslAdapter.js.map +1 -0
  29. package/build/impl/LibxdfAdapter.d.ts +92 -0
  30. package/build/impl/LibxdfAdapter.js +130 -0
  31. package/build/impl/LibxdfAdapter.js.map +1 -0
  32. package/build/index.d.ts +14 -0
  33. package/build/index.js +18 -0
  34. package/build/index.js.map +1 -0
  35. package/build/lib/handleError.d.ts +8 -0
  36. package/build/lib/handleError.js +25 -0
  37. package/build/lib/handleError.js.map +1 -0
  38. package/build/testDoubles/Labrecorder/FakeLabrecorder.d.ts +15 -0
  39. package/build/testDoubles/Labrecorder/FakeLabrecorder.js +26 -0
  40. package/build/testDoubles/Labrecorder/FakeLabrecorder.js.map +1 -0
  41. package/build/testDoubles/Liblsl/FakeLiblsl.d.ts +57 -0
  42. package/build/testDoubles/Liblsl/FakeLiblsl.js +140 -0
  43. package/build/testDoubles/Liblsl/FakeLiblsl.js.map +1 -0
  44. package/build/testDoubles/Libxdf/FakeLibxdf.d.ts +13 -0
  45. package/build/testDoubles/Libxdf/FakeLibxdf.js +25 -0
  46. package/build/testDoubles/Libxdf/FakeLibxdf.js.map +1 -0
  47. package/build/testDoubles/Libxdf/SpyLibxdf.d.ts +6 -0
  48. package/build/testDoubles/Libxdf/SpyLibxdf.js +10 -0
  49. package/build/testDoubles/Libxdf/SpyLibxdf.js.map +1 -0
  50. package/eslint.config.mjs +3 -0
  51. package/package.json +84 -0
  52. package/src/.spruce/settings.json +11 -0
  53. package/src/__tests__/AbstractPackageTest.ts +16 -0
  54. package/src/__tests__/impl/LabrecorderAdapter.test.ts +197 -0
  55. package/src/__tests__/impl/LiblslAdapter.test.ts +907 -0
  56. package/src/__tests__/impl/LibxdfAdapter.test.ts +290 -0
  57. package/src/consts.ts +21 -0
  58. package/src/impl/LabrecorderAdapter.ts +100 -0
  59. package/src/impl/LiblslAdapter.ts +616 -0
  60. package/src/impl/LibxdfAdapter.ts +227 -0
  61. package/src/index.ts +26 -0
  62. package/src/lib/handleError.ts +24 -0
  63. package/src/testDoubles/Labrecorder/FakeLabrecorder.ts +37 -0
  64. package/src/testDoubles/Liblsl/FakeLiblsl.ts +204 -0
  65. package/src/testDoubles/Libxdf/FakeLibxdf.ts +33 -0
  66. package/src/testDoubles/Libxdf/SpyLibxdf.ts +13 -0
  67. package/tsconfig.json +28 -0
@@ -0,0 +1,907 @@
1
+ import { randomInt } from 'crypto'
2
+ import { test, assert } from '@neurodevs/node-tdd'
3
+ import {
4
+ createPointer,
5
+ DataType,
6
+ FieldType,
7
+ FuncObj,
8
+ OpenParams,
9
+ unwrapPointer,
10
+ } from 'ffi-rs'
11
+
12
+ import {
13
+ ChildHandle,
14
+ DescriptionHandle,
15
+ InletHandle,
16
+ OutletHandle,
17
+ InfoHandle,
18
+ Liblsl,
19
+ LiblslBindings,
20
+ LslChannel,
21
+ ResolveByPropOptions,
22
+ } from 'impl/LiblslAdapter.js'
23
+ import LiblslAdapter from '../../impl/LiblslAdapter.js'
24
+ import FakeLiblsl from '../../testDoubles/Liblsl/FakeLiblsl.js'
25
+ import AbstractPackageTest from '../AbstractPackageTest.js'
26
+
27
+ export default class LiblslAdapterTest extends AbstractPackageTest {
28
+ private static instance: Liblsl
29
+ private static libraryPath?: string
30
+ private static libraryOptions?: Record<string, any>
31
+
32
+ private static fakeBindings: LiblslBindings
33
+ private static shouldThrowWhenCreatingBindings: boolean
34
+
35
+ private static fakeInfoHandle: InfoHandle
36
+ private static fakeOutletHandle: OutletHandle
37
+ private static fakeInletHandle: InletHandle = {}
38
+ private static fakeDescHandle: DescriptionHandle
39
+ private static fakeChildHandle: ChildHandle
40
+ private static fakeChildNamedChannel: ChildHandle
41
+
42
+ private static createStreamInfoParams?: any[]
43
+ private static destroyStreamInfoParams?: any[]
44
+ private static appendChildParams: any[] = []
45
+ private static appendChildValueParams: any[]
46
+ private static appendChildHitCount: number
47
+ private static getDescriptionParams?: [InfoHandle]
48
+ private static getChannelCountParams?: [InfoHandle]
49
+
50
+ private static createOutletParams?: any[]
51
+ private static pushSampleFloatTimestampParams?: any[]
52
+ private static pushSampleStringTimestampParams?: any[]
53
+ private static destroyOutletParams?: any[]
54
+
55
+ private static createInletParams?: any[]
56
+ private static flushInletParams?: any[]
57
+ private static destroyInletParams?: any[]
58
+
59
+ private static localClockParams?: any[]
60
+
61
+ private static ffiRsOpenOptions?: OpenParams
62
+ private static ffiRsDefineOptions: FfiRsDefineOptions
63
+ private static ffiRsLoadOptions?: Record<string, any>
64
+
65
+ protected static async beforeEach() {
66
+ await super.beforeEach()
67
+
68
+ delete this.libraryPath
69
+ delete this.libraryOptions
70
+ delete this.createStreamInfoParams
71
+ delete this.createOutletParams
72
+ delete this.destroyOutletParams
73
+ delete this.createInletParams
74
+ delete this.destroyInletParams
75
+ delete this.pushSampleFloatTimestampParams
76
+ delete this.getDescriptionParams
77
+ delete this.getChannelCountParams
78
+
79
+ this.appendChildParams = []
80
+ this.appendChildValueParams = []
81
+
82
+ process.env.LIBLSL_PATH = this.generateId()
83
+
84
+ this.fakeInfoHandle = {}
85
+ this.fakeDescHandle = {}
86
+ this.fakeOutletHandle = {}
87
+ this.fakeChildHandle = {}
88
+ this.fakeChildNamedChannel = {}
89
+ this.fakeBindings = this.FakeBindings()
90
+
91
+ this.shouldThrowWhenCreatingBindings = false
92
+
93
+ this.appendChildHitCount = 0
94
+
95
+ delete this.ffiRsOpenOptions
96
+ LiblslAdapter.open = (options) => {
97
+ this.ffiRsOpenOptions = options
98
+ }
99
+
100
+ LiblslAdapter.define = (options) => {
101
+ this.ffiRsDefineOptions = options as FfiRsDefineOptions
102
+ if (this.shouldThrowWhenCreatingBindings) {
103
+ throw new Error('Failed to create bindings!')
104
+ }
105
+ return this.fakeBindings as any
106
+ }
107
+
108
+ LiblslAdapter.load = (options) => {
109
+ this.ffiRsLoadOptions = options
110
+ return this.fakeNumResolveResults as any
111
+ }
112
+
113
+ LiblslAdapter.resetInstance()
114
+ this.instance = LiblslAdapter.getInstance()
115
+ }
116
+
117
+ @test()
118
+ protected static async createsInstance() {
119
+ assert.isTruthy(this.instance, 'Failed to create instance!')
120
+ }
121
+
122
+ @test()
123
+ protected static async throwsWhenBindingsFailToLoad() {
124
+ this.shouldThrowWhenCreatingBindings = true
125
+ LiblslAdapter.resetInstance()
126
+
127
+ const err = assert.doesThrow(() => LiblslAdapter.getInstance())
128
+
129
+ assert.isTrue(
130
+ err.message.includes(this.generateFailedMessage()),
131
+ 'Did not receive the expected error!'
132
+ )
133
+ }
134
+
135
+ @test()
136
+ protected static async callsOpenOnFfiRs() {
137
+ assert.isEqualDeep(this.ffiRsOpenOptions, {
138
+ library: 'lsl',
139
+ path: process.env.LIBLSL_PATH,
140
+ })
141
+ }
142
+
143
+ @test()
144
+ protected static async worksAsASingleton() {
145
+ const liblsl = LiblslAdapter.getInstance()
146
+ //@ts-ignore
147
+ assert.isInstanceOf(liblsl, LiblslAdapter)
148
+ }
149
+
150
+ @test()
151
+ protected static async singletonIsTheSame() {
152
+ assert.isEqual(LiblslAdapter.getInstance(), LiblslAdapter.getInstance())
153
+ }
154
+
155
+ @test()
156
+ protected static canSetInstance() {
157
+ const fake = new FakeLiblsl()
158
+ LiblslAdapter.setInstance(fake)
159
+ assert.isEqual(LiblslAdapter.getInstance(), fake)
160
+ }
161
+
162
+ @test()
163
+ protected static async createsExpectedBindingsWithFfiRs() {
164
+ assert.isEqualDeep(this.ffiRsDefineOptions, {
165
+ lsl_create_streaminfo: {
166
+ library: 'lsl',
167
+ retType: DataType.External,
168
+ paramsType: [
169
+ DataType.String,
170
+ DataType.String,
171
+ DataType.I32,
172
+ DataType.Double,
173
+ DataType.I32,
174
+ DataType.String,
175
+ ],
176
+ },
177
+ lsl_destroy_streaminfo: {
178
+ library: 'lsl',
179
+ retType: DataType.Void,
180
+ paramsType: [DataType.External],
181
+ },
182
+ lsl_create_outlet: {
183
+ library: 'lsl',
184
+ retType: DataType.External,
185
+ paramsType: [DataType.External, DataType.I32, DataType.I32],
186
+ },
187
+ lsl_push_sample_ft: {
188
+ library: 'lsl',
189
+ retType: DataType.I32,
190
+ paramsType: [
191
+ DataType.External,
192
+ DataType.FloatArray,
193
+ DataType.Double,
194
+ ],
195
+ },
196
+ lsl_push_sample_strt: {
197
+ library: 'lsl',
198
+ retType: DataType.I32,
199
+ paramsType: [
200
+ DataType.External,
201
+ DataType.StringArray,
202
+ DataType.Double,
203
+ ],
204
+ },
205
+ lsl_destroy_outlet: {
206
+ library: 'lsl',
207
+ retType: DataType.Void,
208
+ paramsType: [DataType.External],
209
+ },
210
+ lsl_create_inlet: {
211
+ library: 'lsl',
212
+ retType: DataType.External,
213
+ paramsType: [
214
+ DataType.External,
215
+ DataType.I32,
216
+ DataType.I32,
217
+ DataType.I32,
218
+ ],
219
+ },
220
+ lsl_inlet_flush: {
221
+ library: 'lsl',
222
+ retType: DataType.I32,
223
+ paramsType: [DataType.External],
224
+ },
225
+ lsl_destroy_inlet: {
226
+ library: 'lsl',
227
+ retType: DataType.Void,
228
+ paramsType: [DataType.External],
229
+ },
230
+ lsl_get_desc: {
231
+ library: 'lsl',
232
+ retType: DataType.External,
233
+ paramsType: [DataType.External],
234
+ },
235
+ lsl_get_channel_count: {
236
+ library: 'lsl',
237
+ retType: DataType.I32,
238
+ paramsType: [DataType.External],
239
+ },
240
+ lsl_append_child: {
241
+ library: 'lsl',
242
+ retType: DataType.External,
243
+ paramsType: [DataType.External, DataType.String],
244
+ },
245
+ lsl_append_child_value: {
246
+ library: 'lsl',
247
+ retType: DataType.External,
248
+ paramsType: [
249
+ DataType.External,
250
+ DataType.String,
251
+ DataType.String,
252
+ ],
253
+ },
254
+ lsl_local_clock: {
255
+ library: 'lsl',
256
+ retType: DataType.Double,
257
+ paramsType: [],
258
+ },
259
+ })
260
+ }
261
+
262
+ @test()
263
+ protected static async createsStreamInfoWithRequiredParams() {
264
+ const options = this.generateRandomCreateStreamInfoOptions()
265
+ const actual = this.instance.createStreamInfo(options)
266
+
267
+ assert.isEqual(actual, this.fakeInfoHandle)
268
+ assert.isEqualDeep(this.createStreamInfoParams, Object.values(options))
269
+ }
270
+
271
+ @test()
272
+ protected static async destroyStreamInfoPasses() {
273
+ const infoHandle = this.createRandomInfoHandle()
274
+ this.instance.destroyStreamInfo({ infoHandle })
275
+
276
+ assert.isEqualDeep(
277
+ this.destroyStreamInfoParams,
278
+ [infoHandle],
279
+ 'Did not call destroyStreamInfo with expected params!'
280
+ )
281
+ }
282
+
283
+ @test()
284
+ protected static async resolvesStreamInfoByProp() {
285
+ await this.resolveByProp()
286
+
287
+ assert.isEqualDeep(
288
+ this.ffiRsLoadOptions,
289
+ {
290
+ library: 'lsl',
291
+ funcName: 'lsl_resolve_byprop',
292
+ retType: DataType.I32,
293
+ paramsType: [
294
+ DataType.External,
295
+ DataType.I32,
296
+ DataType.String,
297
+ DataType.String,
298
+ DataType.I32,
299
+ DataType.Double,
300
+ ],
301
+ paramsValue: [
302
+ this.resultsBufferPtr,
303
+ this.maxResults,
304
+ this.prop,
305
+ this.value,
306
+ this.minResults,
307
+ this.timeoutMs / 1000,
308
+ ],
309
+ runInNewThread: true,
310
+ },
311
+ 'Did not call resolveByProp with expected params!'
312
+ )
313
+ }
314
+
315
+ @test()
316
+ protected static async resolveByPropAcceptsOptionalArgs() {
317
+ const minResults = randomInt(0, 10)
318
+ const timeoutMs = Math.random() * 1000
319
+
320
+ await this.resolveByProp({
321
+ minResults,
322
+ timeoutMs,
323
+ })
324
+
325
+ assert.isEqualDeep(
326
+ {
327
+ minResults: this.ffiRsLoadOptions?.['paramsValue']?.[4],
328
+ timeoutMs: this.ffiRsLoadOptions?.['paramsValue']?.[5],
329
+ },
330
+ { minResults, timeoutMs: timeoutMs / 1000 },
331
+ 'Did not call resolveByProp with expected params!'
332
+ )
333
+ }
334
+
335
+ @test()
336
+ protected static async returnsHandlesForResolvedStreamInfos() {
337
+ LiblslAdapter.alloc = (size: number) => {
338
+ const buffer = Buffer.alloc(size)
339
+
340
+ for (let i = 0; i < this.fakeNumResolveResults; i++) {
341
+ const offset = i * this.bytesPerPointer
342
+ buffer.writeBigUInt64LE(BigInt(i + 1), offset)
343
+ }
344
+
345
+ return buffer
346
+ }
347
+
348
+ const expectedHandles: bigint[] = []
349
+
350
+ for (let i = 0; i < this.fakeNumResolveResults; i++) {
351
+ expectedHandles.push(BigInt(i + 1))
352
+ }
353
+
354
+ const actualHandles = await this.resolveByProp()
355
+
356
+ const actual = actualHandles.map((_h, i) => (i + 1).toString())
357
+ const expected = expectedHandles.map((h) => h.toString())
358
+
359
+ assert.isEqualDeep(
360
+ actual,
361
+ expected,
362
+ 'Did not receive expected resolved stream info handles!'
363
+ )
364
+ }
365
+
366
+ @test()
367
+ protected static async createsOutletWithRequiredParams() {
368
+ const { options, outletHandle } = this.createRandomOutlet()
369
+
370
+ const expected = {
371
+ ...options,
372
+ maxBufferedMs: options.maxBufferedMs / 1000,
373
+ }
374
+
375
+ assert.isEqualDeep(this.createOutletParams, Object.values(expected))
376
+ assert.isEqual(outletHandle, this.fakeOutletHandle)
377
+ }
378
+
379
+ @test()
380
+ protected static async pushesFloatSample() {
381
+ const expected = [1.0, 2.0, 3.0]
382
+ const timestamp = randomInt(100)
383
+
384
+ const options = {
385
+ outletHandle: this.fakeOutletHandle,
386
+ sample: expected,
387
+ timestamp,
388
+ }
389
+
390
+ this.instance.pushSampleFloatTimestamp(options)
391
+
392
+ assert.isEqualDeep(this.pushSampleFloatTimestampParams, [
393
+ this.fakeOutletHandle,
394
+ expected,
395
+ timestamp,
396
+ ])
397
+ }
398
+
399
+ @test()
400
+ protected static async pushesStringSample() {
401
+ const expected = [this.generateId()]
402
+ const timestamp = randomInt(100)
403
+
404
+ const options = {
405
+ outletHandle: this.fakeOutletHandle,
406
+ sample: expected,
407
+ timestamp,
408
+ }
409
+
410
+ this.instance.pushSampleStringTimestamp(options)
411
+ assert.isEqual(
412
+ this.pushSampleStringTimestampParams?.[0],
413
+ this.fakeOutletHandle
414
+ )
415
+ assert.isEqualDeep(this.pushSampleStringTimestampParams?.[1], expected)
416
+ assert.isEqual(this.pushSampleStringTimestampParams?.[2], timestamp)
417
+ }
418
+
419
+ @test()
420
+ protected static async addingSingleChannelGetsDescription() {
421
+ const infoHandle = this.createRandomInfoHandle()
422
+ const channel: LslChannel = this.generateRandomChannelValues()
423
+
424
+ this.instance.appendChannelsToStreamInfo({
425
+ infoHandle,
426
+ channels: [channel],
427
+ })
428
+ assert.isEqualDeep(this.getDescriptionParams?.[0], [infoHandle])
429
+
430
+ assert.isEqual(this.appendChildParams?.[0][0], this.fakeDescHandle)
431
+ assert.isEqual(this.appendChildParams?.[0][1], 'channels')
432
+
433
+ assert.isEqual(this.appendChildParams?.[1][0], this.fakeChildHandle)
434
+ assert.isEqual(this.appendChildParams?.[1][1], 'channel')
435
+
436
+ assert.isLength(this.appendChildValueParams, 3)
437
+
438
+ for (let i = 0; i < 3; i++) {
439
+ const param = this.appendChildValueParams[i]
440
+ assert.isEqual(param[0], this.fakeChildNamedChannel)
441
+ }
442
+
443
+ assert.isEqual(this.appendChildValueParams[0][1], 'label')
444
+ assert.isEqual(this.appendChildValueParams[1][1], 'unit')
445
+ assert.isEqual(this.appendChildValueParams[2][1], 'type')
446
+
447
+ assert.isEqual(this.appendChildValueParams[0][2], channel.label)
448
+ assert.isEqual(this.appendChildValueParams[1][2], channel.units)
449
+ assert.isEqual(this.appendChildValueParams[2][2], channel.type)
450
+ }
451
+
452
+ @test()
453
+ protected static async addingMultpleChannelsAddsChildrenToChannelsChild() {
454
+ const infoHandle = this.createRandomInfoHandle()
455
+ const channel1 = this.generateRandomChannelValues()
456
+ const channel2 = this.generateRandomChannelValues()
457
+
458
+ this.instance.appendChannelsToStreamInfo({
459
+ infoHandle,
460
+ channels: [channel1, channel2],
461
+ })
462
+
463
+ assert.isEqual(this.appendChildParams?.[2][0], this.fakeChildHandle)
464
+ assert.isEqual(this.appendChildParams?.[2][1], 'channel')
465
+
466
+ assert.isEqual(this.appendChildValueParams[3][2], channel2.label)
467
+ assert.isEqual(this.appendChildValueParams[4][2], channel2.units)
468
+ assert.isEqual(this.appendChildValueParams[5][2], channel2.type)
469
+ }
470
+
471
+ @test()
472
+ protected static async canDestroyOutlet() {
473
+ const { outletHandle } = this.createRandomOutlet()
474
+ const options = { outletHandle }
475
+ this.instance.destroyOutlet(options)
476
+
477
+ assert.isEqualDeep(this.destroyOutletParams, Object.values(options))
478
+ }
479
+
480
+ @test()
481
+ protected static async callingLocalClockTwiceReturnsDifferentTimestamps() {
482
+ const t1 = this.instance.localClock()
483
+ await this.wait(10)
484
+ const t2 = this.instance.localClock()
485
+ assert.isNotEqual(t1, t2)
486
+ }
487
+
488
+ @test()
489
+ protected static async localClockBindingReceivesEmptyArray() {
490
+ delete this.localClockParams
491
+ this.instance.localClock()
492
+ assert.isEqualDeep(this.localClockParams, [])
493
+ }
494
+
495
+ @test()
496
+ protected static async defaultsToMacOsPath() {
497
+ delete process.env.LIBLSL_PATH
498
+
499
+ LiblslAdapter.resetInstance()
500
+ this.instance = LiblslAdapter.getInstance() as FakeLiblsl
501
+
502
+ assert.isEqual(
503
+ this.ffiRsOpenOptions?.path,
504
+ '/opt/homebrew/Cellar/lsl/1.16.2/lib/liblsl.1.16.2.dylib'
505
+ )
506
+ }
507
+
508
+ @test()
509
+ protected static async createInletWithRequiredParams() {
510
+ const { options, inletHandle } = this.createRandomInlet()
511
+ const { maxBufferedMs } = options
512
+
513
+ const expected = {
514
+ ...options,
515
+ maxBufferedMs: maxBufferedMs / 1000,
516
+ }
517
+
518
+ const maxChunkSize = 0
519
+ const shouldRecover = 1
520
+
521
+ assert.isEqualDeep(
522
+ this.createInletParams,
523
+ [...Object.values(expected), maxChunkSize, shouldRecover],
524
+ 'Did not call createInlet with expected params!'
525
+ )
526
+
527
+ assert.isEqual(
528
+ inletHandle,
529
+ this.fakeInletHandle,
530
+ 'Did not receive expected inlet!'
531
+ )
532
+ }
533
+
534
+ @test()
535
+ protected static async getsChannelCountFromInfoHandle() {
536
+ const infoHandle = this.createRandomInfoHandle()
537
+
538
+ const channelCount = this.instance.getChannelCount({ infoHandle })
539
+ assert.isEqual(channelCount, this.channelCount)
540
+ }
541
+
542
+ @test()
543
+ protected static async openStreamCallsBinding() {
544
+ const { inletHandle } = this.createRandomInlet()
545
+
546
+ const timeoutMs = randomInt(1000)
547
+
548
+ const errorCodePtr = unwrapPointer(
549
+ createPointer({
550
+ paramsType: [DataType.U8Array],
551
+ paramsValue: [new Int32Array(1)],
552
+ })
553
+ )[0]
554
+
555
+ await this.instance.openStream({
556
+ inletHandle,
557
+ timeoutMs,
558
+ errorCodePtr,
559
+ })
560
+
561
+ assert.isEqualDeep(
562
+ this.ffiRsLoadOptions,
563
+ {
564
+ library: 'lsl',
565
+ funcName: 'lsl_open_stream',
566
+ retType: DataType.Void,
567
+ paramsType: [
568
+ DataType.External,
569
+ DataType.Double,
570
+ DataType.External,
571
+ ],
572
+ paramsValue: [inletHandle, timeoutMs / 1000, errorCodePtr],
573
+ runInNewThread: true,
574
+ },
575
+ 'Did not call openStream with expected options!'
576
+ )
577
+ }
578
+
579
+ @test()
580
+ protected static async closeStreamCallsBinding() {
581
+ const { inletHandle } = this.createRandomInlet()
582
+
583
+ this.instance.closeStream({
584
+ inletHandle,
585
+ })
586
+
587
+ assert.isEqualDeep(
588
+ this.ffiRsLoadOptions,
589
+ {
590
+ library: 'lsl',
591
+ funcName: 'lsl_close_stream',
592
+ retType: DataType.Void,
593
+ paramsType: [DataType.External],
594
+ paramsValue: [inletHandle],
595
+ },
596
+ 'Did not call closeStream with expected options!'
597
+ )
598
+ }
599
+
600
+ @test()
601
+ protected static async pullSampleCallsBinding() {
602
+ const { inletHandle } = this.createRandomInlet()
603
+
604
+ const sampleBuffer = Buffer.alloc(4 * this.channelCount)
605
+ const sampleBufferElements = this.channelCount
606
+ const timeoutMs = 0.0
607
+
608
+ const sampleBufferPtr = unwrapPointer(
609
+ createPointer({
610
+ paramsType: [DataType.U8Array],
611
+ paramsValue: [sampleBuffer],
612
+ })
613
+ )[0]
614
+
615
+ const errorCodePtr = unwrapPointer(
616
+ createPointer({
617
+ paramsType: [DataType.U8Array],
618
+ paramsValue: [new Int32Array(1)],
619
+ })
620
+ )[0]
621
+
622
+ this.instance.pullSample({
623
+ inletHandle,
624
+ sampleBufferPtr,
625
+ sampleBufferElements,
626
+ timeoutMs,
627
+ errorCodePtr,
628
+ })
629
+
630
+ assert.isEqualDeep(
631
+ this.ffiRsLoadOptions,
632
+ {
633
+ library: 'lsl',
634
+ funcName: 'lsl_pull_sample_f',
635
+ retType: DataType.Double,
636
+ paramsType: [
637
+ DataType.External,
638
+ DataType.External,
639
+ DataType.I32,
640
+ DataType.Double,
641
+ DataType.External,
642
+ ],
643
+ paramsValue: [
644
+ inletHandle,
645
+ sampleBufferPtr,
646
+ sampleBufferElements,
647
+ timeoutMs,
648
+ errorCodePtr,
649
+ ],
650
+ },
651
+ 'Did not call pullSample with expected options!'
652
+ )
653
+ }
654
+
655
+ @test()
656
+ protected static async pullChunkCallsBinding() {
657
+ const { inletHandle } = this.createRandomInlet()
658
+
659
+ const sampleBuffer = Buffer.alloc(
660
+ 4 * this.chunkSize * this.channelCount
661
+ )
662
+ const timestampBuffer = Buffer.alloc(8 * this.chunkSize)
663
+ const sampleBufferElements = this.chunkSize * this.channelCount
664
+ const timestampBufferElements = this.chunkSize
665
+ const timeoutMs = 0.0
666
+
667
+ const sampleBufferPtr = unwrapPointer(
668
+ createPointer({
669
+ paramsType: [DataType.U8Array],
670
+ paramsValue: [sampleBuffer],
671
+ })
672
+ )[0]
673
+
674
+ const timestampBufferPtr = unwrapPointer(
675
+ createPointer({
676
+ paramsType: [DataType.U8Array],
677
+ paramsValue: [timestampBuffer],
678
+ })
679
+ )[0]
680
+
681
+ const errorCodePtr = unwrapPointer(
682
+ createPointer({
683
+ paramsType: [DataType.U8Array],
684
+ paramsValue: [new Int32Array(1)],
685
+ })
686
+ )[0]
687
+
688
+ this.instance.pullChunk({
689
+ inletHandle,
690
+ sampleBufferPtr,
691
+ timestampBufferPtr,
692
+ sampleBufferElements,
693
+ timestampBufferElements,
694
+ timeoutMs,
695
+ errorCodePtr,
696
+ })
697
+
698
+ assert.isEqualDeep(
699
+ this.ffiRsLoadOptions,
700
+ {
701
+ library: 'lsl',
702
+ funcName: 'lsl_pull_chunk_f',
703
+ retType: DataType.Double,
704
+ paramsType: [
705
+ DataType.External,
706
+ DataType.External,
707
+ DataType.External,
708
+ DataType.I32,
709
+ DataType.I32,
710
+ DataType.Double,
711
+ DataType.External,
712
+ ],
713
+ paramsValue: [
714
+ inletHandle,
715
+ sampleBufferPtr,
716
+ timestampBufferPtr,
717
+ sampleBufferElements,
718
+ timestampBufferElements,
719
+ timeoutMs,
720
+ errorCodePtr,
721
+ ],
722
+ },
723
+ 'Did not call pullChunk with expected options!'
724
+ )
725
+ }
726
+
727
+ @test()
728
+ protected static async flushInletCallsBinding() {
729
+ const { inletHandle } = this.createRandomInlet()
730
+ this.instance.flushInlet({ inletHandle })
731
+
732
+ assert.isEqualDeep(
733
+ this.flushInletParams,
734
+ [inletHandle],
735
+ 'Should have called flushInlet with expected params!'
736
+ )
737
+ }
738
+
739
+ @test()
740
+ protected static async destroyInletCallsBinding() {
741
+ const { inletHandle } = this.createRandomInlet()
742
+ this.instance.destroyInlet({ inletHandle })
743
+
744
+ assert.isEqualDeep(
745
+ this.destroyInletParams,
746
+ [inletHandle],
747
+ 'Should have called destroyInlet with expected params!'
748
+ )
749
+ }
750
+
751
+ private static createRandomInfoHandle() {
752
+ return this.instance.createStreamInfo(
753
+ this.generateRandomCreateStreamInfoOptions()
754
+ )
755
+ }
756
+
757
+ private static async resolveByProp(
758
+ options?: Partial<ResolveByPropOptions>
759
+ ) {
760
+ return this.instance.resolveByProp({
761
+ prop: this.prop,
762
+ value: this.value,
763
+ minResults: this.minResults,
764
+ timeoutMs: this.timeoutMs,
765
+ ...options,
766
+ })
767
+ }
768
+
769
+ private static createRandomOutlet() {
770
+ const options = this.createRandomOutletOptions()
771
+ const outletHandle = this.instance.createOutlet(options)
772
+ return { options, outletHandle }
773
+ }
774
+
775
+ private static createRandomOutletOptions() {
776
+ const infoHandle = this.createRandomInfoHandle()
777
+
778
+ return {
779
+ infoHandle,
780
+ chunkSize: randomInt(10),
781
+ maxBufferedMs: randomInt(10),
782
+ }
783
+ }
784
+
785
+ private static createRandomInlet() {
786
+ const options = this.createRandomInletOptions()
787
+ const inletHandle = this.instance.createInlet(options)
788
+ return { options, inletHandle }
789
+ }
790
+
791
+ private static createRandomInletOptions() {
792
+ const infoHandle = this.createRandomInfoHandle()
793
+
794
+ return {
795
+ infoHandle,
796
+ maxBufferedMs: randomInt(10),
797
+ }
798
+ }
799
+
800
+ private static generateRandomChannelValues() {
801
+ return {
802
+ label: this.generateId(),
803
+ type: this.generateId(),
804
+ units: this.generateId(),
805
+ }
806
+ }
807
+
808
+ private static generateRandomCreateStreamInfoOptions() {
809
+ return {
810
+ name: this.generateId(),
811
+ type: this.generateId(),
812
+ channelCount: randomInt(1, 10),
813
+ sampleRateHz: randomInt(100),
814
+ channelFormat: randomInt(7),
815
+ sourceId: this.generateId(),
816
+ }
817
+ }
818
+
819
+ // resolveByProp
820
+ private static readonly maxResults = 1024
821
+ private static readonly bytesPerPointer = 8
822
+ private static readonly minResults = 1
823
+ private static readonly timeoutMs = 1000
824
+ private static readonly prop = this.generateId()
825
+ private static readonly value = this.generateId()
826
+ private static readonly fakeNumResolveResults = 2
827
+
828
+ private static readonly resultsBuffer = Buffer.alloc(
829
+ this.maxResults * this.bytesPerPointer
830
+ )
831
+
832
+ private static readonly resultsBufferRef = createPointer({
833
+ paramsType: [DataType.U8Array],
834
+ paramsValue: [this.resultsBuffer],
835
+ })
836
+
837
+ private static readonly resultsBufferPtr = unwrapPointer(
838
+ this.resultsBufferRef
839
+ )[0]
840
+
841
+ private static generateFailedMessage() {
842
+ return `Loading the liblsl dylib failed! I tried to load it from ${process.env.LIBLSL_PATH}.`
843
+ }
844
+
845
+ private static FakeBindings() {
846
+ return {
847
+ lsl_create_streaminfo: (params: any[]) => {
848
+ this.createStreamInfoParams = params
849
+ return this.fakeInfoHandle
850
+ },
851
+ lsl_destroy_streaminfo: (params: any[]) => {
852
+ this.destroyStreamInfoParams = params
853
+ },
854
+ lsl_create_outlet: (params: any[]) => {
855
+ this.createOutletParams = params
856
+ return this.fakeOutletHandle
857
+ },
858
+ lsl_push_sample_ft: (params: any[]) => {
859
+ this.pushSampleFloatTimestampParams = params
860
+ return 0
861
+ },
862
+ lsl_push_sample_strt: (params: any[]) => {
863
+ this.pushSampleStringTimestampParams = params
864
+ return 0
865
+ },
866
+ lsl_destroy_outlet: (params: any[]) => {
867
+ this.destroyOutletParams = params
868
+ },
869
+ lsl_create_inlet: (params: any[]) => {
870
+ this.createInletParams = params
871
+ return this.fakeInletHandle
872
+ },
873
+ lsl_inlet_flush: (params: any[]) => {
874
+ this.flushInletParams = params
875
+ return 0
876
+ },
877
+ lsl_destroy_inlet: (params: any[]) => {
878
+ this.destroyInletParams = params
879
+ },
880
+ lsl_local_clock: (params: []) => {
881
+ this.localClockParams = params
882
+ return new Date().getTime()
883
+ },
884
+ lsl_get_channel_count: (info: InfoHandle) => {
885
+ this.getChannelCountParams = [info]
886
+ return this.channelCount
887
+ },
888
+ lsl_get_desc: (info: InfoHandle) => {
889
+ this.getDescriptionParams = [info]
890
+ return this.fakeDescHandle
891
+ },
892
+ lsl_append_child: (params: any) => {
893
+ this.appendChildParams.push(params)
894
+ if (this.appendChildHitCount === 0) {
895
+ this.appendChildHitCount++
896
+ return this.fakeChildHandle
897
+ }
898
+ return this.fakeChildNamedChannel
899
+ },
900
+ lsl_append_child_value: (params: any[]) => {
901
+ this.appendChildValueParams.push(params)
902
+ },
903
+ }
904
+ }
905
+ }
906
+
907
+ export type FfiRsDefineOptions = FuncObj<FieldType, boolean | undefined>