// src/backend-contract/gatt.ts import { contractError } from './errors' import type { CleanupRecord } from './errors' import { attachmentRecordsEqual, type AttachmentRecord } from './identity' import type { AttachmentId, BorrowedBytes, ConnectionId, GattDatabaseId, GenerationId, LeaseId, OwnedBytes, PeerId, SubscriptionId, Uuid } from './primitives' import type { PublicOperationOptions, SubscriptionOptions, WriteMode, WritePolicy, WriteReceipt } from './operations' import type { BoundedAsyncStream } from './streams' export type PathValidity = 'current' | 'stale' export interface GattDatabaseChangedEvent { readonly previousGeneration: string readonly reason: 'service-changed' | 'reconnect' | 'backend-reset' | 'manual-rediscovery' readonly affectedHandleRange: { readonly start: number; readonly end: number } | null } export interface DevicePath { readonly attachment: AttachmentRecord readonly attachmentId: AttachmentId readonly peerId: PeerId } export interface ConnectionPath extends DevicePath { readonly connectionId: ConnectionId readonly ownerLeaseId: LeaseId readonly connectionGeneration: GenerationId<'connection-generation', Connection> } export interface DatabasePath extends ConnectionPath { readonly databaseId: GattDatabaseId readonly databaseGeneration: GenerationId<'database-generation', Database> } export interface ServicePath< Attachment extends string, Connection extends string, Database extends string, ServiceScope extends string > extends DatabasePath { readonly serviceUuid: Uuid readonly serviceOccurrence: GenerationId<'service-occurrence', ServiceScope> } export interface CharacteristicPath< Attachment extends string, Connection extends string, Database extends string, ServiceScope extends string, CharacteristicScope extends string, Validity extends PathValidity = 'current' > extends ServicePath { readonly characteristicUuid: Uuid readonly characteristicOccurrence: GenerationId<'characteristic-occurrence', CharacteristicScope> readonly validity: Validity } export interface DescriptorPath< Attachment extends string, Connection extends string, Database extends string, ServiceScope extends string, CharacteristicScope extends string, DescriptorScope extends string, Validity extends PathValidity = 'current' > extends CharacteristicPath { readonly descriptorUuid: Uuid readonly descriptorOccurrence: GenerationId<'descriptor-occurrence', DescriptorScope> } export interface Service< Attachment extends string, Connection extends string, Database extends string, Occurrence extends string > { readonly path: ServicePath readonly primary: boolean readonly includedServices: readonly GattServiceReference[] } export interface Characteristic< Attachment extends string, Connection extends string, Database extends string, ServiceOccurrence extends string, Occurrence extends string > { readonly path: CharacteristicPath readonly properties: CharacteristicProperties readonly access: GattAccessRequirements } /** Complete normalized operation metadata captured at GATT discovery time. */ export interface CharacteristicProperties { readonly broadcast: boolean readonly read: boolean readonly writeWithResponse: boolean readonly writeWithoutResponse: boolean readonly authenticatedSignedWrites: boolean readonly notify: boolean readonly indicate: boolean readonly extendedProperties: boolean readonly reliableWrite: boolean readonly writableAuxiliaries: boolean readonly availability: GattCharacteristicPropertyAvailability } export interface GattCharacteristicPropertyAvailability { readonly broadcast: 'known' | 'unknown' readonly read: 'known' | 'unknown' readonly writeWithResponse: 'known' | 'unknown' readonly writeWithoutResponse: 'known' | 'unknown' readonly authenticatedSignedWrites: 'known' | 'unknown' readonly notify: 'known' | 'unknown' readonly indicate: 'known' | 'unknown' readonly extendedProperties: 'known' | 'unknown' readonly reliableWrite: 'known' | 'unknown' readonly writableAuxiliaries: 'known' | 'unknown' } export interface GattAccessRequirements { readonly read: 'none' | 'encrypted' | 'authenticated' | 'authorized' | 'unknown' readonly write: 'none' | 'encrypted' | 'authenticated' | 'authorized' | 'unknown' } export interface GattDescriptorProperties { readonly read: boolean readonly write: boolean readonly availability: { readonly read: 'known' | 'unknown' readonly write: 'known' | 'unknown' } readonly access: GattAccessRequirements } export interface GattServiceReference { readonly uuid: Uuid readonly occurrence: string } export interface GattCharacteristicPropertyInput { readonly read: boolean readonly writeWithResponse: boolean readonly writeWithoutResponse: boolean readonly notify: boolean readonly indicate?: boolean readonly broadcast?: boolean readonly authenticatedSignedWrites?: boolean readonly extendedProperties?: boolean readonly reliableWrite?: boolean readonly writableAuxiliaries?: boolean readonly availability?: Partial } export function createGattCharacteristicProperties(input: GattCharacteristicPropertyInput): CharacteristicProperties { const optionalAvailability = input.availability ?? {} return Object.freeze({ broadcast: input.broadcast ?? false, read: input.read, writeWithResponse: input.writeWithResponse, writeWithoutResponse: input.writeWithoutResponse, authenticatedSignedWrites: input.authenticatedSignedWrites ?? false, notify: input.notify, indicate: input.indicate ?? false, extendedProperties: input.extendedProperties ?? false, reliableWrite: input.reliableWrite ?? false, writableAuxiliaries: input.writableAuxiliaries ?? false, availability: Object.freeze({ broadcast: optionalAvailability.broadcast ?? (input.broadcast === undefined ? 'unknown' : 'known'), read: optionalAvailability.read ?? 'known', writeWithResponse: optionalAvailability.writeWithResponse ?? 'known', writeWithoutResponse: optionalAvailability.writeWithoutResponse ?? 'known', authenticatedSignedWrites: optionalAvailability.authenticatedSignedWrites ?? (input.authenticatedSignedWrites === undefined ? 'unknown' : 'known'), notify: optionalAvailability.notify ?? 'known', indicate: optionalAvailability.indicate ?? (input.indicate === undefined ? 'unknown' : 'known'), extendedProperties: optionalAvailability.extendedProperties ?? (input.extendedProperties === undefined ? 'unknown' : 'known'), reliableWrite: optionalAvailability.reliableWrite ?? (input.reliableWrite === undefined ? 'unknown' : 'known'), writableAuxiliaries: optionalAvailability.writableAuxiliaries ?? (input.writableAuxiliaries === undefined ? 'unknown' : 'known') }) }) } export function createGattDescriptorProperties( read: boolean, write: boolean, availability: GattDescriptorProperties['availability'], access: GattAccessRequirements ): GattDescriptorProperties { return Object.freeze({ read, write, availability: Object.freeze({ ...availability }), access: Object.freeze({ ...access }) }) } export interface Descriptor< Attachment extends string, Connection extends string, Database extends string, ServiceOccurrence extends string, CharacteristicOccurrence extends string, Occurrence extends string > { readonly path: DescriptorPath< Attachment, Connection, Database, ServiceOccurrence, CharacteristicOccurrence, Occurrence > readonly properties: GattDescriptorProperties } export interface GattDatabaseSnapshot { readonly path: DatabasePath readonly services: readonly Service[] readonly characteristics: readonly Characteristic[] readonly descriptors: readonly Descriptor[] } export interface GattDatabase { readonly path: DatabasePath snapshot(): Promise> read( path: CharacteristicPath, options: PublicOperationOptions ): Promise write( path: CharacteristicPath, value: BorrowedBytes, options: WritePolicy ): Promise> readDescriptor< ServiceOccurrence extends string, CharacteristicOccurrence extends string, DescriptorOccurrence extends string >( path: DescriptorPath< Attachment, Connection, Database, ServiceOccurrence, CharacteristicOccurrence, DescriptorOccurrence, 'current' >, options: PublicOperationOptions ): Promise writeDescriptor< ServiceOccurrence extends string, CharacteristicOccurrence extends string, DescriptorOccurrence extends string >( path: DescriptorPath< Attachment, Connection, Database, ServiceOccurrence, CharacteristicOccurrence, DescriptorOccurrence, 'current' >, value: BorrowedBytes, options: WritePolicy ): Promise> subscribe( path: CharacteristicPath, options: SubscriptionOptions ): Promise> } /** Current backend observation used by the portable chunking policy. */ export interface MaximumWriteLengthObservation { readonly connectionId: ConnectionId readonly connectionGeneration: GenerationId<'connection-generation', string> readonly mode: WriteMode readonly maximumWriteLength: number readonly observedAtMonotonicMs: number } export interface NotificationValue { readonly value: OwnedBytes readonly indication: boolean } export interface Subscription< Attachment extends string = string, Connection extends string = string, Database extends string = string, ServiceOccurrence extends string = string, CharacteristicOccurrence extends string = string, SubscriptionScope extends string = string > { readonly subscriptionId: SubscriptionId< Attachment, Connection, Database, ServiceOccurrence, CharacteristicOccurrence, SubscriptionScope > readonly path: CharacteristicPath< Attachment, Connection, Database, ServiceOccurrence, CharacteristicOccurrence, 'current' > readonly values: BoundedAsyncStream remove(): Promise } export function assertCurrentPath< Attachment extends string, Connection extends string, Database extends string, ServiceOccurrence extends string, CharacteristicOccurrence extends string >( path: CharacteristicPath ): asserts path is CharacteristicPath< Attachment, Connection, Database, ServiceOccurrence, CharacteristicOccurrence, 'current' > { if (path.validity !== 'current') { throw contractError('gatt.stale-handle', 'gatt', 'gatt.assert-current-path') } } export function assertPathMatchesAttachment( path: DevicePath, attachment: AttachmentRecord ): void { if (path.attachmentId !== attachment.attachmentId || !attachmentRecordsEqual(path.attachment, attachment)) { throw contractError('gatt.stale-handle', 'gatt', 'gatt.assert-path-matches-attachment') } }