@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,616 @@
1
+ import {
2
+ createPointer,
3
+ DataType,
4
+ define,
5
+ JsExternal,
6
+ load,
7
+ open,
8
+ unwrapPointer,
9
+ } from 'ffi-rs'
10
+
11
+ import { CHANNEL_FORMATS } from '../consts.js'
12
+ import { LslErrorCode } from '../lib/handleError.js'
13
+
14
+ export default class LiblslAdapter implements Liblsl {
15
+ public static open = open
16
+ public static define = define
17
+ public static load = load
18
+ public static alloc = Buffer.alloc
19
+
20
+ private static instance?: Liblsl
21
+
22
+ public liblslPath: string
23
+ private bindings!: LiblslBindings
24
+
25
+ private readonly defaultMacOsPath = `/opt/homebrew/Cellar/lsl/1.16.2/lib/liblsl.1.16.2.dylib`
26
+
27
+ protected constructor() {
28
+ this.liblslPath = process.env.LIBLSL_PATH ?? this.defaultMacOsPath
29
+ this.tryToLoadBindings()
30
+ }
31
+
32
+ private tryToLoadBindings() {
33
+ try {
34
+ this.loadBindings()
35
+ } catch (error) {
36
+ this.throwFailedToLoadLiblsl(error as Error)
37
+ }
38
+ }
39
+
40
+ private throwFailedToLoadLiblsl(error: Error) {
41
+ throw new Error(
42
+ `Loading the liblsl dylib failed! I tried to load it from ${this.liblslPath}.\n\n${error.message}\n\n`
43
+ )
44
+ }
45
+
46
+ public static getInstance() {
47
+ if (!this.instance) {
48
+ this.setInstance(new this())
49
+ }
50
+ return this.instance!
51
+ }
52
+
53
+ public static setInstance(instance: Liblsl) {
54
+ this.instance = instance
55
+ }
56
+
57
+ public static resetInstance() {
58
+ delete this.instance
59
+ }
60
+
61
+ private loadBindings() {
62
+ this.openLiblsl()
63
+ this.bindings = this.defineLiblslBindings()
64
+ }
65
+
66
+ private openLiblsl() {
67
+ this.open({
68
+ library: 'lsl',
69
+ path: this.liblslPath,
70
+ })
71
+ }
72
+
73
+ private defineLiblslBindings() {
74
+ return this.define(this.liblslFuncs) as LiblslBindings
75
+ }
76
+
77
+ public createStreamInfo(options: CreateStreamInfoOptions) {
78
+ const {
79
+ name,
80
+ type,
81
+ sourceId,
82
+ channelCount,
83
+ channelFormat,
84
+ sampleRateHz,
85
+ } = options
86
+
87
+ return this.bindings.lsl_create_streaminfo([
88
+ name,
89
+ type,
90
+ channelCount,
91
+ sampleRateHz,
92
+ channelFormat,
93
+ sourceId,
94
+ ])
95
+ }
96
+
97
+ public appendChannelsToStreamInfo(
98
+ options: AppendChannelsToStreamInfoOptions
99
+ ) {
100
+ const { infoHandle, channels } = options
101
+
102
+ const description = this.bindings.lsl_get_desc([infoHandle])
103
+ const parent = this.bindings.lsl_append_child([description, 'channels'])
104
+
105
+ for (const channel of channels) {
106
+ const child = this.bindings.lsl_append_child([parent, 'channel'])
107
+ this.bindings.lsl_append_child_value([
108
+ child,
109
+ 'label',
110
+ channel.label,
111
+ ])
112
+ this.bindings.lsl_append_child_value([child, 'unit', channel.units])
113
+ this.bindings.lsl_append_child_value([child, 'type', channel.type])
114
+ }
115
+ }
116
+
117
+ public destroyStreamInfo(options: DestroyStreamInfoOptions) {
118
+ const { infoHandle } = options
119
+ this.bindings.lsl_destroy_streaminfo([infoHandle])
120
+ }
121
+
122
+ public async resolveByProp(options: ResolveByPropOptions) {
123
+ const { prop, value, minResults = 1, timeoutMs = 1000 } = options
124
+
125
+ const maxResults = 1024
126
+ const bytesPerPointer = 8
127
+
128
+ const resultsBuffer = this.alloc(maxResults * bytesPerPointer)
129
+
130
+ const resultsBufferPtr = unwrapPointer(
131
+ createPointer({
132
+ paramsType: [DataType.U8Array],
133
+ paramsValue: [resultsBuffer],
134
+ })
135
+ )[0]
136
+
137
+ const numResults = await this.load({
138
+ library: 'lsl',
139
+ funcName: 'lsl_resolve_byprop',
140
+ retType: DataType.I32,
141
+ paramsType: [
142
+ DataType.External,
143
+ DataType.I32,
144
+ DataType.String,
145
+ DataType.String,
146
+ DataType.I32,
147
+ DataType.Double,
148
+ ],
149
+ paramsValue: [
150
+ resultsBufferPtr,
151
+ maxResults,
152
+ prop,
153
+ value,
154
+ minResults,
155
+ timeoutMs / 1000,
156
+ ],
157
+ runInNewThread: true,
158
+ })
159
+
160
+ const handles: InfoHandle[] = []
161
+
162
+ for (let i = 0; i < numResults; i++) {
163
+ const handle = resultsBuffer.readBigUInt64LE(i * bytesPerPointer)
164
+
165
+ if (handle !== 0n) {
166
+ const handleRef = createPointer({
167
+ paramsType: [DataType.BigInt],
168
+ paramsValue: [handle],
169
+ })
170
+
171
+ const handlePtr = unwrapPointer(handleRef)[0]
172
+
173
+ handles.push(handlePtr)
174
+ }
175
+ }
176
+
177
+ return handles
178
+ }
179
+
180
+ public createOutlet(options: CreateOutletOptions) {
181
+ const { infoHandle, chunkSize, maxBufferedMs } = options
182
+
183
+ return this.bindings.lsl_create_outlet([
184
+ infoHandle,
185
+ chunkSize,
186
+ maxBufferedMs / 1000,
187
+ ])
188
+ }
189
+
190
+ public pushSampleFloatTimestamp(options: PushSampleFloatTimestampOptions) {
191
+ const { outletHandle, sample, timestamp } = options
192
+
193
+ return this.bindings.lsl_push_sample_ft([
194
+ outletHandle,
195
+ sample,
196
+ timestamp,
197
+ ])
198
+ }
199
+
200
+ public pushSampleStringTimestamp(
201
+ options: PushSampleStringTimestampOptions
202
+ ) {
203
+ const { outletHandle, sample, timestamp } = options
204
+
205
+ return this.bindings.lsl_push_sample_strt([
206
+ outletHandle,
207
+ sample,
208
+ timestamp,
209
+ ])
210
+ }
211
+
212
+ public destroyOutlet(options: DestroyOutletOptions) {
213
+ const { outletHandle } = options
214
+ this.bindings.lsl_destroy_outlet([outletHandle])
215
+ }
216
+
217
+ public createInlet(options: CreateInletOptions) {
218
+ const { infoHandle, maxBufferedMs } = options
219
+
220
+ return this.bindings.lsl_create_inlet([
221
+ infoHandle,
222
+ maxBufferedMs / 1000,
223
+ this.maxChunkSize,
224
+ this.shouldRecover,
225
+ ])
226
+ }
227
+
228
+ private readonly maxChunkSize = 0
229
+ private readonly shouldRecover = 1
230
+
231
+ public getChannelCount(options: GetChannelCountOptions) {
232
+ const { infoHandle } = options
233
+ return this.bindings.lsl_get_channel_count([infoHandle])
234
+ }
235
+
236
+ public async openStream(options: OpenStreamOptions) {
237
+ const { inletHandle, timeoutMs, errorCodePtr } = options
238
+
239
+ await this.load({
240
+ library: 'lsl',
241
+ funcName: 'lsl_open_stream',
242
+ retType: DataType.Void,
243
+ paramsType: [DataType.External, DataType.Double, DataType.External],
244
+ paramsValue: [inletHandle, timeoutMs / 1000, errorCodePtr],
245
+ runInNewThread: true,
246
+ })
247
+ }
248
+
249
+ public closeStream(options: CloseStreamOptions) {
250
+ const { inletHandle } = options
251
+
252
+ this.load({
253
+ library: 'lsl',
254
+ funcName: 'lsl_close_stream',
255
+ retType: DataType.Void,
256
+ paramsType: [DataType.External],
257
+ paramsValue: [inletHandle],
258
+ })
259
+ }
260
+
261
+ public pullSample(options: PullSampleOptions) {
262
+ const {
263
+ inletHandle,
264
+ sampleBufferPtr,
265
+ sampleBufferElements,
266
+ timeoutMs,
267
+ errorCodePtr: errorCodePtr,
268
+ } = options
269
+
270
+ return this.load({
271
+ library: 'lsl',
272
+ funcName: 'lsl_pull_sample_f',
273
+ retType: DataType.Double,
274
+ paramsType: [
275
+ DataType.External,
276
+ DataType.External,
277
+ DataType.I32,
278
+ DataType.Double,
279
+ DataType.External,
280
+ ],
281
+ paramsValue: [
282
+ inletHandle,
283
+ sampleBufferPtr,
284
+ sampleBufferElements,
285
+ timeoutMs / 1000,
286
+ errorCodePtr,
287
+ ],
288
+ })
289
+ }
290
+
291
+ public pullChunk(options: PullChunkOptions) {
292
+ const {
293
+ inletHandle,
294
+ sampleBufferPtr,
295
+ timestampBufferPtr,
296
+ sampleBufferElements,
297
+ timestampBufferElements,
298
+ timeoutMs,
299
+ errorCodePtr: errorCodePtr,
300
+ } = options
301
+
302
+ return this.load({
303
+ library: 'lsl',
304
+ funcName: 'lsl_pull_chunk_f',
305
+ retType: DataType.Double,
306
+ paramsType: [
307
+ DataType.External,
308
+ DataType.External,
309
+ DataType.External,
310
+ DataType.I32,
311
+ DataType.I32,
312
+ DataType.Double,
313
+ DataType.External,
314
+ ],
315
+ paramsValue: [
316
+ inletHandle,
317
+ sampleBufferPtr,
318
+ timestampBufferPtr,
319
+ sampleBufferElements,
320
+ timestampBufferElements,
321
+ timeoutMs / 1000,
322
+ errorCodePtr,
323
+ ],
324
+ })
325
+ }
326
+
327
+ public flushInlet(options: FlushInletOptions) {
328
+ const { inletHandle } = options
329
+ this.bindings.lsl_inlet_flush([inletHandle])
330
+ }
331
+
332
+ public destroyInlet(options: DestroyInletOptions) {
333
+ const { inletHandle } = options
334
+ this.bindings.lsl_destroy_inlet([inletHandle])
335
+ }
336
+
337
+ public localClock() {
338
+ return this.bindings.lsl_local_clock([])
339
+ }
340
+
341
+ private get open() {
342
+ return LiblslAdapter.open
343
+ }
344
+
345
+ private get define() {
346
+ return LiblslAdapter.define
347
+ }
348
+
349
+ private get load() {
350
+ return LiblslAdapter.load
351
+ }
352
+
353
+ private get alloc() {
354
+ return LiblslAdapter.alloc
355
+ }
356
+
357
+ private get liblslFuncs() {
358
+ return {
359
+ lsl_create_streaminfo: {
360
+ library: 'lsl',
361
+ retType: DataType.External,
362
+ paramsType: [
363
+ DataType.String,
364
+ DataType.String,
365
+ DataType.I32,
366
+ DataType.Double,
367
+ DataType.I32,
368
+ DataType.String,
369
+ ],
370
+ },
371
+ lsl_destroy_streaminfo: {
372
+ library: 'lsl',
373
+ retType: DataType.Void,
374
+ paramsType: [DataType.External],
375
+ },
376
+ lsl_create_outlet: {
377
+ library: 'lsl',
378
+ retType: DataType.External,
379
+ paramsType: [DataType.External, DataType.I32, DataType.I32],
380
+ },
381
+ lsl_push_sample_ft: {
382
+ library: 'lsl',
383
+ retType: DataType.I32,
384
+ paramsType: [
385
+ DataType.External,
386
+ DataType.FloatArray,
387
+ DataType.Double,
388
+ ],
389
+ },
390
+ lsl_push_sample_strt: {
391
+ library: 'lsl',
392
+ retType: DataType.I32,
393
+ paramsType: [
394
+ DataType.External,
395
+ DataType.StringArray,
396
+ DataType.Double,
397
+ ],
398
+ },
399
+ lsl_destroy_outlet: {
400
+ library: 'lsl',
401
+ retType: DataType.Void,
402
+ paramsType: [DataType.External],
403
+ },
404
+ lsl_create_inlet: {
405
+ library: 'lsl',
406
+ retType: DataType.External,
407
+ paramsType: [
408
+ DataType.External,
409
+ DataType.I32,
410
+ DataType.I32,
411
+ DataType.I32,
412
+ ],
413
+ },
414
+ lsl_inlet_flush: {
415
+ library: 'lsl',
416
+ retType: DataType.I32,
417
+ paramsType: [DataType.External],
418
+ },
419
+ lsl_destroy_inlet: {
420
+ library: 'lsl',
421
+ retType: DataType.Void,
422
+ paramsType: [DataType.External],
423
+ },
424
+ lsl_get_desc: {
425
+ library: 'lsl',
426
+ retType: DataType.External,
427
+ paramsType: [DataType.External],
428
+ },
429
+ lsl_get_channel_count: {
430
+ library: 'lsl',
431
+ retType: DataType.I32,
432
+ paramsType: [DataType.External],
433
+ },
434
+ lsl_append_child: {
435
+ library: 'lsl',
436
+ retType: DataType.External,
437
+ paramsType: [DataType.External, DataType.String],
438
+ },
439
+ lsl_append_child_value: {
440
+ library: 'lsl',
441
+ retType: DataType.External,
442
+ paramsType: [
443
+ DataType.External,
444
+ DataType.String,
445
+ DataType.String,
446
+ ],
447
+ },
448
+ lsl_local_clock: {
449
+ library: 'lsl',
450
+ retType: DataType.Double,
451
+ paramsType: [],
452
+ },
453
+ }
454
+ }
455
+ }
456
+
457
+ export interface Liblsl {
458
+ liblslPath: string
459
+
460
+ createStreamInfo(options: CreateStreamInfoOptions): InfoHandle
461
+ destroyStreamInfo(options: DestroyStreamInfoOptions): void
462
+ appendChannelsToStreamInfo(options: AppendChannelsToStreamInfoOptions): void
463
+ getChannelCount(options: GetChannelCountOptions): number
464
+
465
+ resolveByProp(options: ResolveByPropOptions): Promise<InfoHandle[]>
466
+
467
+ createOutlet(options: CreateOutletOptions): OutletHandle
468
+
469
+ pushSampleFloatTimestamp(
470
+ options: PushSampleFloatTimestampOptions
471
+ ): LslErrorCode
472
+
473
+ pushSampleStringTimestamp(
474
+ options: PushSampleStringTimestampOptions
475
+ ): LslErrorCode
476
+
477
+ destroyOutlet(options: DestroyOutletOptions): void
478
+
479
+ createInlet(options: CreateInletOptions): InletHandle
480
+ openStream(options: OpenStreamOptions): Promise<void>
481
+ closeStream(options: CloseStreamOptions): void
482
+ pullSample(options: PullSampleOptions): number
483
+ pullChunk(options: PullChunkOptions): number
484
+ flushInlet(options: FlushInletOptions): void
485
+ destroyInlet(options: DestroyInletOptions): void
486
+
487
+ localClock(): number
488
+ }
489
+
490
+ export interface CreateStreamInfoOptions {
491
+ name: string
492
+ type: string
493
+ sourceId: string
494
+ channelCount: number
495
+ channelFormat: number
496
+ sampleRateHz: number
497
+ manufacturer?: string
498
+ units?: string
499
+ }
500
+
501
+ export interface AppendChannelsToStreamInfoOptions {
502
+ infoHandle: InfoHandle
503
+ channels: LslChannel[]
504
+ }
505
+
506
+ export interface GetChannelCountOptions {
507
+ infoHandle: InfoHandle
508
+ }
509
+
510
+ export interface DestroyStreamInfoOptions {
511
+ infoHandle: InfoHandle
512
+ }
513
+
514
+ export interface ResolveByPropOptions {
515
+ prop: string
516
+ value: string
517
+ minResults?: number
518
+ timeoutMs?: number
519
+ }
520
+
521
+ export interface CreateOutletOptions {
522
+ infoHandle: InfoHandle
523
+ chunkSize: number
524
+ maxBufferedMs: number
525
+ }
526
+
527
+ export interface PushSampleFloatTimestampOptions {
528
+ outletHandle: OutletHandle
529
+ sample: number[]
530
+ timestamp: number
531
+ }
532
+
533
+ export interface PushSampleStringTimestampOptions {
534
+ outletHandle: OutletHandle
535
+ sample: string[]
536
+ timestamp: number
537
+ }
538
+
539
+ export interface DestroyOutletOptions {
540
+ outletHandle: OutletHandle
541
+ }
542
+
543
+ export interface CreateInletOptions {
544
+ infoHandle: InfoHandle
545
+ maxBufferedMs: number
546
+ }
547
+
548
+ export interface OpenStreamOptions {
549
+ inletHandle: InletHandle
550
+ timeoutMs: number
551
+ errorCodePtr: JsExternal
552
+ }
553
+
554
+ export interface CloseStreamOptions {
555
+ inletHandle: InletHandle
556
+ }
557
+
558
+ export interface PullSampleOptions {
559
+ inletHandle: InletHandle
560
+ sampleBufferPtr: JsExternal
561
+ sampleBufferElements: number
562
+ timeoutMs: number
563
+ errorCodePtr: JsExternal
564
+ }
565
+
566
+ export interface PullChunkOptions extends PullSampleOptions {
567
+ timestampBufferPtr: JsExternal
568
+ timestampBufferElements: number
569
+ }
570
+
571
+ export interface FlushInletOptions {
572
+ inletHandle: InletHandle
573
+ }
574
+
575
+ export interface DestroyInletOptions {
576
+ inletHandle: InletHandle
577
+ }
578
+
579
+ export interface LiblslBindings {
580
+ lsl_create_streaminfo(
581
+ args: [string, string, number, number, number, string]
582
+ ): InfoHandle
583
+
584
+ lsl_get_desc(args: [InfoHandle]): DescriptionHandle
585
+ lsl_get_channel_count(args: [InfoHandle]): number
586
+ lsl_append_child(args: [DescriptionHandle, string]): ChildHandle
587
+ lsl_append_child_value(args: [ChildHandle, string, string]): void
588
+ lsl_destroy_streaminfo(args: [InfoHandle]): void
589
+
590
+ lsl_create_outlet(args: [InfoHandle, number, number]): OutletHandle
591
+ lsl_push_sample_ft(args: [OutletHandle, LslSample, number]): LslErrorCode
592
+ lsl_push_sample_strt(args: [OutletHandle, LslSample, number]): LslErrorCode
593
+ lsl_destroy_outlet(args: [OutletHandle]): void
594
+
595
+ lsl_create_inlet(args: any): InletHandle
596
+ lsl_inlet_flush(args: [InletHandle]): void
597
+ lsl_destroy_inlet(args: any): void
598
+
599
+ lsl_local_clock(args: []): number
600
+ }
601
+
602
+ export type ChannelFormat = (typeof CHANNEL_FORMATS)[number]
603
+
604
+ export interface LslChannel {
605
+ label: string
606
+ units: string
607
+ type: string
608
+ }
609
+
610
+ export type LslSample = (number | string | undefined)[]
611
+
612
+ export interface InfoHandle {}
613
+ export interface OutletHandle {}
614
+ export interface InletHandle {}
615
+ export interface DescriptionHandle {}
616
+ export interface ChildHandle {}