import type { DeepPartial, ProtectCameraConfig, ProtectChimeConfig, ProtectFobConfig, ProtectLightConfig, ProtectRelayConfig, ProtectSensorConfig, ProtectViewerConfig } from "../types/index.ts"; import type { HttpMethod, ProtectResponse } from "../transport/http.ts"; import type { DeviceContext } from "./context.ts"; import type { DeviceModelKey } from "../protocol/events.ts"; import type { ProtectState } from "../protocol/reducer.ts"; /** * The config-record union a device projection can wrap - every modeled device except the NVR singleton. * * @category Devices */ export type ProtectDeviceConfig = ProtectCameraConfig | ProtectChimeConfig | ProtectFobConfig | ProtectLightConfig | ProtectRelayConfig | ProtectSensorConfig | ProtectViewerConfig; /** * The read-only core every projection shares - whether it is a command-bearing device ({@link DeviceProjection}) or the read-only NVR singleton (`Nvr`). A projection is * **a live view, not a cached entity**: it holds only its context and a stable selector, and every property access reads through to the current store snapshot. Holding a * projection across hours of state change is safe - its getters always reflect the latest reduced state. * * This base supplies exactly what *every* projection has in common - the read-through `config` getter, its non-throwing `peek()` companion, the absent-record guard, and * live `observe()` - and nothing more. It is unbounded in `TConfig` and carries no `id`, because reading-through-and-observing is meaningful for any reduced record, * including a singleton like the NVR that has no id. The command surface (`update`, `reboot`) and device identity (`id`, `modelKey`, `name`, `isOnline`) live one level * down, on {@link DeviceProjection}. A read-only subject thus inherits the view machinery without the operations it does not support. * * If the underlying record has been removed from state (or has not arrived yet, as before the first bootstrap), the asserting getters (`config` and everything built on * it) throw - for a caller whose contract requires the record, a held handle to an absent one is a programming error. A caller that treats absence as an expected runtime * condition asks non-throwingly instead: `peek()` returns the config or `undefined` on the held handle itself, and a fresh lookup (`client.camera(id)`) likewise returns * `undefined` when the record is gone. * * @typeParam TConfig - The concrete config-record type this projection wraps. * * @category Devices */ export declare abstract class Projection { #private; protected readonly ctx: DeviceContext; protected constructor(ctx: DeviceContext, select: (state: ProtectState) => TConfig | undefined); /** * The record's current config, read through to the live store snapshot. * * @returns The current config. * * @throws {ReferenceError} If the record is not present in the controller state. */ get config(): Readonly; /** * The record's current config if it is present, or `undefined` if it is not - the non-throwing companion to {@link config}. Reach for `peek()` to ask "is it present, * and if so what is it?": a record leaving the controller state (a device unadopted or removed, or the NVR before the first bootstrap) is an expected runtime * condition, so `peek()` reports its absence as `undefined` rather than treating it as an error. Reach for {@link config} instead where the record's presence is a * precondition and an absence is a defect that should fail loudly. `peek() !== undefined` is the idiom for "is it still here?" on a held handle. * * Like every projection read this is a live read-through to the current store snapshot, not a value cached at construction, so it always reflects the latest reduced * state. It is the single home of the raw snapshot read; {@link config} is this read plus the absent-record guard. * * @returns The current config, or `undefined` if the record is not present in the controller state. */ peek(): Readonly | undefined; /** * Observe this projection. The returned async iterable yields `this` each time the record's config changes by reference, and stays silent on dispatches that did not * touch it. Pair it with the sync getters: getters for "what is it now", observe for "tell me when it changes". * * @param opts - Optional abort signal that terminates the iteration. * * @returns An async iterable of this projection, yielding once per change. */ observe(opts?: { signal?: AbortSignal; }): AsyncGenerator; /** * The message thrown by the absent-record guard, supplied by each concrete projection so the error names its specific subject (a device by id and modelKey, the NVR as * a singleton). Kept abstract rather than templated because the device and singleton phrasings genuinely differ. * * @returns The `ReferenceError` message for an absent record. */ protected abstract absenceMessage(): string; } /** * The shared base of every *device* projection (`Camera`, `Chime`, `Fob`, `Light`, `Relay`, `Sensor`, `Viewer`). It extends the read-only {@link Projection} core with * what every device category adds: a stable `id`, the `modelKey` discriminant, the `name` / `isOnline` getters, and the write-through `update` and `reboot` commands * (every Protect device type exposes a reboot endpoint). * * Commands are **write-through**: `update` PATCHes the controller and returns the same live handle, but does *not* fold the response into the store. State advances * solely through the reducer's two universal inputs - the realtime event stream and the bootstrap refresh failsafe - so the issuer learns of its own change the way every * other subscriber does (the controller broadcasts the resulting `devicePatched` to all WebSocket subscribers). The consequence is that a projection reflects a command's * effect once the stream (or the next refresh) delivers it, not synchronously after the `await`; consumers `observe()` rather than read-after-write. * * @typeParam TConfig - The concrete device-config-record type this projection wraps. * * @category Devices */ export declare abstract class DeviceProjection extends Projection { /** The device's stable id. */ readonly id: string; /** The device category discriminant, fixed by the concrete subclass. */ abstract readonly modelKey: DeviceModelKey; protected constructor(ctx: DeviceContext, id: string, select: (id: string) => (state: ProtectState) => TConfig | undefined); /** * The device's display name - its user-assigned `name` when set, otherwise the controller's `displayName`. * * @throws {ReferenceError} If the device is no longer present in the controller state. */ get name(): string; /** * Whether the device is currently connected to the controller. Uses the same definition as the `online` selectors, so the projection and the collection views never * disagree about what "online" means. * * @throws {ReferenceError} If the device is no longer present in the controller state. */ get isOnline(): boolean; /** * Issue an authenticated REST call to this device's endpoint and return the 2xx-classified response. This is the single home for the device-command mechanic - URL * construction, the JSON-body convention, content-type, the GET/HEAD no-body rule, signal threading, and `ensureOk` classification - so every command and query shares * one definition rather than re-spelling the request shape. A body-bearing method (any method other than GET/HEAD) always carries a JSON body - * the supplied `payload`, or `{}` for a parameterless command - so a command's wire shape never depends on whether it happens to take parameters; * `GET`/`HEAD` carry none. The caller keeps what is * genuinely its own - any capability guard (thrown before this is reached) and any extraction from the response (`.body` bytes, a parsed `json()` field) - and the * write-through contract holds: this returns the classified response but never folds it into the store. * * @param path - The suffix appended to this device's endpoint (e.g. `/reboot`, `/snapshot?w=640`); `""` targets the device record itself, as the `update` PATCH does. * @param opts - The HTTP `method` (default `POST`), an optional JSON `payload`, and an optional abort `signal`. * * @returns The 2xx-classified {@link ProtectResponse}; callers extract from it or ignore it. * * @throws The classified `FatalError` on a non-2xx, or a transport-level `ProtectError` on failure. */ protected dispatch(path: string, opts?: { method?: HttpMethod; payload?: object; signal?: AbortSignal; }): Promise; /** * Apply a configuration change to the device. Write-through: PATCHes the controller and returns this live handle on success. Does not mutate the store - the change * is reflected once the realtime stream (or the next refresh) delivers it. * * @param payload - The partial configuration to apply. * @param opts - Optional abort signal. * * @returns This projection. * * @throws The classified `FatalError` on a non-2xx (e.g., {@link ProtectAuthorizationError} when the account lacks admin rights), or a transport-level * `ProtectError` on failure. */ update(payload: DeepPartial, opts?: { signal?: AbortSignal; }): Promise; /** * Reboot the device. Every Protect device category exposes a reboot endpoint, so this is shared device behavior. Write-through: the controller drops and * re-establishes the device; the projection reflects the cycle as the realtime stream reports it (the response is not folded into the store). * * @param opts - Optional abort signal. * * @throws The classified `FatalError` on a non-2xx, or a transport-level `ProtectError` on failure. */ reboot(opts?: { signal?: AbortSignal; }): Promise; protected absenceMessage(): string; } //# sourceMappingURL=device.d.ts.map