@neurodevs/node-lsl 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 (60) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc.js +3 -0
  3. package/.nvmrc +1 -0
  4. package/.vscode/launch.json +58 -0
  5. package/.vscode/settings.json +70 -0
  6. package/.vscode/tasks.json +112 -0
  7. package/LICENSE +21 -0
  8. package/README.md +2 -0
  9. package/build/.spruce/errors/errors.types.d.ts +21 -0
  10. package/build/.spruce/errors/errors.types.js +5 -0
  11. package/build/.spruce/errors/errors.types.js.map +1 -0
  12. package/build/.spruce/errors/nodeLsl/failedToLoadLiblsl.schema.d.ts +3 -0
  13. package/build/.spruce/errors/nodeLsl/failedToLoadLiblsl.schema.js +19 -0
  14. package/build/.spruce/errors/nodeLsl/failedToLoadLiblsl.schema.js.map +1 -0
  15. package/build/.spruce/errors/options.types.d.ts +7 -0
  16. package/build/.spruce/errors/options.types.js +3 -0
  17. package/build/.spruce/errors/options.types.js.map +1 -0
  18. package/build/.spruce/schemas/fields/fieldClassMap.d.ts +2 -0
  19. package/build/.spruce/schemas/fields/fieldClassMap.js +5 -0
  20. package/build/.spruce/schemas/fields/fieldClassMap.js.map +1 -0
  21. package/build/.spruce/schemas/fields/fields.types.d.ts +1 -0
  22. package/build/.spruce/schemas/fields/fields.types.js +3 -0
  23. package/build/.spruce/schemas/fields/fields.types.js.map +1 -0
  24. package/build/.spruce/settings.json +13 -0
  25. package/build/Liblsl.d.ts +47 -0
  26. package/build/Liblsl.js +81 -0
  27. package/build/Liblsl.js.map +1 -0
  28. package/build/LslOutlet.d.ts +30 -0
  29. package/build/LslOutlet.js +122 -0
  30. package/build/LslOutlet.js.map +1 -0
  31. package/build/__tests__/behavioral/LslOutlet.test.d.ts +26 -0
  32. package/build/__tests__/behavioral/LslOutlet.test.js +232 -0
  33. package/build/__tests__/behavioral/LslOutlet.test.js.map +1 -0
  34. package/build/__tests__/implementation/Liblsl.test.d.ts +28 -0
  35. package/build/__tests__/implementation/Liblsl.test.js +236 -0
  36. package/build/__tests__/implementation/Liblsl.test.js.map +1 -0
  37. package/build/errors/SpruceError.d.ts +6 -0
  38. package/build/errors/SpruceError.js +26 -0
  39. package/build/errors/SpruceError.js.map +1 -0
  40. package/build/errors/failedToLoadLiblsl.builder.d.ts +11 -0
  41. package/build/errors/failedToLoadLiblsl.builder.js +14 -0
  42. package/build/errors/failedToLoadLiblsl.builder.js.map +1 -0
  43. package/build/index.d.ts +4 -0
  44. package/build/index.js +27 -0
  45. package/build/index.js.map +1 -0
  46. package/package.json +80 -0
  47. package/src/.spruce/errors/errors.types.ts +40 -0
  48. package/src/.spruce/errors/nodeLsl/failedToLoadLiblsl.schema.ts +22 -0
  49. package/src/.spruce/errors/options.types.ts +10 -0
  50. package/src/.spruce/schemas/fields/fieldClassMap.ts +6 -0
  51. package/src/.spruce/schemas/fields/fields.types.ts +1 -0
  52. package/src/.spruce/settings.json +13 -0
  53. package/src/Liblsl.ts +151 -0
  54. package/src/LslOutlet.ts +143 -0
  55. package/src/__tests__/behavioral/LslOutlet.test.ts +217 -0
  56. package/src/__tests__/implementation/Liblsl.test.ts +231 -0
  57. package/src/errors/SpruceError.ts +23 -0
  58. package/src/errors/failedToLoadLiblsl.builder.ts +12 -0
  59. package/src/index.ts +5 -0
  60. package/tsconfig.json +40 -0
@@ -0,0 +1,217 @@
1
+ import { randomInt } from 'crypto'
2
+ import AbstractSpruceTest, {
3
+ test,
4
+ assert,
5
+ errorAssert,
6
+ generateId,
7
+ } from '@sprucelabs/test-utils'
8
+ import LiblslImpl, {
9
+ CreateOutletOptions,
10
+ CreateStreamInfoOptions,
11
+ Liblsl,
12
+ LslBindingsOutlet,
13
+ LslBindingsStreamInfo,
14
+ LslSample,
15
+ } from '../../Liblsl'
16
+ import LslOutlet, { ChannelFormat, LslOutletOptions } from '../../LslOutlet'
17
+
18
+ export const CHANNEL_FORMATS = [
19
+ 'undefined',
20
+ 'float32',
21
+ 'double64',
22
+ 'string',
23
+ 'int32',
24
+ 'int16',
25
+ 'int8',
26
+ 'int64',
27
+ ] as const
28
+
29
+ export default class LslOutletTest extends AbstractSpruceTest {
30
+ private static spyLsl: SpyLiblsl
31
+ private static randomOutletOptions: LslOutletOptions
32
+ private static channelIdx: number
33
+
34
+ protected static async beforeEach() {
35
+ await super.beforeEach()
36
+ this.channelIdx = randomInt(7)
37
+ this.randomOutletOptions = {
38
+ name: generateId(),
39
+ type: generateId(),
40
+ channelCount: randomInt(1, 10),
41
+ sampleRate: Math.random() * 10,
42
+ channelFormat: CHANNEL_FORMATS[this.channelIdx],
43
+ sourceId: generateId(),
44
+ manufacturer: generateId(),
45
+ unit: generateId(),
46
+ chunkSize: randomInt(0, 10),
47
+ maxBuffered: randomInt(0, 10),
48
+ }
49
+
50
+ this.spyLsl = new SpyLiblsl()
51
+ LiblslImpl.setInstance(this.spyLsl)
52
+ }
53
+
54
+ @test()
55
+ protected static async throwsWhenMissingRequiredParameters() {
56
+ //@ts-ignore
57
+ const err = assert.doesThrow(() => new LslOutlet())
58
+ errorAssert.assertError(err, 'MISSING_PARAMETERS', {
59
+ parameters: [
60
+ 'name',
61
+ 'type',
62
+ 'channelCount',
63
+ 'sampleRate',
64
+ 'channelFormat',
65
+ 'sourceId',
66
+ 'manufacturer',
67
+ 'unit',
68
+ 'chunkSize',
69
+ 'maxBuffered',
70
+ ],
71
+ })
72
+ }
73
+
74
+ @test()
75
+ protected static async canCreateWithRequiredParams() {
76
+ this.Outlet()
77
+ }
78
+
79
+ @test()
80
+ protected static async throwsWithInvalidChannelCount() {
81
+ this.assertThrowsInvalidChannelCount(0)
82
+ this.assertThrowsInvalidChannelCount(-1)
83
+ this.assertThrowsInvalidChannelCount(1.5)
84
+ }
85
+
86
+ @test()
87
+ protected static async throwsWithInvalidSampleRate() {
88
+ this.assertThrowsInvalidSampleRate(0)
89
+ this.assertThrowsInvalidSampleRate(-1)
90
+ this.assertThrowsInvalidSampleRate(-1.5)
91
+ }
92
+
93
+ @test()
94
+ protected static async throwsWithInvalidChannelFormat() {
95
+ //@ts-ignore
96
+ this.assertDoesThrowInvalidParameters({ channelFormat: generateId() }, [
97
+ 'channelFormat',
98
+ ])
99
+ }
100
+
101
+ @test()
102
+ protected static async supportsAllKnownChannelFormats() {
103
+ const valid = CHANNEL_FORMATS
104
+ for (const format of valid) {
105
+ this.Outlet({ channelFormat: format as ChannelFormat })
106
+ }
107
+ }
108
+
109
+ @test()
110
+ protected static async throwWithInvalidChunkSize() {
111
+ this.Outlet({ chunkSize: 0 })
112
+ this.assertThrowsInvalidChunkSize(-1)
113
+ this.assertThrowsInvalidChunkSize(-1.5)
114
+ this.assertThrowsInvalidChunkSize(1.5)
115
+ }
116
+
117
+ @test()
118
+ protected static async throwsWithInvalidMaxBuffered() {
119
+ this.Outlet({ maxBuffered: 0 })
120
+ this.assertThrowsInvalidMaxBuffered(-1)
121
+ this.assertThrowsInvalidMaxBuffered(-1.5)
122
+ this.assertThrowsInvalidMaxBuffered(1.5)
123
+ }
124
+
125
+ @test('pushing sample sends to lsl 1', [1])
126
+ @test('pushing sample sends to lsl 2', [1, 2])
127
+ protected static async pushingSampleSendsToLsl(sample: LslSample) {
128
+ this.pushSample(sample)
129
+ assert.isEqual(this.spyLsl.lastPushedOutlet, this.spyLsl.outlet)
130
+ assert.isEqualDeep(this.spyLsl.lastPushedSample, sample)
131
+ assert.isEqualDeep(this.spyLsl.lastOutletOptions, {
132
+ info: this.spyLsl.streamInfo,
133
+ chunkSize: this.randomOutletOptions.chunkSize,
134
+ maxBuffered: this.randomOutletOptions.maxBuffered,
135
+ })
136
+ }
137
+
138
+ @test()
139
+ protected static async pushingSamplePassesCorrectOptionsToCreateStreamInfo() {
140
+ this.pushSample([1])
141
+ const { ...options } = this.randomOutletOptions as any
142
+
143
+ delete options.manufacturer
144
+ delete options.unit
145
+ delete options.chunkSize
146
+ delete options.maxBuffered
147
+
148
+ assert.isEqualDeep(this.spyLsl.lastStreamInfoOptions, {
149
+ ...options,
150
+ channelFormat: this.channelIdx,
151
+ })
152
+ }
153
+
154
+ private static pushSample(sample: LslSample) {
155
+ const outlet = this.Outlet()
156
+ outlet.pushSample(sample)
157
+ }
158
+
159
+ private static assertThrowsInvalidMaxBuffered(maxBuffered: number) {
160
+ this.assertDoesThrowInvalidParameters({ maxBuffered }, ['maxBuffered'])
161
+ }
162
+
163
+ private static assertThrowsInvalidChunkSize(chunkSize: number) {
164
+ this.assertDoesThrowInvalidParameters({ chunkSize }, ['chunkSize'])
165
+ }
166
+
167
+ private static assertThrowsInvalidSampleRate(sampleRate: number) {
168
+ this.assertDoesThrowInvalidParameters({ sampleRate }, ['sampleRate'])
169
+ }
170
+
171
+ private static assertThrowsInvalidChannelCount(count: number) {
172
+ this.assertDoesThrowInvalidParameters({ channelCount: count }, [
173
+ 'channelCount',
174
+ ])
175
+ }
176
+
177
+ private static assertDoesThrowInvalidParameters(
178
+ options: Partial<LslOutletOptions>,
179
+ parameters: string[]
180
+ ) {
181
+ const err = assert.doesThrow(() => this.Outlet(options))
182
+ errorAssert.assertError(err, 'INVALID_PARAMETERS', {
183
+ parameters,
184
+ })
185
+ }
186
+
187
+ private static Outlet(options?: Partial<LslOutletOptions>) {
188
+ return new LslOutlet({
189
+ ...this.randomOutletOptions,
190
+ ...options,
191
+ })
192
+ }
193
+ }
194
+
195
+ class SpyLiblsl implements Liblsl {
196
+ public lastPushedOutlet?: LslBindingsOutlet
197
+ public lastPushedSample?: LslSample
198
+ public lastOutletOptions?: CreateOutletOptions
199
+ public lastStreamInfoOptions?: CreateStreamInfoOptions
200
+ public outlet: LslBindingsOutlet = {} as LslBindingsOutlet
201
+ public streamInfo: LslBindingsStreamInfo = {} as LslBindingsStreamInfo
202
+
203
+ public createStreamInfo(
204
+ options: CreateStreamInfoOptions
205
+ ): LslBindingsStreamInfo {
206
+ this.lastStreamInfoOptions = options
207
+ return this.streamInfo
208
+ }
209
+ public createOutlet(options: CreateOutletOptions): LslBindingsOutlet {
210
+ this.lastOutletOptions = options
211
+ return this.outlet
212
+ }
213
+ public pushSample(outlet: LslBindingsOutlet, sample: LslSample): void {
214
+ this.lastPushedOutlet = outlet
215
+ this.lastPushedSample = sample
216
+ }
217
+ }
@@ -0,0 +1,231 @@
1
+ import { randomInt } from 'crypto'
2
+ import AbstractSpruceTest, {
3
+ test,
4
+ assert,
5
+ errorAssert,
6
+ generateId,
7
+ } from '@sprucelabs/test-utils'
8
+ import ArrayType from 'ref-array-napi'
9
+ import ref from 'ref-napi'
10
+ import LiblslImpl, {
11
+ CreateStreamInfoOptions,
12
+ LiblslBindings,
13
+ Liblsl,
14
+ LslSample,
15
+ StreamInfo,
16
+ CreateOutletOptions,
17
+ LslBindingsOutlet,
18
+ LslBindingsStreamInfo,
19
+ } from '../../Liblsl'
20
+
21
+ export default class LiblslTest extends AbstractSpruceTest {
22
+ private static lsl: Liblsl
23
+ private static libraryPath?: string
24
+ private static libraryOptions?: Record<string, any>
25
+ private static fakeBindings: LiblslBindings
26
+ private static fakeStreamInfo: StreamInfo
27
+ private static fakeOutlet: LslBindingsOutlet
28
+ private static createStreamInfoParams?: any[]
29
+ private static createOutletParams?: any[]
30
+ private static pushSampleParams?: any[]
31
+ private static localClock: number
32
+ private static shouldThrowWhenCreatingBindings: boolean
33
+
34
+ protected static async beforeEach(): Promise<void> {
35
+ await super.beforeEach()
36
+
37
+ delete this.libraryPath
38
+ delete this.libraryOptions
39
+ delete this.createStreamInfoParams
40
+ delete this.createOutletParams
41
+ delete this.pushSampleParams
42
+
43
+ process.env.LIBLSL_PATH = generateId()
44
+
45
+ this.fakeStreamInfo = {}
46
+ this.fakeOutlet = {}
47
+ this.localClock = new Date().getTime()
48
+ this.shouldThrowWhenCreatingBindings = false
49
+
50
+ this.fakeBindings = {
51
+ lsl_create_streaminfo: (...params: any[]) => {
52
+ this.createStreamInfoParams = params
53
+ return this.fakeStreamInfo
54
+ },
55
+ lsl_create_outlet: (...params: any[]) => {
56
+ this.createOutletParams = params
57
+ return this.fakeOutlet
58
+ },
59
+ lsl_push_sample_ft: (...params: any[]) => {
60
+ this.pushSampleParams = params
61
+ return 0
62
+ },
63
+ lsl_local_clock: () => this.localClock,
64
+ }
65
+
66
+ LiblslImpl.ffi = {
67
+ //@ts-ignore
68
+ Library: (path: string, options: Record<string, any>) => {
69
+ this.libraryPath = path
70
+ this.libraryOptions = options
71
+ if (this.shouldThrowWhenCreatingBindings) {
72
+ throw new Error('Failed to create bindings')
73
+ }
74
+ return this.fakeBindings
75
+ },
76
+ }
77
+
78
+ LiblslImpl.reset()
79
+ this.lsl = LiblslImpl.getInstance()
80
+ }
81
+
82
+ @test()
83
+ protected static async throwsIfNoEnvSet() {
84
+ delete process.env.LIBLSL_PATH
85
+ const err = assert.doesThrow(() => new LiblslImpl())
86
+ errorAssert.assertError(err, 'MISSING_PARAMETERS', {
87
+ parameters: ['env.LIBLSL_PATH'],
88
+ })
89
+ }
90
+
91
+ @test()
92
+ protected static async throwsHelpfulErrorWhenBindingsFailToLoad() {
93
+ this.shouldThrowWhenCreatingBindings = true
94
+ const err = assert.doesThrow(() => new LiblslImpl())
95
+ errorAssert.assertError(err, 'FAILED_TO_LOAD_LIBLSL', {
96
+ liblslPath: process.env.LIBLSL_PATH,
97
+ })
98
+ }
99
+
100
+ @test()
101
+ protected static async worksAsASingleton() {
102
+ const liblsl = LiblslImpl.getInstance()
103
+ assert.isInstanceOf(liblsl, LiblslImpl)
104
+ }
105
+
106
+ @test()
107
+ protected static async singletonIsTheSame() {
108
+ assert.isEqual(LiblslImpl.getInstance(), LiblslImpl.getInstance())
109
+ }
110
+
111
+ @test()
112
+ protected static canSetInstance() {
113
+ const fake = new FakeLiblsl()
114
+ LiblslImpl.setInstance(fake)
115
+ assert.isEqual(LiblslImpl.getInstance(), fake)
116
+ }
117
+
118
+ @test()
119
+ protected static async createsExpectedBindingsToLiblsl() {
120
+ process.env.LIBLSL_PATH = generateId()
121
+ new LiblslImpl()
122
+ assert.isEqual(this.libraryPath, process.env.LIBLSL_PATH)
123
+ const expected = {
124
+ lsl_create_streaminfo: [
125
+ streamInfo,
126
+ ['string', 'string', 'int', 'double', 'int', 'string'],
127
+ ],
128
+ lsl_create_outlet: [outletType, [streamInfo, 'int', 'int']],
129
+ lsl_local_clock: ['double', []],
130
+ lsl_push_sample_ft: ['void', [outletType, FloatArray, 'double']],
131
+ }
132
+ assert.isEqual(
133
+ JSON.stringify(this.libraryOptions),
134
+ JSON.stringify(expected)
135
+ )
136
+ }
137
+
138
+ @test()
139
+ protected static createStreamInfoThrowsWithMissingRequiredParams() {
140
+ //@ts-ignore
141
+ const err = assert.doesThrow(() => this.lsl.createStreamInfo())
142
+ errorAssert.assertError(err, 'MISSING_PARAMETERS', {
143
+ parameters: [
144
+ 'name',
145
+ 'type',
146
+ 'channelCount',
147
+ 'sampleRate',
148
+ 'channelFormat',
149
+ 'sourceId',
150
+ ],
151
+ })
152
+ }
153
+
154
+ @test()
155
+ protected static async canGetStreamInfoWithRequiredParams() {
156
+ const options = this.generateRandomCreateStreamInfoOptions()
157
+ const actual = this.lsl.createStreamInfo(options)
158
+
159
+ assert.isEqual(actual, this.fakeStreamInfo)
160
+ assert.isEqualDeep(this.createStreamInfoParams, Object.values(options))
161
+ }
162
+
163
+ @test()
164
+ protected static async createOutletThrowsWhenMissingRequiredParams() {
165
+ //@ts-ignore
166
+ const err = assert.doesThrow(() => this.lsl.createOutlet({}))
167
+ errorAssert.assertError(err, 'MISSING_PARAMETERS', {
168
+ parameters: ['info', 'chunkSize', 'maxBuffered'],
169
+ })
170
+ }
171
+
172
+ @test()
173
+ protected static async canCreateOutletWithRequiredParams() {
174
+ const info = this.lsl.createStreamInfo(
175
+ this.generateRandomCreateStreamInfoOptions()
176
+ )
177
+ const options = {
178
+ info,
179
+ chunkSize: randomInt(10),
180
+ maxBuffered: randomInt(10),
181
+ }
182
+ const actual = this.lsl.createOutlet(options)
183
+ assert.isEqualDeep(this.createOutletParams, Object.values(options))
184
+ assert.isEqual(actual, this.fakeOutlet)
185
+ }
186
+
187
+ @test()
188
+ protected static async pushSampleThrowsWhenMissingRequiredParams() {
189
+ //@ts-ignore
190
+ const err = assert.doesThrow(() => this.lsl.pushSample())
191
+ errorAssert.assertError(err, 'MISSING_PARAMETERS', {
192
+ parameters: ['outlet', 'sample'],
193
+ })
194
+ }
195
+
196
+ @test()
197
+ protected static async pushSampleCallsBindings() {
198
+ const expected = [1, 2, 3]
199
+ this.lsl.pushSample(this.fakeOutlet, expected)
200
+ assert.isEqual(this.pushSampleParams?.[0], this.fakeOutlet)
201
+ assert.isEqual(this.pushSampleParams?.[1], expected)
202
+ assert.isEqual(this.pushSampleParams?.[2], this.localClock)
203
+ }
204
+
205
+ private static generateRandomCreateStreamInfoOptions() {
206
+ return {
207
+ name: generateId(),
208
+ type: generateId(),
209
+ channelCount: randomInt(10),
210
+ sampleRate: randomInt(10),
211
+ channelFormat: randomInt(10),
212
+ sourceId: generateId(),
213
+ }
214
+ }
215
+ }
216
+
217
+ class FakeLiblsl implements Liblsl {
218
+ public createOutlet(_options: CreateOutletOptions): LslBindingsOutlet {
219
+ return {} as LslBindingsOutlet
220
+ }
221
+ public createStreamInfo(
222
+ _options: CreateStreamInfoOptions
223
+ ): LslBindingsStreamInfo {
224
+ return {} as StreamInfo
225
+ }
226
+ public pushSample(_sample: LslSample): void {}
227
+ }
228
+
229
+ const streamInfo = ref.refType(ref.types.void)
230
+ const outletType = ref.refType(ref.types.void)
231
+ const FloatArray = ArrayType(ref.types.float)
@@ -0,0 +1,23 @@
1
+ import BaseSpruceError from '@sprucelabs/error'
2
+ import ErrorOptions from '#spruce/errors/options.types'
3
+
4
+ export default class SpruceError extends BaseSpruceError<ErrorOptions> {
5
+ /** an easy to understand version of the errors */
6
+ public friendlyMessage(): string {
7
+ const { options } = this
8
+ let message
9
+ switch (options?.code) {
10
+ case 'FAILED_TO_LOAD_LIBLSL':
11
+ message = `Well shoot! Loading the liblsl dylib failed. I tried to load it from ${options.liblslPath}.`
12
+ break
13
+ default:
14
+ message = super.friendlyMessage()
15
+ }
16
+
17
+ const fullMessage = options.friendlyMessage
18
+ ? options.friendlyMessage
19
+ : message
20
+
21
+ return fullMessage
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ import { buildErrorSchema } from '@sprucelabs/schema'
2
+
3
+ export default buildErrorSchema({
4
+ id: 'failedToLoadLiblsl',
5
+ name: 'FAILED_TO_LOAD_LIBLSL',
6
+ fields: {
7
+ liblslPath: {
8
+ type: 'text',
9
+ isRequired: true,
10
+ },
11
+ },
12
+ })
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { default as LiblslImpl } from './Liblsl'
2
+ export * from './Liblsl'
3
+
4
+ export { default as LiblslOutlet } from './LslOutlet'
5
+ export * from './LslOutlet'
package/tsconfig.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "compilerOptions": {
3
+ "skipLibCheck": true,
4
+ "module": "commonjs",
5
+ "esModuleInterop": true,
6
+ "target": "ES2017",
7
+ "lib": [
8
+ "DOM",
9
+ "es2017",
10
+ "ES2020"
11
+ ],
12
+ "declaration": true,
13
+ "noImplicitAny": true,
14
+ "allowJs": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "noImplicitReturns": true,
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "resolveJsonModule": true,
20
+ "moduleResolution": "node",
21
+ "sourceMap": false,
22
+ "outDir": "build",
23
+ "baseUrl": "src",
24
+ "experimentalDecorators": true,
25
+ "paths": {
26
+ "#spruce/*": [
27
+ ".spruce/*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "./src/*.ts",
33
+ "./src/**/*.ts",
34
+ "./src/.spruce/**/*"
35
+ ],
36
+ "exclude": [
37
+ "build",
38
+ "esm"
39
+ ]
40
+ }