You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: make LSL optional via lazy native load + feature detection
node-labstreaminglayer dlopen's liblsl at require time, so a static
import crashed the whole app on startup when liblsl was missing/
incompatible (e.g. Apple Silicon without the Homebrew build) — even for
Muse-only users who never need LSL.
Load the native bindings lazily and fail soft so LSL becomes a true
advanced, opt-in feature:
- src/main/lsl/native.ts: guarded require() in try/catch (memoized),
exposing loadLSL() and isLSLAvailable()
- outlets.ts/inlets.ts: type-only imports + loadLSL() at call time;
all ops no-op gracefully when liblsl is unavailable
- lsl:isAvailable IPC + preload bridge for renderer feature detection
- ConnectModal hides 'External LSL stream' when unavailable
- lslBridge no-ops sendEpoch/sendMarker when unavailable (no IPC spam)
Result: Muse/Neurosity work with zero LSL/LabRecorder dependency; LSL
features appear only where liblsl loads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .llms/learnings.md
+10Lines changed: 10 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -89,6 +89,16 @@ Any hook function (e.g. `initResponseHandlers` in `src/renderer/utils/labjs/func
89
89
90
90
Alternatives evaluated and rejected: `@neurodevs/node-lsl` and `@neurodevs/ndx-native` both require the same Homebrew install (they hard-code `/opt/homebrew/Cellar/lsl/...` paths) and have a much different async/worker-thread API that would force a substantial rewrite.
91
91
92
+
## LSL is optional — load the native module lazily, never statically
93
+
94
+
`node-labstreaminglayer``dlopen`s liblsl via koffi **at require time**. A static `import … from 'node-labstreaminglayer'` in the main process therefore runs that dlopen during module evaluation at startup — so a missing/incompatible liblsl (e.g. Apple Silicon without the Homebrew build) crashes the *entire app* on launch, even for Muse-only users who don't need LSL at all.
95
+
96
+
LSL is an advanced, opt-in capability: Muse and Neurosity connect via Web Bluetooth and record to CSV without it. So the native module must load lazily and fail soft:
97
+
-`src/main/lsl/native.ts` does a guarded `require('node-labstreaminglayer')` in try/catch (memoized), exposing `loadLSL()` (module | null) and `isLSLAvailable()`. `outlets.ts`/`inlets.ts` use **type-only** imports + `loadLSL()` at call time, no-opping when null.
98
+
- Feature-detect in the renderer via the `lsl:isAvailable` IPC: `ConnectModal` hides the "External LSL stream" option and `lslBridge` no-ops `sendEpoch`/`sendMarker` when unavailable (avoids IPC spam from first-party devices).
99
+
100
+
Build note: with `module: ESNext` source but CommonJS main output, a guarded `require(...)` of an externalized dep type-checks (global `require` from `@types/node`) and stays a `require` in `out/main/index.js` (electron-vite externalizes it) — confirmed lazy, not bundled. Do **not** revert to a static import.
101
+
92
102
## Pre-existing TypeScript errors (do not treat as regressions)
93
103
94
104
-`src/renderer/epics/experimentEpics.ts` (lines 170, 205) — RxJS operator type mismatch
0 commit comments