// src/node-host-manager.ts import { contractError } from './backend-contract/errors' import type { AdapterDescriptor, BackendProvider, HostNeutralBackendIdentity } from './backend-contract/identity' import { byteLimit, opaqueId, type BackendCompatibilityOffer } from './backend-contract/primitives' import { createEphemeralHostIdentity, normalizeBleManagerCreateOptions } from './public/host-identity' import type { BleManagerCreateOptions } from './public/host-identity' import { rehydratePublicPromise } from './public/error-bridge' import { createBleManagerFromProvider, DEFAULT_BLE_MANAGER_OPTIONS, type BleManager } from './manager/ble-manager' export interface NodeBleManagerAppOptions extends BleManagerCreateOptions { readonly now?: () => number } export async function createNodeBleManagerFromProvider( provider: BackendProvider>, compatibility: BackendCompatibilityOffer, options: NodeBleManagerAppOptions ): Promise>> { return rehydratePublicPromise(createNodeBleManagerFromProviderInternal(provider, compatibility, options)) } async function createNodeBleManagerFromProviderInternal( provider: BackendProvider>, compatibility: BackendCompatibilityOffer, options: NodeBleManagerAppOptions ): Promise>> { const { now = () => performance.now(), ...createOptions } = options normalizeBleManagerCreateOptions(createOptions) if (createOptions.restoration !== undefined) { throw contractError('capability.unsupported', 'restoration', 'node-manager.restoration') } const adapters = await provider.listAdapters() const selected = selectNodeAdapter(adapters, options.adapterId) const ephemeral = createEphemeralHostIdentity({ randomBytes: createOptions.randomBytes }) const instanceSuffix = options.instanceId === undefined ? '' : `-${options.instanceId}` return createBleManagerFromProvider( { provider, selection: { selectedAdapterId: selected.adapterId }, coreCompatibility: compatibility, manager: { clientId: opaqueId(`node-${ephemeral.managerNonce}${instanceSuffix}`, 'client', 'node:host'), managerId: opaqueId(`node-${ephemeral.attachmentNonce}${instanceSuffix}`, 'manager', 'node:host'), ownerMode: 'owning' } }, { ...DEFAULT_BLE_MANAGER_OPTIONS, now, maximumValueBytes: createOptions.diagnostics?.maximumValueBytes === undefined ? DEFAULT_BLE_MANAGER_OPTIONS.maximumValueBytes : byteLimit(createOptions.diagnostics.maximumValueBytes), traceMaximumRecords: createOptions.diagnostics?.traceMaximumRecords ?? DEFAULT_BLE_MANAGER_OPTIONS.traceMaximumRecords, traceMaximumBytes: createOptions.diagnostics?.traceMaximumBytes ?? DEFAULT_BLE_MANAGER_OPTIONS.traceMaximumBytes } ) } function selectNodeAdapter( adapters: readonly AdapterDescriptor[], selectedAdapterId: string | undefined ): AdapterDescriptor { if (selectedAdapterId !== undefined) { const match = adapters.find(adapter => String(adapter.adapterId) === selectedAdapterId) if (match === undefined) { throw contractError('adapter.unavailable', 'adapter', 'node-host-manager.selected-adapter') } return match } if (adapters.length === 0) { throw contractError('adapter.unavailable', 'adapter', 'node-host-manager.adapter') } // Default to the first adapter, ordered deterministically by id, so a host // with more than one controller "just works" and picks the same one every // run instead of forcing every caller to name an adapter. Choosing a specific // controller (for example a second USB dongle used only for debugging) is the // unusual case: pass `adapterId` for it. Order by code unit, not // `localeCompare`, so the choice cannot shift with the host's locale or ICU // version. const [first] = [...adapters].sort((left, right) => { const leftId = String(left.adapterId) const rightId = String(right.adapterId) return leftId < rightId ? -1 : leftId > rightId ? 1 : 0 }) if (first === undefined) { throw contractError('adapter.unavailable', 'adapter', 'node-host-manager.adapter') } return first }