@camstack/ui-library 1.2.16 → 1.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composites/camera-stream-player.d.ts +16 -1
- package/dist/composites/cap-settings/RecordingSettings.d.ts +1 -5
- package/dist/composites/copy-button.d.ts +11 -2
- package/dist/composites/data-table-layout.d.ts +35 -0
- package/dist/composites/data-table.d.ts +9 -1
- package/dist/composites/device-list/columns.d.ts +27 -2
- package/dist/composites/index.d.ts +6 -0
- package/dist/composites/key-value-list.d.ts +8 -0
- package/dist/composites/reconnect-schedule.d.ts +32 -0
- package/dist/composites/setting-row.d.ts +24 -0
- package/dist/generated/system-hooks.d.ts +4 -0
- package/dist/hooks/turn-server-cache.d.ts +77 -0
- package/dist/index.cjs +407 -151
- package/dist/index.js +396 -152
- package/dist/lib/responsive.d.ts +28 -0
- package/dist/lib/tailwind-variants.d.ts +27 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { CamProfile } from '@camstack/types';
|
|
3
|
+
import { ReconnectAction } from './reconnect-schedule';
|
|
3
4
|
type WebkitPresentationMode = 'inline' | 'picture-in-picture' | 'fullscreen';
|
|
4
5
|
declare global {
|
|
5
6
|
interface HTMLVideoElement {
|
|
@@ -145,6 +146,13 @@ export interface CameraStreamPlayerProps {
|
|
|
145
146
|
onStateChange?: (state: PlayerConnectionState) => void;
|
|
146
147
|
/** Called on error */
|
|
147
148
|
onError?: (error: string) => void;
|
|
149
|
+
/**
|
|
150
|
+
* Observer of the auto-retry loop: called with every scheduled attempt and
|
|
151
|
+
* with the exhaustion edge. Hosts wire it to their log pipeline (tagged with
|
|
152
|
+
* the camera) — a retry loop nobody can see is how a camera stays dark
|
|
153
|
+
* while looking busy.
|
|
154
|
+
*/
|
|
155
|
+
onReconnectAttempt?: (action: ReconnectAction) => void;
|
|
148
156
|
/** Custom overlay rendered on top of the video */
|
|
149
157
|
overlay?: ReactNode;
|
|
150
158
|
/**
|
|
@@ -242,6 +250,13 @@ export interface CameraStreamPlayerProps {
|
|
|
242
250
|
* live player — no data channel is created and behaviour is unchanged.
|
|
243
251
|
*/
|
|
244
252
|
onControlChannel?: (channel: RTCDataChannel) => void;
|
|
253
|
+
/**
|
|
254
|
+
* Optional handle to the player's `<video>` element, delivered once on mount
|
|
255
|
+
* (and `null` on unmount). Consumers that pace server-pushed frames by what
|
|
256
|
+
* the viewer actually RENDERED (`requestVideoFrameCallback`) need the
|
|
257
|
+
* element itself — the recorded-scrub receiver-credit acks are the use case.
|
|
258
|
+
*/
|
|
259
|
+
onVideoElement?: (el: HTMLVideoElement | null) => void;
|
|
245
260
|
}
|
|
246
|
-
export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className, onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, }: CameraStreamPlayerProps): import("react").JSX.Element;
|
|
261
|
+
export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className, onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, onVideoElement, }: CameraStreamPlayerProps): import("react").JSX.Element;
|
|
247
262
|
export {};
|
|
@@ -3,10 +3,6 @@ interface RecordingSettingsProps {
|
|
|
3
3
|
readonly initial: RecordingConfig;
|
|
4
4
|
readonly saving: boolean;
|
|
5
5
|
readonly onSave: (config: RecordingConfig) => void;
|
|
6
|
-
/** Regenerate the scrub strips for this camera (clear + rebuild from the
|
|
7
|
-
* recorded low segments). Absent → the button is hidden. */
|
|
8
|
-
readonly onRegenerateStrips?: () => void;
|
|
9
|
-
readonly regeneratingStrips?: boolean;
|
|
10
6
|
}
|
|
11
|
-
export declare function RecordingSettings({ initial, saving, onSave
|
|
7
|
+
export declare function RecordingSettings({ initial, saving, onSave }: RecordingSettingsProps): import("react").JSX.Element;
|
|
12
8
|
export {};
|
|
@@ -2,11 +2,20 @@ import { ReactElement } from 'react';
|
|
|
2
2
|
export interface CopyButtonProps {
|
|
3
3
|
/** String copied to the clipboard when the button is pressed. */
|
|
4
4
|
readonly value: string;
|
|
5
|
-
/** Optional visible label rendered next to the icon. Icon-only when omitted.
|
|
5
|
+
/** Optional visible label rendered next to the icon. Icon-only when omitted.
|
|
6
|
+
*
|
|
7
|
+
* Do NOT pass the name of the field the button sits next to: the row
|
|
8
|
+
* already shows it, and the repeat is what overlapped the value on a phone
|
|
9
|
+
* (a token row rendered its own name on top of the masked secret). Use
|
|
10
|
+
* {@link CopyButtonProps.srLabel} for that — it names the target for screen
|
|
11
|
+
* readers without spending horizontal space. */
|
|
6
12
|
readonly label?: string;
|
|
13
|
+
/** Names what is copied, for assistive tech only — never rendered. Falls back
|
|
14
|
+
* to `label`, then to a generic word. */
|
|
15
|
+
readonly srLabel?: string;
|
|
7
16
|
/** Extra classes merged onto the button. */
|
|
8
17
|
readonly className?: string;
|
|
9
18
|
/** Disable the button (e.g. nothing to copy). */
|
|
10
19
|
readonly disabled?: boolean;
|
|
11
20
|
}
|
|
12
|
-
export declare function CopyButton({ value, label, className, disabled }: CopyButtonProps): ReactElement;
|
|
21
|
+
export declare function CopyButton({ value, label, srLabel, className, disabled, }: CopyButtonProps): ReactElement;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one decision a shared table has to make on a phone.
|
|
3
|
+
*
|
|
4
|
+
* A wide table has no good phone rendering. There are exactly two honest
|
|
5
|
+
* answers — become a list of cards, or scroll horizontally INSIDE its own
|
|
6
|
+
* container while the page body never moves sideways — and shrinking the font
|
|
7
|
+
* is not one of them. `DataTable` supports both; this module decides which, so
|
|
8
|
+
* the choice is a pure, testable rule instead of per-page CSS.
|
|
9
|
+
*
|
|
10
|
+
* The default rule is column count. Below the card breakpoint a phone fits
|
|
11
|
+
* roughly three readable columns; past that, a sideways scroll means the
|
|
12
|
+
* operator reads a wide table through a slit and loses the row they started
|
|
13
|
+
* on. Cards trade the column alignment (which nobody can use at that width
|
|
14
|
+
* anyway) for every field of one row being readable at once.
|
|
15
|
+
*
|
|
16
|
+
* Pass an explicit `mobileMode` to override per table — a wide table whose
|
|
17
|
+
* value IS the side-by-side comparison (a schedule, a matrix) is better
|
|
18
|
+
* scrolled than carded, and says so at its call site.
|
|
19
|
+
*/
|
|
20
|
+
/** Per-table override. `auto` applies the column-count rule below. */
|
|
21
|
+
export type TableMobileMode = 'auto' | 'scroll' | 'cards';
|
|
22
|
+
/** How the table renders once the decision is made. */
|
|
23
|
+
export type TableLayoutKind = 'table' | 'cards';
|
|
24
|
+
/**
|
|
25
|
+
* At this many columns and above, `auto` switches a narrow viewport to cards.
|
|
26
|
+
* Three columns still fit a phone at a readable size; four do not.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CARD_MODE_MIN_COLUMNS = 4;
|
|
29
|
+
export interface ResolveTableLayoutInput {
|
|
30
|
+
readonly mode: TableMobileMode;
|
|
31
|
+
readonly columnCount: number;
|
|
32
|
+
/** Viewport below the card breakpoint (`useIsMobile`). */
|
|
33
|
+
readonly isNarrow: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function resolveTableLayout({ mode, columnCount, isNarrow, }: ResolveTableLayoutInput): TableLayoutKind;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
+
import { TableMobileMode } from './data-table-layout';
|
|
2
3
|
export interface DataColumn<T> {
|
|
3
4
|
/** Stable key for React reconciliation + tests. */
|
|
4
5
|
readonly key: string;
|
|
@@ -33,5 +34,12 @@ export interface DataTableProps<T> {
|
|
|
33
34
|
readonly bordered?: boolean;
|
|
34
35
|
/** Per-row className resolver (e.g. status-coloured backgrounds). */
|
|
35
36
|
readonly rowClassName?: (row: T, rowIndex: number) => string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Narrow-viewport rendering. Default `auto` — cards from
|
|
39
|
+
* `CARD_MODE_MIN_COLUMNS` columns up, rows below it. Pass `scroll` when the
|
|
40
|
+
* point of the table is the side-by-side comparison, `cards` to force the
|
|
41
|
+
* card list at any column count.
|
|
42
|
+
*/
|
|
43
|
+
readonly mobileMode?: TableMobileMode;
|
|
36
44
|
}
|
|
37
|
-
export declare function DataTable<T>({ columns, rows, rowKey, onRowClick, minWidthPx, emptyMessage, className, bordered, rowClassName, }: DataTableProps<T>): import("react").JSX.Element | null;
|
|
45
|
+
export declare function DataTable<T>({ columns, rows, rowKey, onRowClick, minWidthPx, emptyMessage, className, bordered, rowClassName, mobileMode, }: DataTableProps<T>): import("react").JSX.Element | null;
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* longest). So as the viewport shrinks the HIGHEST-priority-number column
|
|
14
14
|
* drops first.
|
|
15
15
|
*
|
|
16
|
-
* name (0) — never hidden;
|
|
16
|
+
* name (0) — never hidden; pinned left from `md` up (see
|
|
17
|
+
* `NAME_COLUMN_PIN_CLASS`), scrolls below it
|
|
17
18
|
* previewActions (1) — never hidden; carries the live control + status dot
|
|
18
19
|
* icon (2) — integration badge; rendered INSIDE the name cell,
|
|
19
20
|
* not a standalone column, so it has no breakpoint
|
|
@@ -44,7 +45,7 @@ export type TableContext = 'devices' | 'integration-detail';
|
|
|
44
45
|
export declare function columnsForContext(ctx: TableContext): readonly DeviceColumnId[];
|
|
45
46
|
/**
|
|
46
47
|
* Lower number = higher priority (kept longest as width shrinks). `name` = 0
|
|
47
|
-
* (
|
|
48
|
+
* (never dropped); `previewActions` = 1 (always visible). The optional
|
|
48
49
|
* columns drop in REVERSE priority order as width shrinks — highest number
|
|
49
50
|
* (`type`) hides first, then `features`. `icon` is a name-cell badge, not a
|
|
50
51
|
* standalone column. See `COLUMN_BREAKPOINT_CLASS` for the derived classes.
|
|
@@ -66,3 +67,27 @@ export declare const COLUMN_PRIORITY: Record<DeviceColumnId, number>;
|
|
|
66
67
|
* `features` (3) below `md`.
|
|
67
68
|
*/
|
|
68
69
|
export declare const COLUMN_BREAKPOINT_CLASS: Record<DeviceColumnId, string>;
|
|
70
|
+
/**
|
|
71
|
+
* ── The NAME column pin ───────────────────────────────────────────────────
|
|
72
|
+
*
|
|
73
|
+
* A sticky first column earns its keep only where the remaining columns still
|
|
74
|
+
* fit BESIDE it. Read the drop order above: below `md` every optional column
|
|
75
|
+
* is already folded away, so the table is NAME + Preview and there is nothing
|
|
76
|
+
* left to scroll under the pin — the pinned column simply holds its width
|
|
77
|
+
* permanently and pushes Preview off the right edge. On a phone the viewport
|
|
78
|
+
* is narrower than the pinned column alone, which is the reported defect:
|
|
79
|
+
* "il nome alla sinistra è fixed, dovrebbe scorrere naturalmente."
|
|
80
|
+
*
|
|
81
|
+
* So the pin starts at `md`, the same width at which `features` returns and a
|
|
82
|
+
* column exists to scroll under it. Below `md` the name column scrolls with
|
|
83
|
+
* everything else.
|
|
84
|
+
*/
|
|
85
|
+
export declare const NAME_COLUMN_PIN_BREAKPOINT: "md";
|
|
86
|
+
/** Pin classes for the NAME `<th>`/`<td>` — never unconditional. */
|
|
87
|
+
export declare const NAME_COLUMN_PIN_CLASS: "md:sticky md:left-0 md:z-[1]";
|
|
88
|
+
/**
|
|
89
|
+
* NAME column width. Narrower below the pin breakpoint so NAME + Preview fit a
|
|
90
|
+
* phone viewport without a horizontal scroll at all; the roomier desktop width
|
|
91
|
+
* returns with the pin.
|
|
92
|
+
*/
|
|
93
|
+
export declare const NAME_COLUMN_WIDTH_CLASS: "w-44 max-w-[11rem] md:w-64 md:max-w-[16rem]";
|
|
@@ -27,6 +27,10 @@ export * from './error-box';
|
|
|
27
27
|
export * from './confirm-dialog';
|
|
28
28
|
export * from './stat-card';
|
|
29
29
|
export * from './key-value-list';
|
|
30
|
+
export { SettingRow } from './setting-row';
|
|
31
|
+
export type { SettingRowElements, SettingRowProps } from './setting-row';
|
|
32
|
+
export { CARD_MODE_MIN_COLUMNS, resolveTableLayout } from './data-table-layout';
|
|
33
|
+
export type { TableLayoutKind, TableMobileMode } from './data-table-layout';
|
|
30
34
|
export * from './code-block';
|
|
31
35
|
export * from './filter-bar';
|
|
32
36
|
export * from './app-shell';
|
|
@@ -74,6 +78,8 @@ export type { ProbeState } from './config-form-field';
|
|
|
74
78
|
export { DetectionOverlay } from './detection-overlay';
|
|
75
79
|
export type { DetectionOverlayProps } from './detection-overlay';
|
|
76
80
|
export { CameraStreamPlayer } from './camera-stream-player';
|
|
81
|
+
export { nextReconnectAction, RECONNECT_POLICY } from './reconnect-schedule';
|
|
82
|
+
export type { ReconnectAction, ReconnectPolicy } from './reconnect-schedule';
|
|
77
83
|
export type { CameraStreamPlayerProps, PlayerConnectionState, PlayerWebrtcTarget, SignalingResult, ClientOfferResult, ClientStreamHints, SessionSignalingState, } from './camera-stream-player';
|
|
78
84
|
export { StreamPanel } from './stream-panel';
|
|
79
85
|
export type { StreamPanelProps, StreamChoice, StreamStats } from './stream-panel';
|
|
@@ -6,4 +6,12 @@ export interface KeyValueListProps {
|
|
|
6
6
|
}[];
|
|
7
7
|
className?: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* A `<dl>` of label/value rows. Layout (including the L1 stacking) is owned by
|
|
11
|
+
* `SettingRow`; this composite only supplies the description-list semantics.
|
|
12
|
+
*
|
|
13
|
+
* The rows used to be `flex items-center h-7` with a `w-1/3` term, so a label
|
|
14
|
+
* could neither wrap nor stack: on a phone it took a third of the width and
|
|
15
|
+
* the value took whatever was left.
|
|
16
|
+
*/
|
|
9
17
|
export declare function KeyValueList({ items, className }: KeyValueListProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconnect schedule — the stream player's auto-retry policy as a pure
|
|
3
|
+
* decision function.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists as a module: the inline version had two silent behaviours the
|
|
6
|
+
* operator ran into — exhaustion returned without a trace (the retry loop
|
|
7
|
+
* just stopped, so a camera stayed dark while looking busy), and no attempt
|
|
8
|
+
* was observable anywhere. Attempt in, action out: the component executes the
|
|
9
|
+
* action (timer + callbacks) and can log/report every step; the backoff shape
|
|
10
|
+
* and the exhaustion edge are pinned by tests.
|
|
11
|
+
*/
|
|
12
|
+
export interface ReconnectPolicy {
|
|
13
|
+
readonly baseDelayMs: number;
|
|
14
|
+
readonly maxDelayMs: number;
|
|
15
|
+
readonly maxAttempts: number;
|
|
16
|
+
}
|
|
17
|
+
/** Exponential backoff, ±20% jitter, bounded attempts. The bound exists so a
|
|
18
|
+
* permanently-dead stream stops consuming signaling; the EXHAUSTED action is
|
|
19
|
+
* the caller's cue to surface a hard error (never to go silent). */
|
|
20
|
+
export declare const RECONNECT_POLICY: ReconnectPolicy;
|
|
21
|
+
export type ReconnectAction = {
|
|
22
|
+
readonly kind: 'retry';
|
|
23
|
+
readonly delayMs: number;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'exhausted';
|
|
26
|
+
readonly attempts: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Decide what attempt number `attempt` (0-based) should do. `random` is the
|
|
30
|
+
* jitter source (unit interval), injectable for tests.
|
|
31
|
+
*/
|
|
32
|
+
export declare function nextReconnectAction(attempt: number, random?: () => number): ReconnectAction;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Which elements the row emits.
|
|
4
|
+
*
|
|
5
|
+
* - `plain` (default) — `div` / `span`; valid anywhere.
|
|
6
|
+
* - `description` — `dt` / `dd` inside a wrapper `div`, so the row is a
|
|
7
|
+
* valid child of a `<dl>` (`KeyValueList`).
|
|
8
|
+
*/
|
|
9
|
+
export type SettingRowElements = 'plain' | 'description';
|
|
10
|
+
export interface SettingRowProps {
|
|
11
|
+
/** Left-hand label. Free to wrap — it is never given a fixed width at L1. */
|
|
12
|
+
readonly label: ReactNode;
|
|
13
|
+
/** The value. Wraps at L1, truncates from the stack breakpoint up. */
|
|
14
|
+
readonly children: ReactNode;
|
|
15
|
+
/** Trailing affordances (copy / reveal), kept beside the value. */
|
|
16
|
+
readonly actions?: ReactNode;
|
|
17
|
+
/** Element flavour — see `SettingRowElements`. Default `plain`. */
|
|
18
|
+
readonly elements?: SettingRowElements;
|
|
19
|
+
/** Extra classes on the outer row. */
|
|
20
|
+
readonly className?: string;
|
|
21
|
+
/** Extra classes on the value text (e.g. `font-mono`). */
|
|
22
|
+
readonly valueClassName?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function SettingRow({ label, children, actions, elements, className, valueClassName, }: SettingRowProps): ReactNode;
|
|
@@ -1399,6 +1399,10 @@ export declare const useSettingsStoreInsert: typeof trpc.settingsStore.insert.us
|
|
|
1399
1399
|
export declare const useSettingsStoreUpdate: typeof trpc.settingsStore.update.useMutation;
|
|
1400
1400
|
/** Generated alias around `trpc.settingsStore.delete.useMutation`. */
|
|
1401
1401
|
export declare const useSettingsStoreDelete: typeof trpc.settingsStore.delete.useMutation;
|
|
1402
|
+
/** Generated alias around `trpc.settingsStore.deleteWhere.useMutation`. */
|
|
1403
|
+
export declare const useSettingsStoreDeleteWhere: typeof trpc.settingsStore.deleteWhere.useMutation;
|
|
1404
|
+
/** Generated alias around `trpc.settingsStore.updateWhere.useMutation`. */
|
|
1405
|
+
export declare const useSettingsStoreUpdateWhere: typeof trpc.settingsStore.updateWhere.useMutation;
|
|
1402
1406
|
/** Generated alias around `trpc.settingsStore.count.useQuery`. */
|
|
1403
1407
|
export declare const useSettingsStoreCount: typeof trpc.settingsStore.count.useQuery;
|
|
1404
1408
|
/** Generated alias around `trpc.settingsStore.histogram.useQuery`. */
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TurnServerCache — shared STUN/TURN credential cache for the WebRTC connect
|
|
3
|
+
* path.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* The ICE-server fetch is serialized BEFORE `createOffer` on every connect
|
|
8
|
+
* (`camera-stream-player.tsx` → `getIceServers()`), so its latency lands 1:1
|
|
9
|
+
* in the time-to-first-frame. Production `embed:webrtc-timing` lines measured
|
|
10
|
+
* it at 0.2–3.6 s on a cold page. Two verified defects drove that:
|
|
11
|
+
*
|
|
12
|
+
* 1. **No in-flight coalescing.** The mount-time prefetch and the connect's
|
|
13
|
+
* own call raced: whichever started second issued a SECOND full
|
|
14
|
+
* `getTurnServers` round trip instead of awaiting the first.
|
|
15
|
+
* 2. **Per-page cache.** The cache was a `WeakMap` keyed on the page's tRPC
|
|
16
|
+
* client. The native app loads a fresh embed page per camera open, so the
|
|
17
|
+
* common remote path started cold every single time.
|
|
18
|
+
*
|
|
19
|
+
* ## Design
|
|
20
|
+
*
|
|
21
|
+
* Three layers, checked in order:
|
|
22
|
+
* - **memory** (`WeakMap` keyed on the caller's tRPC client — unchanged
|
|
23
|
+
* semantics: two admin tabs pointed at different hubs never cross-serve),
|
|
24
|
+
* - **in-flight** (concurrent callers share one promise),
|
|
25
|
+
* - **storage** (`localStorage` when available) so a fresh page finds warm
|
|
26
|
+
* credentials. Storage is per-origin, and the page's origin IS the hub, so
|
|
27
|
+
* cross-hub leakage is structurally impossible.
|
|
28
|
+
*
|
|
29
|
+
* Freshness: entries younger than `freshTtlMs` are served as-is. Entries older
|
|
30
|
+
* than that but younger than `staleCapMs` are served IMMEDIATELY while a
|
|
31
|
+
* background refresh runs (credentials from the hub's providers are valid for
|
|
32
|
+
* hours — e.g. 24 h for the managed-relay provider — so a bounded stale window
|
|
33
|
+
* is safe; an actually-expired relay credential degrades to the public-STUN
|
|
34
|
+
* fallback exactly like an absent one, and the refresh heals the next
|
|
35
|
+
* connect). Beyond `staleCapMs` the fetch is awaited.
|
|
36
|
+
*
|
|
37
|
+
* Failure: a failed fetch serves the stale entry when one exists, else
|
|
38
|
+
* `undefined` (caller falls back to public STUN). Failures are never cached.
|
|
39
|
+
*/
|
|
40
|
+
/** Narrow storage contract — satisfied by `localStorage`, injectable in tests. */
|
|
41
|
+
export interface TurnCacheStorage {
|
|
42
|
+
getItem(key: string): string | null;
|
|
43
|
+
setItem(key: string, value: string): void;
|
|
44
|
+
}
|
|
45
|
+
export interface TurnServerCacheOptions {
|
|
46
|
+
/** Entries younger than this are served without any fetch. */
|
|
47
|
+
readonly freshTtlMs?: number;
|
|
48
|
+
/** Entries older than `freshTtlMs` but younger than this are served
|
|
49
|
+
* immediately while a background refresh runs. */
|
|
50
|
+
readonly staleCapMs?: number;
|
|
51
|
+
/** Storage layer. `null` disables persistence (memory-only). When omitted,
|
|
52
|
+
* `globalThis.localStorage` is used if present. */
|
|
53
|
+
readonly storage?: TurnCacheStorage | null;
|
|
54
|
+
readonly now?: () => number;
|
|
55
|
+
readonly storageKey?: string;
|
|
56
|
+
}
|
|
57
|
+
export declare class TurnServerCache {
|
|
58
|
+
private readonly freshTtlMs;
|
|
59
|
+
private readonly staleCapMs;
|
|
60
|
+
private readonly storage;
|
|
61
|
+
private readonly now;
|
|
62
|
+
private readonly storageKey;
|
|
63
|
+
/** Keyed on the caller's tRPC client object (stable per connected system). */
|
|
64
|
+
private readonly memory;
|
|
65
|
+
/** In-flight fetch per key — concurrent callers share one round trip. */
|
|
66
|
+
private readonly inflight;
|
|
67
|
+
constructor(options?: TurnServerCacheOptions);
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the ICE servers for `key`, fetching via `fetch` only when no
|
|
70
|
+
* fresh-enough entry exists. Never rejects: a failed fetch resolves to the
|
|
71
|
+
* stale entry when one exists, else `undefined`.
|
|
72
|
+
*/
|
|
73
|
+
getOrFetch(key: object, fetch: () => Promise<readonly RTCIceServer[]>): Promise<readonly RTCIceServer[] | undefined>;
|
|
74
|
+
private startFetch;
|
|
75
|
+
private readStorage;
|
|
76
|
+
private writeStorage;
|
|
77
|
+
}
|