@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,290 @@
1
+ import generateId from '@neurodevs/generate-id'
2
+ import {
3
+ MangledNameExtractor,
4
+ FakeNameExtractor,
5
+ } from '@neurodevs/node-mangled-names'
6
+ import { test, assert } from '@neurodevs/node-tdd'
7
+ import { DataType, OpenParams } from 'ffi-rs'
8
+
9
+ import LibxdfAdapter, {
10
+ FfiRsDefineOptions,
11
+ LibxdfBindings,
12
+ XdfFile,
13
+ } from '../../impl/LibxdfAdapter.js'
14
+ import SpyLibxdf from '../../testDoubles/Libxdf/SpyLibxdf.js'
15
+ import AbstractPackageTest from '../AbstractPackageTest.js'
16
+
17
+ export default class LibxdfTest extends AbstractPackageTest {
18
+ private static instance: SpyLibxdf
19
+ private static libxdfPath = generateId()
20
+ private static path = generateId()
21
+ private static shouldThrowWhenLoadingBindings: boolean
22
+ private static mangledLoadXdfName: string
23
+ private static fakeBindings: LibxdfBindings
24
+ private static ffiRsOpenOptions?: OpenParams
25
+ private static ffiRsDefineOptions?: FfiRsDefineOptions
26
+ private static loadXdfCalls: string[][] = []
27
+
28
+ protected static async beforeEach() {
29
+ await super.beforeEach()
30
+
31
+ LibxdfAdapter.Class = SpyLibxdf
32
+
33
+ MangledNameExtractor.Class = FakeNameExtractor
34
+ FakeNameExtractor.clearTestDouble()
35
+
36
+ this.shouldThrowWhenLoadingBindings = false
37
+ this.mangledLoadXdfName = this.generateMangledName()
38
+ this.fakeBindings = this.FakeBindings()
39
+
40
+ this.setFakeExtractResult()
41
+ this.clearAndFakeFfi()
42
+
43
+ this.instance = await this.LibxdfAdapter()
44
+ }
45
+
46
+ @test()
47
+ protected static canCreateInstance() {
48
+ assert.isTruthy(this.instance, 'Should create an instance!')
49
+ }
50
+
51
+ @test()
52
+ protected static async throwsWhenBindingsFailToLoad() {
53
+ this.shouldThrowWhenLoadingBindings = true
54
+
55
+ const err = await assert.doesThrowAsync(
56
+ async () => await this.LibxdfAdapter()
57
+ )
58
+
59
+ const actual = (err.message ?? err.stack).replace(/\s+/g, '')
60
+
61
+ const expected = this.generateFailedMessageWithFakeError().replace(
62
+ /\s+/g,
63
+ ''
64
+ )
65
+
66
+ assert.isEqual(actual, expected, 'Did not receive the expected error!')
67
+ }
68
+
69
+ @test()
70
+ protected static async callsFfiRsOpenWithRequiredOptions() {
71
+ assert.isEqualDeep(this.ffiRsOpenOptions, {
72
+ library: 'xdf',
73
+ path: this.libxdfPath,
74
+ })
75
+ }
76
+
77
+ @test()
78
+ protected static async callsFfiRsDefineWithRequiredOptions() {
79
+ const mangledLoadXdfName = generateId()
80
+ this.setFakeExtractResult(mangledLoadXdfName)
81
+
82
+ await this.LibxdfAdapter()
83
+
84
+ assert.isEqualDeep(
85
+ this.ffiRsDefineOptions,
86
+ {
87
+ [mangledLoadXdfName.slice(1)]: {
88
+ library: 'xdf',
89
+ retType: DataType.String,
90
+ paramsType: [DataType.String],
91
+ },
92
+ },
93
+ 'Please pass valid options to ffiRsDefine!'
94
+ )
95
+ }
96
+
97
+ @test()
98
+ protected static async loadXdfCallsBindings() {
99
+ this.loadXdf()
100
+
101
+ assert.isEqualDeep(
102
+ this.loadXdfCalls[0],
103
+ [this.path],
104
+ 'Should have called load_xdf_to_json(path)!'
105
+ )
106
+ }
107
+
108
+ @test()
109
+ protected static async createsMangledNameExtractor() {
110
+ assert.isEqual(FakeNameExtractor.numConstructorCalls, 1)
111
+ }
112
+
113
+ @test()
114
+ protected static async callsExtractorWithCorrectParams() {
115
+ assert.isEqualDeep(FakeNameExtractor.extractCalls[0], {
116
+ libPath: this.libxdfPath,
117
+ unmangledNames: [this.loadXdfName],
118
+ })
119
+ }
120
+
121
+ @test()
122
+ protected static async returnsResultInJson() {
123
+ const result = this.loadXdf()
124
+ assert.isEqualDeep(result, this.fakeParsedXdf)
125
+ }
126
+
127
+ @test()
128
+ protected static async throwsIfLibxdfPathDoesNotExist() {
129
+ this.libxdfPath = generateId()
130
+
131
+ const err = await assert.doesThrowAsync(
132
+ async () => await this.LibxdfAdapter(this.libxdfPath, true)
133
+ )
134
+
135
+ const actual = (err.message ?? err.stack).replace(/\s+/g, '')
136
+ const expected = this.generateFailedMessage().replace(/\s+/g, '')
137
+
138
+ assert.isEqual(actual, expected, 'Did not receive the expected error!')
139
+ }
140
+
141
+ @test()
142
+ protected static async doesNotThrowIfEventsUndefined() {
143
+ this.fakeSerializedXdf = `{"streams": [{"stream_id": ${this.fakeStreamId}, "time_series": [], "time_stamps": [], "stream_info": {"channel_count": ${this.fakeChannelCount}, "channel_format": "${this.fakeChannelFormat}", "nominal_srate": ${this.fakeNominalSampleRate}, "stream_name": "${this.fakeStreamName}", "stream_type": "${this.fakeStreamType}"}}]}`
144
+ this.loadXdf()
145
+ }
146
+
147
+ private static setFakeExtractResult(
148
+ mangledLoadXdfName = this.mangledLoadXdfName
149
+ ) {
150
+ FakeNameExtractor.fakeResult = {
151
+ load_xdf_to_json: mangledLoadXdfName,
152
+ }
153
+ }
154
+
155
+ private static generateMangledName() {
156
+ return `${generateId()}${this.loadXdfName}${generateId()}`
157
+ }
158
+
159
+ private static clearAndFakeFfi() {
160
+ delete this.ffiRsOpenOptions
161
+ delete this.ffiRsDefineOptions
162
+ this.fakeFfiRsOpen()
163
+ this.fakeFfiRsDefine()
164
+ }
165
+
166
+ private static fakeFfiRsOpen() {
167
+ LibxdfAdapter.ffiRsOpen = (options) => {
168
+ this.ffiRsOpenOptions = options
169
+ }
170
+ }
171
+
172
+ private static fakeFfiRsDefine() {
173
+ LibxdfAdapter.ffiRsDefine = (options) => {
174
+ this.ffiRsDefineOptions = options
175
+
176
+ if (this.shouldThrowWhenLoadingBindings) {
177
+ throw new Error(this.fakeErrorMessage)
178
+ }
179
+
180
+ return this.fakeBindings as any
181
+ }
182
+ }
183
+
184
+ private static generateFailedMessage() {
185
+ return `
186
+ \n -----------------------------------
187
+ \n Failed to load libxdf! Tried to load from:
188
+ \n ${this.libxdfPath}
189
+ \n Instructions to save your day (on MacOS):
190
+ \n 1. git clone https://github.com/neurodevs/libxdf.git
191
+ \n 2. cd libxdf && cmake -S . -B build && cmake --build build
192
+ \n 3. sudo cp build/libxdf.dylib /opt/local/lib/
193
+ \n 4. Try whatever you were doing again!
194
+ \n Modify step 3 for your OS if you are not on MacOS.
195
+ \n Check the official repo for OS-specific instructions:
196
+ \n https://github.com/xdf-modules/libxdf
197
+ \n If you're still unsure, ask an LLM with this error and your OS.
198
+ \n You could also post an issue on the repo:
199
+ \n https://github.com/neurodevs/node-xdf/issues
200
+ \n Good luck!
201
+ \n @ericthecurious
202
+ \n -----------------------------------
203
+ `
204
+ }
205
+
206
+ private static generateFailedMessageWithFakeError() {
207
+ return `
208
+ \n -----------------------------------
209
+ \n Failed to load libxdf! Tried to load from:
210
+ \n ${this.libxdfPath}
211
+ \n Instructions to save your day (on MacOS):
212
+ \n 1. git clone https://github.com/neurodevs/libxdf.git
213
+ \n 2. cd libxdf && cmake -S . -B build && cmake --build build
214
+ \n 3. sudo cp build/libxdf.dylib /opt/local/lib/
215
+ \n 4. Try whatever you were doing again!
216
+ \n Modify step 3 for your OS if you are not on MacOS.
217
+ \n Check the official repo for OS-specific instructions:
218
+ \n https://github.com/xdf-modules/libxdf
219
+ \n If you're still unsure, ask an LLM with this error and your OS.
220
+ \n You could also post an issue on the repo:
221
+ \n https://github.com/neurodevs/node-xdf/issues
222
+ \n Good luck!
223
+ \n @ericthecurious
224
+ \n -----------------------------------
225
+ \n Error: ${this.fakeErrorMessage}
226
+ \n
227
+ `
228
+ }
229
+
230
+ private static loadXdf() {
231
+ return this.instance.loadXdf(this.path)
232
+ }
233
+
234
+ private static readonly loadXdfName = 'load_xdf_to_json'
235
+
236
+ private static readonly fakeStreamId = 0
237
+ private static readonly fakeStreamName = generateId()
238
+ private static readonly fakeStreamType = generateId()
239
+ private static readonly fakeChannelCount = 0
240
+ private static readonly fakeChannelFormat = generateId()
241
+ private static readonly fakeNominalSampleRate = 0
242
+ private static readonly fakeEventName = generateId()
243
+ private static readonly fakeEventTimestamp = 0
244
+ private static readonly fakeErrorMessage = 'Fake error message!'
245
+
246
+ private static fakeSerializedXdf = `{"streams": [{"stream_id": ${this.fakeStreamId}, "time_series": [], "time_stamps": [], "stream_info": {"channel_count": ${this.fakeChannelCount}, "channel_format": "${this.fakeChannelFormat}", "nominal_srate": ${this.fakeNominalSampleRate}, "stream_name": "${this.fakeStreamName}", "stream_type": "${this.fakeStreamType}"}}], "events": [{"stream_id": ${this.fakeStreamId}, "event_name": "${this.fakeEventName}", "event_timestamp": ${this.fakeEventTimestamp}}]}`
247
+
248
+ private static readonly fakeParsedXdf: XdfFile = {
249
+ path: this.path,
250
+ streams: [
251
+ {
252
+ id: this.fakeStreamId,
253
+ name: this.fakeStreamName,
254
+ type: this.fakeStreamType,
255
+ channelCount: this.fakeChannelCount,
256
+ channelFormat: this.fakeChannelFormat,
257
+ nominalSampleRateHz: this.fakeNominalSampleRate,
258
+ data: [],
259
+ timestamps: [],
260
+ },
261
+ ],
262
+ events: [
263
+ {
264
+ name: this.fakeEventName,
265
+ timestamp: this.fakeEventTimestamp,
266
+ streamId: this.fakeStreamId,
267
+ },
268
+ ],
269
+ }
270
+
271
+ private static FakeBindings(mangledLoadXdfName = this.mangledLoadXdfName) {
272
+ return {
273
+ [mangledLoadXdfName.slice(1)]: (path: string[]) => {
274
+ this.loadXdfCalls.push(path)
275
+ return this.fakeSerializedXdf
276
+ },
277
+ }
278
+ }
279
+
280
+ private static async LibxdfAdapter(
281
+ libxdfPath?: string,
282
+ throwIfPathNotExists = false
283
+ ) {
284
+ const libxdf = await LibxdfAdapter.Create(
285
+ libxdfPath ?? this.libxdfPath,
286
+ throwIfPathNotExists
287
+ )
288
+ return libxdf as SpyLibxdf
289
+ }
290
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,21 @@
1
+ export const CHANNEL_FORMATS = [
2
+ 'undefined',
3
+ 'float32',
4
+ 'double64',
5
+ 'string',
6
+ 'int32',
7
+ 'int16',
8
+ 'int8',
9
+ 'int64',
10
+ ] as const
11
+
12
+ export const CHANNEL_FORMATS_MAP = {
13
+ undefined: 0,
14
+ float32: 1,
15
+ double64: 2,
16
+ string: 3,
17
+ int32: 4,
18
+ int16: 5,
19
+ int8: 6,
20
+ int64: 7,
21
+ }
@@ -0,0 +1,100 @@
1
+ import { DataType, define, open } from 'ffi-rs'
2
+
3
+ export default class LabrecorderAdapter implements Labrecorder {
4
+ public static Class?: LabrecorderConstructor
5
+ public static ffiRsOpen = open
6
+ public static ffiRsDefine = define
7
+
8
+ private labrecorderPath: string
9
+ private bindings: LabrecorderBindings
10
+
11
+ protected constructor(labrecorderPath: string) {
12
+ this.labrecorderPath = labrecorderPath
13
+ this.loadCppLibrary()
14
+
15
+ this.bindings = this.defineBindingFunctions()
16
+ }
17
+
18
+ public static Create(labrecorderPath?: string) {
19
+ const path = labrecorderPath ?? '/opt/local/lib/liblabrecorder.dylib'
20
+ return new (this.Class ?? this)(path)
21
+ }
22
+
23
+ private loadCppLibrary() {
24
+ this.ffiRsOpen({
25
+ library: 'labrecorder',
26
+ path: this.labrecorderPath,
27
+ })
28
+ }
29
+
30
+ private defineBindingFunctions() {
31
+ return this.ffiRsDefine(this.functions) as LabrecorderBindings
32
+ }
33
+
34
+ private get functions() {
35
+ return {
36
+ recording_create: {
37
+ library: 'labrecorder',
38
+ retType: DataType.External,
39
+ paramsType: [
40
+ DataType.String,
41
+ DataType.StringArray,
42
+ DataType.U64,
43
+ ],
44
+ },
45
+ recording_stop: {
46
+ library: 'labrecorder',
47
+ retType: DataType.Void,
48
+ paramsType: [DataType.External],
49
+ },
50
+ recording_delete: {
51
+ library: 'labrecorder',
52
+ retType: DataType.Void,
53
+ paramsType: [DataType.External],
54
+ },
55
+ }
56
+ }
57
+
58
+ public createRecording(filename: string, watchFor: string[]) {
59
+ return this.bindings.recording_create([
60
+ filename,
61
+ watchFor,
62
+ watchFor.length,
63
+ ])
64
+ }
65
+
66
+ public stopRecording(recording: RecordingHandle) {
67
+ this.bindings.recording_stop([recording])
68
+ }
69
+
70
+ public deleteRecording(recording: RecordingHandle) {
71
+ this.bindings.recording_delete([recording])
72
+ }
73
+
74
+ private get ffiRsOpen() {
75
+ return LabrecorderAdapter.ffiRsOpen
76
+ }
77
+
78
+ private get ffiRsDefine() {
79
+ return LabrecorderAdapter.ffiRsDefine
80
+ }
81
+ }
82
+
83
+ export interface Labrecorder {
84
+ createRecording(filename: string, watchFor: string[]): RecordingHandle
85
+ stopRecording(recording: RecordingHandle): void
86
+ deleteRecording(recording: RecordingHandle): void
87
+ }
88
+
89
+ export type LabrecorderConstructor = new (
90
+ labrecorderPath: string
91
+ ) => Labrecorder
92
+
93
+ export interface LabrecorderBindings {
94
+ recording_create(args: [string, string[], number]): RecordingHandle
95
+
96
+ recording_stop(args: [RecordingHandle]): void
97
+ recording_delete(args: [RecordingHandle]): void
98
+ }
99
+
100
+ export type RecordingHandle = DataType.External