Summary
Make blim.subscribe non-blocking: register the callback and return the cancel handle immediately, perform the CCCD (Client Characteristic Configuration Descriptor) write in the background, and report write failures asynchronously. This removes the synchronous BLE round-trip stall and makes blim.subscribe safe and non-blocking from any context, including from within a subscription/PTY callback.
Background
To receive notifications, the central must write the characteristic's CCCD (0x2902, 2 bytes 0x0001 for notify) — a round-trip to the device. Today blim.subscribe -> executeSubscription -> connection.Subscribe -> client.Subscribe performs this write synchronously while holding stateMutex.
Consequences:
- From the main loop: the whole Lua world is frozen for the round-trip duration (usually ms, but seconds on a bad link).
- From a callback: the notification fan-out is stalled for the round-trip (the callback holds
stateMutex during the write).
This is a stall, not corruption — the corruption on the callback path was fixed separately in #3 (subscribe now returns (nil, err) instead of RaiseError). But the stall remains, and it is why a blim.subscribe from a callback is still undesirable.
Proposed behavior
blim.subscribe{...}:
- Validate config synchronously (no services / no callback / bad table) -> still return
(nil, err) synchronously, as today.
- Register the callback locally (in-memory fan-out routing) immediately.
- Return the cancel handle immediately (synchronously).
- Fire the CCCD write on a background goroutine.
- Report a CCCD-write failure asynchronously (device rejected, disconnect) via an error/log callback — since we returned before the write completed.
Result: the caller never waits for the BLE round-trip -> no stall. Notifications begin flowing once the async write completes. blim.subscribe needs no in-callback guard at all — legal and non-blocking from any context.
Trade-offs / design notes
- Async error reporting: config/validation errors stay synchronous, but the CCCD-write runtime failure becomes asynchronous. Contract change:
local cancel, err = blim.subscribe{...} no longer surfaces write failures synchronously. Decide the async error channel (log, an on_error field, or a status callback).
- Cancel-before-write: cancelling before the background write completes must cancel the pending write; more concurrency in the subscribe path.
- First-notification latency: small delay before the first notification (until the async write lands).
- Ordering: two rapid subscribes should keep deterministic CCCD-write ordering per connection.
Relationship to other work
Acceptance
blim.subscribe returns without waiting for the CCCD round-trip.
- Config/validation errors still returned synchronously as
(nil, err).
- CCCD-write failures surfaced asynchronously (documented channel).
blim.subscribe from within a callback neither stalls the fan-out nor corrupts state.
- Cancelling a just-created subscription cancels a still-pending CCCD write.
Summary
Make
blim.subscribenon-blocking: register the callback and return the cancel handle immediately, perform the CCCD (Client Characteristic Configuration Descriptor) write in the background, and report write failures asynchronously. This removes the synchronous BLE round-trip stall and makesblim.subscribesafe and non-blocking from any context, including from within a subscription/PTY callback.Background
To receive notifications, the central must write the characteristic's CCCD (
0x2902, 2 bytes0x0001for notify) — a round-trip to the device. Todayblim.subscribe->executeSubscription->connection.Subscribe->client.Subscribeperforms this write synchronously while holdingstateMutex.Consequences:
stateMutexduring the write).This is a stall, not corruption — the corruption on the callback path was fixed separately in #3 (subscribe now returns
(nil, err)instead ofRaiseError). But the stall remains, and it is why ablim.subscribefrom a callback is still undesirable.Proposed behavior
blim.subscribe{...}:(nil, err)synchronously, as today.Result: the caller never waits for the BLE round-trip -> no stall. Notifications begin flowing once the async write completes.
blim.subscribeneeds no in-callback guard at all — legal and non-blocking from any context.Trade-offs / design notes
local cancel, err = blim.subscribe{...}no longer surfaces write failures synchronously. Decide the async error channel (log, anon_errorfield, or a status callback).Relationship to other work
(nil, err)).blim.subscribefrom a callback is fully first-class (no stall, no guard) — an alternative/complement to ablim.defer(fn)continuation mechanism.Acceptance
blim.subscribereturns without waiting for the CCCD round-trip.(nil, err).blim.subscribefrom within a callback neither stalls the fan-out nor corrupts state.