/** * Pre-built selectors over {@link ProtectState}. Every selector is a **pure function returning config records** (`ProtectCameraConfig`, ...), never a Layer-3 `Camera` * projection - selectors live in the data layer, and a projection needs the client (Layer 3, built by the `DeviceRegistry`). * * Returning config records is required, not stylistic. {@link StateStore.observe} detects change with `Object.is`; the reducer keeps a record's reference stable * across dispatches that did not touch it (structural sharing), and these selectors keep a *derived array's* reference stable across dispatches that did not touch its * backing map (memoization on map identity). A selector that constructed a fresh `Camera` on each call would compare unequal every time, fire every observer on every * dispatch, and defeat the "refresh is invisible when nothing drifted" guarantee. * * Memoization is keyed on the backing `Map` reference via a `WeakMap`: when the map identity is unchanged, the previously computed array is returned (`Object.is`-equal * to the prior return) and is garbage-collected automatically once the state that held the map is gone. * * That map-identity memo is the right regime for a selector that returns *records* (`all`, `online`): those views must change reference whenever any member's config * changes, because the consumer reads the records. A selector that returns a *membership set* - the ids adopted by this controller - wants the opposite: it must stay * referentially stable through config churn and change only when an id enters or leaves the set. That is a second, distinct regime, **content memoization** (see * {@link memoizeArrayByContent}): layered over the map-identity memo, it returns the previously computed array whenever the freshly derived one is element-for-element * equal, so a `lastSeen`/stats patch that rebuilds the backing map does not wake a membership observer - only a real add, removal, or adoption flip does. * * @module StateSelectors */ import type { ProtectCameraConfig, ProtectChimeConfig, ProtectFobConfig, ProtectLightConfig, ProtectNvrConfig, ProtectNvrLiveviewConfig, ProtectNvrUserConfig, ProtectRelayConfig, ProtectRingtoneConfig, ProtectSensorConfig, ProtectViewerConfig } from "../types/index.ts"; import type { DeviceCollectionKey } from "../protocol/events.ts"; import type { ProtectState } from "../protocol/reducer.ts"; /** * Whether a device is currently connected to the controller. The single definition of "online" shared by the `online` selectors and the device projections' * `isOnline` getter, so the collection views and the per-device view never disagree. * * @param device - Any device config (only its `state` field is read). * * @returns `true` when the device is fully connected. * * @category State */ export declare function isDeviceOnline(device: { state: string; }): boolean; /** * Whether a device is adopted by *this* controller - owned and managed here, rather than merely visible on the network or owned by a different NVR. The single * definition of "adopted by us", shared by the membership-id selectors so the collection views and any per-device check never disagree, exactly as {@link isDeviceOnline} * is the single definition of "online". * * The predicate is **ownership**, not **readiness**: `isAdopted` is set once any controller has adopted the device, and `isAdoptedByOther` marks one adopted by a * *different* NVR, so `isAdopted && !isAdoptedByOther` is precisely "ours". We deliberately do not also exclude the transient `isAdopting`: a device mid-adoption has not * yet flipped `isAdopted` true (so it is already excluded), and a device being *re*-provisioned is still owned by us - dropping it from the membership set on a transient * would churn the set, the opposite of what the membership selectors are for. "Can it stream right now?" is liveness ({@link isDeviceOnline}), a separate question. The * predicate stays this simple because the reducer already refuses the provably-false record - `isAdoptedByOther` true while the record names this very controller as its * owner - at ingestion, so this read never has to second-guess a self-contradictory flag. * * @param device - Any device config (only its adoption flags are read). * * @returns `true` when the device is adopted by this controller and not by another. * * @category State */ export declare function isDeviceAdopted(device: { isAdopted: boolean; isAdoptedByOther: boolean; }): boolean; /** * The livestream (fMP4) audio sample rate for a camera, in hertz. On the fMP4 livestream, Protect doorbells deliver AAC audio at 48 kHz and every other camera delivers * 16 kHz, so this is the single source of that wire fact - a consumer branching on the rate reads it here rather than re-deriving it from the doorbell flag. The scope is * the livestream source rate only: the RTSP transport delivers 48 kHz regardless of camera type, a separate fact this helper deliberately does not model. * * @param camera - Any camera config (only `featureFlags.isDoorbell` is read). * * @returns 48000 for a doorbell, 16000 for every other camera. * * @category State */ export declare function livestreamAudioSampleRate(camera: { featureFlags: { isDoorbell: boolean; }; }): 16000 | 48000; /** * The base pair of selectors every id-keyed collection exposes: the full array (memoized on the backing map's identity) and an O(1) by-id lookup. * * @typeParam T - The collection's config record type. * * @category State */ export interface CollectionViews { all: (state: ProtectState) => readonly T[]; byId: (id: string) => (state: ProtectState) => T | undefined; } /** * The selectors every *device* collection exposes: the {@link CollectionViews} pair (records, memoized on map identity), the connected-only subset, and the * content-memoized set of ids adopted by this controller - the membership set a reactive consumer observes to react to devices being adopted or removed without waking on * every config patch. See `memoizeArrayByContent` for the content-vs-identity memoization distinction `adoptedIds` rests on. * * @typeParam T - The collection's config record type. * * @category State */ export interface CollectionSelectors extends CollectionViews { adoptedIds: (state: ProtectState) => readonly string[]; online: (state: ProtectState) => readonly T[]; } /** * The key-to-config map for the id-keyed device collections: each {@link DeviceCollectionKey} paired with the config-record type its collection holds. The catalog below * is typed against it, and consumers derive the device-config union from it (the library's `ProtectDeviceConfig`), so the collection vocabulary and its record types stay * locked to one map rather than drifting across a hand-maintained union. * * @category State */ export interface ProtectDeviceConfigMap { camera: ProtectCameraConfig; chime: ProtectChimeConfig; fob: ProtectFobConfig; light: ProtectLightConfig; relay: ProtectRelayConfig; sensor: ProtectSensorConfig; viewer: ProtectViewerConfig; } /** * The category-keyed selector catalog: one {@link CollectionSelectors} quartet per {@link DeviceCollectionKey}, each exactly typed to its category's config record. The * single surface a consumer reaches every device collection's selectors through - `deviceSelectors.camera.all`, `deviceSelectors[key].byId(id)`, and the rest - so the * map-identity and content-memoization regimes this module documents reach every category through one entry rather than a * flat export per category. The mapped type pins each entry to its own config record, so `deviceSelectors.camera.all` returns `readonly ProtectCameraConfig[]` while * generic iteration over the key set stays fully typed. * * @category State */ export declare const deviceSelectors: { readonly [K in DeviceCollectionKey]: CollectionSelectors; }; /** All liveviews, memoized on the liveview-map identity. Each record carries its `slots[].cameras`. @category State */ export declare const selectLiveviews: (state: Readonly<{ authUserId: string | null; bootstrapId: number; cameras: ReadonlyMap; chimes: ReadonlyMap; fobs: ReadonlyMap; lights: ReadonlyMap; liveviews: ReadonlyMap; nvr: ProtectNvrConfig | null; relays: ReadonlyMap; ringtones: ReadonlyMap; sensors: ReadonlyMap; users: ReadonlyMap; viewers: ReadonlyMap; }>) => readonly import("../types/nvr.ts").ProtectNvrLiveviewConfigInterface[]; /** A single liveview by id, or `undefined` when absent. @category State */ export declare const selectLiveview: (id: string) => (state: Readonly<{ authUserId: string | null; bootstrapId: number; cameras: ReadonlyMap; chimes: ReadonlyMap; fobs: ReadonlyMap; lights: ReadonlyMap; liveviews: ReadonlyMap; nvr: ProtectNvrConfig | null; relays: ReadonlyMap; ringtones: ReadonlyMap; sensors: ReadonlyMap; users: ReadonlyMap; viewers: ReadonlyMap; }>) => import("../types/nvr.ts").ProtectNvrLiveviewConfigInterface | undefined; /** All ringtones, memoized on the ringtone-map identity. @category State */ export declare const selectRingtones: (state: Readonly<{ authUserId: string | null; bootstrapId: number; cameras: ReadonlyMap; chimes: ReadonlyMap; fobs: ReadonlyMap; lights: ReadonlyMap; liveviews: ReadonlyMap; nvr: ProtectNvrConfig | null; relays: ReadonlyMap; ringtones: ReadonlyMap; sensors: ReadonlyMap; users: ReadonlyMap; viewers: ReadonlyMap; }>) => readonly import("../types/nvr.ts").ProtectRingtoneConfigInterface[]; /** A single ringtone by id, or `undefined` when absent. @category State */ export declare const selectRingtone: (id: string) => (state: Readonly<{ authUserId: string | null; bootstrapId: number; cameras: ReadonlyMap; chimes: ReadonlyMap; fobs: ReadonlyMap; lights: ReadonlyMap; liveviews: ReadonlyMap; nvr: ProtectNvrConfig | null; relays: ReadonlyMap; ringtones: ReadonlyMap; sensors: ReadonlyMap; users: ReadonlyMap; viewers: ReadonlyMap; }>) => import("../types/nvr.ts").ProtectRingtoneConfigInterface | undefined; /** * The authenticated session's own user record, or `null` when there is no such user (before the first bootstrap, or if the id is absent from the roster). The * single-source primitive for session-identity facts: {@link selectIsAdmin} derives from it, and consumers can `observe` it directly to react to "who am I" changes. * Reads straight through the roster map, so the returned record reference is structurally-sharing-stable across dispatches that did not touch it - no memoization needed. * * @param state - The current state. * * @returns The authenticated user's config record, or `null`. * * @category State */ export declare function selectAuthUser(state: ProtectState): ProtectNvrUserConfig | null; /** * Whether the authenticated session has Super Admin (camera-write) privileges. Derived from the session user's record and the controller's permission grammar; `false` * before the first bootstrap or when the session's user is absent. Re-evaluated whenever the user's record changes, so a role change at the controller surfaces as * soon as the record next changes, whether through a realtime `user` patch (the common, low-latency path) or a periodic bootstrap refresh (the failsafe). * * @param state - The current state. * * @returns `true` when the authenticated user holds the administrative camera-write permission. * * @category State */ export declare function selectIsAdmin(state: ProtectState): boolean; /** * The controller's NVR configuration record, or `null` before the first bootstrap. The single-source primitive the {@link Nvr} singleton projection reads through (its * `config` getter and `observe()`), and the record the narrower NVR-derived selectors ({@link selectControllerName}, ...) pull their fields from. Reads straight through * the `nvr` singleton field, so the returned reference is structurally-sharing-stable across dispatches that did not touch it - no memoization needed; the observer's * `Object.is` dedup yields only when the record actually changes. * * @param state - The current state. * * @returns The NVR config record, or `null` when no bootstrap has been applied. * * @category State */ export declare function selectNvr(state: ProtectState): ProtectNvrConfig | null; /** * The controller's display label - its user-assigned name, else the always-present `marketName` - or `null` only before the first bootstrap (no `nvr` record yet), never * for a merely-unnamed controller (which surfaces its model). Single-sourced with `Nvr.name`, so the projection getter and this primitive never disagree; a consumer * needing the raw "was it explicitly named?" distinction reads `client.nvr.config.name`. Returns a primitive, so it needs no memoization: the value is its own * `Object.is` key for the observer's dedup, and two property reads are already cheaper than a cache lookup. * * @param state - The current state. * * @returns The NVR's display label, or `null` when the NVR is not yet known. * * @category State */ export declare function selectControllerName(state: ProtectState): string | null; /** * The controller's self-reported boot time (`nvr.upSince`), or `null` before the first bootstrap. We mirror the controller's own field name rather than inventing one, * exactly as {@link selectControllerName} mirrors `nvr.name` - the library renames only derived facts, never a value it passes straight through. * * This is the `ConnectionMonitor`'s **internal** reboot-detection input, not a consumer-facing selector, which is why it is not exported from the package surface. * Reboot is a derived, policy-laden fact with a single home: the monitor applies a noise-floor threshold to `upSince` (the wire value jitters by milliseconds across * bootstraps) and emits the blessed `controllerRebooted` event. A consumer wanting "did the controller reboot?" subscribes to that event; observing this raw selector * directly would re-derive reboot detection while bypassing the jitter threshold, firing spuriously on every refresh - a second, wrong source of truth. * * Returns a primitive, so it needs no memoization: even though the `nvr` record reference churns on nearly every refresh (`lastSeen`, `storageStats`), the monitor's * `Object.is` dedup over this selector yields only when the boot time itself changes. * * @param state - The current state. * * @returns The controller's boot time in epoch milliseconds, or `null` when the NVR is not yet known. * * @internal */ export declare function selectControllerUpSince(state: ProtectState): number | null; //# sourceMappingURL=selectors.d.ts.map