// src/backends/winrt/winrt-handles.ts import type { AdvertisementObservation, OwnerScanOptions } from '../../backend-contract/advertisement' import type { BackendConnection, BackendSubscription, ConnectionLease, ScanLease } from '../../backend-contract/backend' import { BackendContractError, contractError, type CleanupFailure, type CleanupRecord } from '../../backend-contract/errors' import { createGattCharacteristicProperties, createGattDescriptorProperties, type Characteristic, type CharacteristicPath, type DatabasePath, type Descriptor, type DescriptorPath, type GattDatabase, type GattDatabaseSnapshot, type NotificationValue, type Service } from '../../backend-contract/gatt' import { attachmentRecordsEqual, type AttachmentRecord } from '../../backend-contract/identity' import type { OperationOptions, OperationTerminalRecord, PublicOperationOptions, SubscriptionOptions, WritePolicy, WriteReceipt } from '../../backend-contract/operations' import { canonicalUuid, opaqueId, type AttachmentId, type ConnectionId, type GenerationId, type LeaseId, type OwnedBytes, type PeerId, type ScanSessionId, type ScanShareToken, type SubscriptionId } from '../../backend-contract/primitives' import type { BoundedAsyncStream } from '../../backend-contract/streams' import { CoreBoundedStream } from '../../core/bounded-stream' import type { WinRtBackend, WinRtConnectionRecord, WinRtPhysicalSubscription, WinRtScanConsumer } from './winrt-backend' import type { WinRtCharacteristicAddress, WinRtDescriptorAddress, WinRtGattSnapshot } from './winrt-boundary' export const releasedCleanup: CleanupRecord = Object.freeze({ state: 'released', failures: Object.freeze([]) }) /** Couples a public notification stream close to its owning retryable CCCD cleanup. */ export class WinRtSubscriptionStream extends CoreBoundedStream { private ownerRemoval: (() => Promise) | null = null private closeCleanup: Promise | null = null bindOwnerRemoval(ownerRemoval: () => Promise): void { if (this.ownerRemoval !== null) { throw contractError('lifecycle.invariant-violation', 'stream', 'winrt.subscription-stream.owner-removal') } this.ownerRemoval = ownerRemoval } override close(): Promise { super.close() if (this.closeCleanup !== null) { return this.closeCleanup } const ownerRemoval = this.ownerRemoval if (ownerRemoval === null) { return Promise.resolve(releasedCleanup) } let cleanup: Promise try { cleanup = ownerRemoval() } catch (error) { return Promise.reject(error) } const trackedCleanup = cleanup.then( result => { if (result.state === 'release-failed' && this.closeCleanup === trackedCleanup) { this.closeCleanup = null } return result }, error => { if (this.closeCleanup === trackedCleanup) { this.closeCleanup = null } throw error } ) this.closeCleanup = trackedCleanup return trackedCleanup } } export class WinRtScanLease implements ScanLease { readonly scanSessionId: ScanSessionId readonly leaseId: LeaseId readonly shareToken: ScanShareToken | null readonly observations: BoundedAsyncStream> constructor( private readonly backend: WinRtBackend, private readonly consumer: WinRtScanConsumer ) { this.scanSessionId = consumer.scanSessionId this.leaseId = consumer.leaseId this.shareToken = consumer.shareToken this.observations = consumer.stream } stop(): Promise { return this.backend.stopScanConsumer(this.consumer) } } export class WinRtConnection implements BackendConnection { constructor( private readonly backend: WinRtBackend, readonly record: WinRtConnectionRecord ) {} get attachment(): AttachmentRecord { return this.record.attachment } get attachmentId(): AttachmentId { return this.attachment.attachmentId } get peerId(): PeerId { return this.record.peerId } get connectionId(): ConnectionId { return this.record.connectionId } get connectionGeneration(): GenerationId<'connection-generation', string> { return this.record.connectionGeneration } get state(): BackendConnection['state'] { return this.record.state } disconnect(): Promise { return this.backend.disconnect(this.record, 'winrt.connection.disconnect') } } export class WinRtConnectionLease implements ConnectionLease { private released = false private releaseResult: Promise | null = null constructor( private readonly backend: WinRtBackend, readonly record: WinRtConnectionRecord, readonly connection: WinRtConnection ) {} get leaseId(): LeaseId { return this.record.ownerLeaseId } release(): Promise { if (this.released) { return Promise.resolve(releasedCleanup) } if (this.releaseResult === null) { this.releaseResult = this.backend.releaseConnectionLease(this).then(result => { if (result.state === 'released') { this.released = true } else { this.releaseResult = null } return result }) } return this.releaseResult } markReleased(): void { this.released = true } } export class WinRtGattDatabase implements GattDatabase { private valid = true constructor( private readonly backend: WinRtBackend, readonly connectionRecord: WinRtConnectionRecord, readonly path: DatabasePath, private readonly snapshotRecord: WinRtGattSnapshot ) {} async snapshot(): Promise> { this.backend.assertGattUsable('winrt.gatt.snapshot') this.assertCurrent('winrt.gatt.snapshot') const services: Service[] = [] const characteristics: Characteristic[] = [] const descriptors: Descriptor[] = [] for (const service of this.snapshotRecord.services) { const servicePath = Object.freeze({ ...this.path, serviceUuid: canonicalUuid(service.uuid), serviceOccurrence: opaqueId(String(service.occurrence), 'service-occurrence', String(this.path.databaseId)) }) services.push(Object.freeze({ path: servicePath, primary: true, includedServices: Object.freeze([]) })) for (const characteristic of service.characteristics) { const characteristicPath: CharacteristicPath = Object.freeze( { ...servicePath, characteristicUuid: canonicalUuid(characteristic.uuid), characteristicOccurrence: opaqueId( String(characteristic.occurrence), 'characteristic-occurrence', String(servicePath.serviceOccurrence) ), validity: 'current' } ) characteristics.push( Object.freeze({ path: characteristicPath, properties: createGattCharacteristicProperties({ read: characteristic.readable, writeWithResponse: characteristic.writableWithResponse, writeWithoutResponse: characteristic.writableWithoutResponse, notify: characteristic.notifiable, indicate: characteristic.indicatable }), access: Object.freeze({ read: 'unknown', write: 'unknown' }) }) ) for (const descriptor of characteristic.descriptors) { const descriptorPath: DescriptorPath = Object.freeze({ ...characteristicPath, descriptorUuid: canonicalUuid(descriptor.uuid), descriptorOccurrence: opaqueId( String(descriptor.occurrence), 'descriptor-occurrence', String(characteristicPath.characteristicOccurrence) ) }) descriptors.push( Object.freeze({ path: descriptorPath, properties: createGattDescriptorProperties( false, false, { read: 'unknown', write: 'unknown' }, { read: 'unknown', write: 'unknown' } ) }) ) } } } return Object.freeze({ path: this.path, services: Object.freeze(services), characteristics: Object.freeze(characteristics), descriptors: Object.freeze(descriptors) }) } async read( path: CharacteristicPath, options: PublicOperationOptions ): Promise { this.backend.assertGattUsable('winrt.gatt.database-read') this.assertCurrent('winrt.gatt.database-read') return this.backend.gattOperations.readFromDatabase( this.connectionRecord, this.addressFor(path, 'winrt.gatt.database-read'), options ) } async write( path: CharacteristicPath, value: Uint8Array, options: WritePolicy ): Promise> { this.backend.assertGattUsable('winrt.gatt.database-write') this.assertCurrent('winrt.gatt.database-write') return this.backend.gattOperations.writeFromDatabase( this.connectionRecord, this.addressFor(path, 'winrt.gatt.database-write'), value, options ) } async readDescriptor< ServiceOccurrence extends string, CharacteristicOccurrence extends string, DescriptorOccurrence extends string >( path: DescriptorPath< string, string, string, ServiceOccurrence, CharacteristicOccurrence, DescriptorOccurrence, 'current' >, options: PublicOperationOptions ): Promise { this.backend.assertGattUsable('winrt.gatt.database-read-descriptor') this.assertCurrent('winrt.gatt.database-read-descriptor') return this.backend.gattOperations.readDescriptorFromDatabase( this.connectionRecord, this.descriptorAddressFor(path, 'winrt.gatt.database-read-descriptor'), options ) } async writeDescriptor< ServiceOccurrence extends string, CharacteristicOccurrence extends string, DescriptorOccurrence extends string >( path: DescriptorPath< string, string, string, ServiceOccurrence, CharacteristicOccurrence, DescriptorOccurrence, 'current' >, value: Uint8Array, options: WritePolicy ): Promise> { this.backend.assertGattUsable('winrt.gatt.database-write-descriptor') this.assertCurrent('winrt.gatt.database-write-descriptor') return this.backend.gattOperations.writeDescriptorFromDatabase( this.connectionRecord, this.descriptorAddressFor(path, 'winrt.gatt.database-write-descriptor'), value, options ) } async subscribe( path: CharacteristicPath, options: SubscriptionOptions ): Promise { this.backend.assertGattUsable('winrt.gatt.database-subscribe') this.assertCurrent('winrt.gatt.database-subscribe') return this.backend.gattOperations.subscribeFromDatabase(path, options) } invalidate(): void { this.valid = false } assertCurrent(operation: string): void { if (!this.valid || this.connectionRecord.database !== this || this.connectionRecord.state !== 'connected') { throw contractError('gatt.stale-handle', 'gatt', operation) } } matchesPath(path: CharacteristicPath): boolean { return ( attachmentRecordsEqual(path.attachment, this.path.attachment) && path.attachmentId === this.path.attachmentId && path.peerId === this.path.peerId && path.connectionId === this.path.connectionId && path.ownerLeaseId === this.path.ownerLeaseId && path.connectionGeneration === this.path.connectionGeneration && path.databaseId === this.path.databaseId && path.databaseGeneration === this.path.databaseGeneration && path.validity === 'current' ) } addressFor( path: CharacteristicPath, operation: string ): WinRtCharacteristicAddress { this.assertCurrent(operation) if (!this.matchesPath(path)) { throw contractError('gatt.stale-handle', 'gatt', operation) } const service = this.snapshotRecord.services.find( candidate => candidate.uuid === path.serviceUuid && candidate.occurrence === Number(path.serviceOccurrence) ) const characteristic = service?.characteristics.find( candidate => candidate.uuid === path.characteristicUuid && candidate.occurrence === Number(path.characteristicOccurrence) ) if (service === undefined || characteristic === undefined) { throw contractError('gatt.not-found', 'gatt', operation) } return Object.freeze({ nativePeerId: this.connectionRecord.nativePeerId, connectionGeneration: String(this.connectionRecord.connectionGeneration), serviceUuid: service.uuid, serviceOccurrence: service.occurrence, characteristicUuid: characteristic.uuid, characteristicOccurrence: characteristic.occurrence }) } descriptorAddressFor( path: DescriptorPath, operation: string ): WinRtDescriptorAddress { const characteristic = this.addressFor(path, operation) const service = this.snapshotRecord.services.find( candidate => candidate.uuid === path.serviceUuid && candidate.occurrence === Number(path.serviceOccurrence) ) const characteristicRecord = service?.characteristics.find( candidate => candidate.uuid === path.characteristicUuid && candidate.occurrence === Number(path.characteristicOccurrence) ) const descriptor = characteristicRecord?.descriptors.find( candidate => candidate.uuid === path.descriptorUuid && candidate.occurrence === Number(path.descriptorOccurrence) ) if (descriptor === undefined) { throw contractError('gatt.not-found', 'gatt', operation) } return Object.freeze({ ...characteristic, descriptorUuid: descriptor.uuid, descriptorOccurrence: descriptor.occurrence }) } notificationModeForPath( path: CharacteristicPath ): 'notify' | 'indicate' { const address = this.addressFor(path, 'winrt.gatt.subscribe.mode') const service = this.snapshotRecord.services.find( candidate => candidate.uuid === address.serviceUuid && candidate.occurrence === address.serviceOccurrence ) const characteristic = service?.characteristics.find( candidate => candidate.uuid === address.characteristicUuid && candidate.occurrence === address.characteristicOccurrence ) if (characteristic === undefined) { throw contractError('gatt.not-found', 'gatt', 'winrt.gatt.subscribe.mode') } if (characteristic.notifiable) { return 'notify' } if (characteristic.indicatable) { return 'indicate' } throw contractError('gatt.property-not-supported', 'gatt', 'winrt.gatt.subscribe.mode') } } export class WinRtBackendSubscription implements BackendSubscription { removed = false constructor( private readonly backend: WinRtBackend, readonly connectionRecord: WinRtConnectionRecord, readonly physical: WinRtPhysicalSubscription, readonly path: CharacteristicPath, readonly subscriptionId: SubscriptionId, readonly terminal: OperationTerminalRecord, readonly stream: WinRtSubscriptionStream ) {} get notifications(): BoundedAsyncStream { return this.stream } get values(): BoundedAsyncStream { return this.stream } remove(): Promise { return this.backend.gattOperations.removeSubscription(this) } isOwnedBy(backend: WinRtBackend): boolean { const attachment = backend.attachment() return ( this.backend === backend && attachmentRecordsEqual(this.path.attachment, attachment) && this.path.attachmentId === attachment.attachmentId ) } } export function successfulTerminal( operation: OperationOptions ): OperationTerminalRecord { return Object.freeze({ correlation: operation.correlation, outcome: 'succeeded', cause: null }) } export function characteristicAddressKey(address: WinRtCharacteristicAddress): string { return [ address.nativePeerId, address.connectionGeneration, address.serviceUuid, String(address.serviceOccurrence), address.characteristicUuid, String(address.characteristicOccurrence) ].join('\u0000') } export function matchesScan( options: OwnerScanOptions, observation: AdvertisementObservation ): boolean { if ( options.filter.localNamePrefix !== null && (observation.localName.state !== 'present' || !observation.localName.value.startsWith(options.filter.localNamePrefix)) ) { return false } if (options.filter.serviceUuids.length === 0) { return true } if (observation.serviceUuids.state !== 'present') { return false } const observedServiceUuids = observation.serviceUuids.value return options.filter.serviceUuids.every(uuid => observedServiceUuids.includes(uuid)) } export function advertisementByteLength(observation: AdvertisementObservation): number { let size = 64 if (observation.localName.state === 'present') { size += observation.localName.value.length } if (observation.serviceUuids.state === 'present') { size += observation.serviceUuids.value.length * 36 } return size } export function cleanupFailure(resourceKind: string, operation: string, error: unknown): CleanupRecord { const safeMessage = error instanceof Error ? error.message : 'WinRT cleanup rejected with a non-Error value' const platform = error instanceof BackendContractError ? error.normalized.platform : { domain: 'winrt', code: 'native-cleanup-failed', safeMessage, metadata: Object.freeze({}) } const failure: CleanupFailure = Object.freeze({ resourceKind, error: contractError('platform.failure', 'cleanup', operation, platform).normalized }) return Object.freeze({ state: 'release-failed', failures: Object.freeze([failure]) }) }