Execute oxfmt (#1166)
Ran pnpm oxfmt . This should get the web repo aligned so that we can better enforce oxfmt going forward.
This commit is contained in:
@@ -21,7 +21,10 @@ export function stopHeartbeat(id: ConnectionId): void {
|
||||
* Fast-cadence heartbeat used while the device is in `configuring`. Replaced
|
||||
* by the maintenance heartbeat once the device fires onConfigComplete.
|
||||
*/
|
||||
export function startConfigHeartbeat(id: ConnectionId, meshDevice: MeshDevice): void {
|
||||
export function startConfigHeartbeat(
|
||||
id: ConnectionId,
|
||||
meshDevice: MeshDevice,
|
||||
): void {
|
||||
stopHeartbeat(id);
|
||||
const intervalId = setInterval(() => {
|
||||
meshDevice.heartbeat().catch((error) => {
|
||||
@@ -34,7 +37,10 @@ export function startConfigHeartbeat(id: ConnectionId, meshDevice: MeshDevice):
|
||||
/**
|
||||
* Slow-cadence keep-alive used after configuration completes.
|
||||
*/
|
||||
export function startMaintenanceHeartbeat(id: ConnectionId, meshDevice: MeshDevice): void {
|
||||
export function startMaintenanceHeartbeat(
|
||||
id: ConnectionId,
|
||||
meshDevice: MeshDevice,
|
||||
): void {
|
||||
stopHeartbeat(id);
|
||||
const intervalId = setInterval(() => {
|
||||
meshDevice.heartbeat().catch((error) => {
|
||||
|
||||
@@ -13,10 +13,19 @@ import type { TransportWebSerial } from "@meshtastic/transport-web-serial";
|
||||
|
||||
const log = createLogger("sdkClient");
|
||||
|
||||
export type AnyTransport = TransportHTTP | TransportWebBluetooth | TransportWebSerial;
|
||||
export type AnyTransport =
|
||||
| TransportHTTP
|
||||
| TransportWebBluetooth
|
||||
| TransportWebSerial;
|
||||
|
||||
const CHAT_RETENTION = { maxPerBucket: 1000, olderThanMs: 1000 * 60 * 60 * 24 * 90 } as const;
|
||||
const TELEMETRY_RETENTION = { maxPerNode: 500, olderThanMs: 1000 * 60 * 60 * 24 * 30 } as const;
|
||||
const CHAT_RETENTION = {
|
||||
maxPerBucket: 1000,
|
||||
olderThanMs: 1000 * 60 * 60 * 24 * 90,
|
||||
} as const;
|
||||
const TELEMETRY_RETENTION = {
|
||||
maxPerNode: 500,
|
||||
olderThanMs: 1000 * 60 * 60 * 24 * 30,
|
||||
} as const;
|
||||
const STORAGE_OPEN_TIMEOUT_MS = 5000;
|
||||
|
||||
/**
|
||||
@@ -44,24 +53,38 @@ export async function buildMeshDevice(
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(`sqlocal DB open did not resolve within ${STORAGE_OPEN_TIMEOUT_MS}ms`),
|
||||
new Error(
|
||||
`sqlocal DB open did not resolve within ${STORAGE_OPEN_TIMEOUT_MS}ms`,
|
||||
),
|
||||
),
|
||||
STORAGE_OPEN_TIMEOUT_MS,
|
||||
),
|
||||
),
|
||||
]);
|
||||
log.debug("buildMeshDevice: storage DB ready", { ms: Date.now() - t0 });
|
||||
chatRepository = new SqlocalMessageRepository(db, { deviceId: connectionId, coordinator });
|
||||
draftRepository = new SqlocalDraftRepository(db, { deviceId: connectionId });
|
||||
nodesRepository = new SqlocalNodesRepository(db, { deviceId: connectionId });
|
||||
telemetryRepository = new SqlocalTelemetryRepository(db, { deviceId: connectionId });
|
||||
chatRepository = new SqlocalMessageRepository(db, {
|
||||
deviceId: connectionId,
|
||||
coordinator,
|
||||
});
|
||||
draftRepository = new SqlocalDraftRepository(db, {
|
||||
deviceId: connectionId,
|
||||
});
|
||||
nodesRepository = new SqlocalNodesRepository(db, {
|
||||
deviceId: connectionId,
|
||||
});
|
||||
telemetryRepository = new SqlocalTelemetryRepository(db, {
|
||||
deviceId: connectionId,
|
||||
});
|
||||
log.debug("buildMeshDevice: repositories opened");
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
log.warn("buildMeshDevice: sqlocal unavailable, falling back to in-memory", {
|
||||
name: e?.name,
|
||||
message: e?.message,
|
||||
});
|
||||
log.warn(
|
||||
"buildMeshDevice: sqlocal unavailable, falling back to in-memory",
|
||||
{
|
||||
name: e?.name,
|
||||
message: e?.message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return new MeshDevice(transport, {
|
||||
|
||||
@@ -2,8 +2,14 @@ import type { Connection } from "@core/stores/deviceStore/types";
|
||||
import { testHttpReachable } from "@pages/Connections/utils";
|
||||
import { createLogger } from "@meshtastic/sdk";
|
||||
import { TransportHTTP } from "@meshtastic/transport-http";
|
||||
import { BluetoothConnectError, TransportWebBluetooth } from "@meshtastic/transport-web-bluetooth";
|
||||
import { SerialConnectError, TransportWebSerial } from "@meshtastic/transport-web-serial";
|
||||
import {
|
||||
BluetoothConnectError,
|
||||
TransportWebBluetooth,
|
||||
} from "@meshtastic/transport-web-bluetooth";
|
||||
import {
|
||||
SerialConnectError,
|
||||
TransportWebSerial,
|
||||
} from "@meshtastic/transport-web-serial";
|
||||
import { Result } from "better-result";
|
||||
import type { AnyTransport } from "./sdkClient.ts";
|
||||
|
||||
@@ -46,7 +52,9 @@ export async function openTransport(
|
||||
default: {
|
||||
const _exhaustive: never = conn;
|
||||
void _exhaustive;
|
||||
throw new Error(`Unknown transport type: ${(conn as { type?: string }).type}`);
|
||||
throw new Error(
|
||||
`Unknown transport type: ${(conn as { type?: string }).type}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,8 +105,12 @@ async function openBluetooth(
|
||||
if (!device && opts.allowPrompt) {
|
||||
device = await navigator.bluetooth.requestDevice({
|
||||
acceptAllDevices: !conn.gattServiceUUID,
|
||||
optionalServices: conn.gattServiceUUID ? [conn.gattServiceUUID] : undefined,
|
||||
filters: conn.gattServiceUUID ? [{ services: [conn.gattServiceUUID] }] : undefined,
|
||||
optionalServices: conn.gattServiceUUID
|
||||
? [conn.gattServiceUUID]
|
||||
: undefined,
|
||||
filters: conn.gattServiceUUID
|
||||
? [{ services: [conn.gattServiceUUID] }]
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
if (!device) {
|
||||
@@ -155,7 +167,10 @@ async function openSerial(
|
||||
getInfo?: () => { usbVendorId?: number; usbProductId?: number };
|
||||
}
|
||||
).getInfo?.() ?? {};
|
||||
return info.usbVendorId === conn.usbVendorId && info.usbProductId === conn.usbProductId;
|
||||
return (
|
||||
info.usbVendorId === conn.usbVendorId &&
|
||||
info.usbProductId === conn.usbProductId
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -208,7 +223,9 @@ export async function probeConnection(
|
||||
getDevices?: () => Promise<BluetoothDevice[]>;
|
||||
}
|
||||
).getDevices?.();
|
||||
const hasPermission = known?.some((d: BluetoothDevice) => d.id === conn.deviceId);
|
||||
const hasPermission = known?.some(
|
||||
(d: BluetoothDevice) => d.id === conn.deviceId,
|
||||
);
|
||||
// Permission granted ≠ device configured. The card surfaces "online"
|
||||
// (i.e. "available, click to connect") so the user explicitly opts
|
||||
// into the configure handshake. "configured" is reserved for when
|
||||
@@ -233,7 +250,10 @@ export async function probeConnection(
|
||||
getInfo?: () => { usbVendorId?: number; usbProductId?: number };
|
||||
}
|
||||
).getInfo?.() ?? {};
|
||||
return info.usbVendorId === conn.usbVendorId && info.usbProductId === conn.usbProductId;
|
||||
return (
|
||||
info.usbVendorId === conn.usbVendorId &&
|
||||
info.usbProductId === conn.usbProductId
|
||||
);
|
||||
});
|
||||
return hasPermission ? "online" : "disconnected";
|
||||
} catch {
|
||||
@@ -246,7 +266,9 @@ export async function probeConnection(
|
||||
/**
|
||||
* Best-effort cleanup for a held-onto BT device or serial port. Safe on either.
|
||||
*/
|
||||
export function closeTransport(handle: BluetoothDevice | SerialPort | undefined): void {
|
||||
export function closeTransport(
|
||||
handle: BluetoothDevice | SerialPort | undefined,
|
||||
): void {
|
||||
if (!handle) return;
|
||||
const bt = handle as BluetoothDevice;
|
||||
if (bt.gatt?.connected) {
|
||||
|
||||
@@ -15,7 +15,9 @@ function createDefaultUser(num: number): Protobuf.Mesh.User {
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureDefaultUser(node: Protobuf.Mesh.NodeInfo): Protobuf.Mesh.NodeInfo {
|
||||
export function ensureDefaultUser(
|
||||
node: Protobuf.Mesh.NodeInfo,
|
||||
): Protobuf.Mesh.NodeInfo {
|
||||
if (!node) {
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ class PacketToMessageDTO {
|
||||
this.to = data.to;
|
||||
this.from = data.from;
|
||||
this.messageId = data.id;
|
||||
this.state = data.from !== nodeNum ? MessageState.Ack : MessageState.Waiting;
|
||||
this.state =
|
||||
data.from !== nodeNum ? MessageState.Ack : MessageState.Waiting;
|
||||
this.message = data.data;
|
||||
this.type = data.type === "direct" ? MessageType.Direct : MessageType.Broadcast;
|
||||
this.type =
|
||||
data.type === "direct" ? MessageType.Direct : MessageType.Broadcast;
|
||||
|
||||
let dateTimestamp = Date.now();
|
||||
if (data.rxTime instanceof Date) {
|
||||
|
||||
@@ -14,7 +14,8 @@ export function useBrowserFeatureDetection(): BrowserSupport {
|
||||
["Web Serial", !!navigator.serial],
|
||||
[
|
||||
"Secure Context",
|
||||
globalThis.location.protocol === "https:" || globalThis.location.hostname === "localhost",
|
||||
globalThis.location.protocol === "https:" ||
|
||||
globalThis.location.hostname === "localhost",
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -30,17 +30,32 @@ export type UseChatAsLegacyMessagesParams =
|
||||
| UseChatAsLegacyMessagesBroadcast
|
||||
| UseChatAsLegacyMessagesDirect;
|
||||
|
||||
export function useChatAsLegacyMessages(params: UseChatAsLegacyMessagesParams): LegacyMessage[] {
|
||||
export function useChatAsLegacyMessages(
|
||||
params: UseChatAsLegacyMessagesParams,
|
||||
): LegacyMessage[] {
|
||||
const broadcast = useChat(
|
||||
params.type === MessageType.Broadcast ? params.channelId : (0 as Types.ChannelNumber),
|
||||
params.type === MessageType.Broadcast
|
||||
? params.channelId
|
||||
: (0 as Types.ChannelNumber),
|
||||
);
|
||||
const direct = useDirectChat(params.type === MessageType.Direct ? params.peer : 0);
|
||||
const sdkMessages = params.type === MessageType.Broadcast ? broadcast.messages : direct.messages;
|
||||
const direct = useDirectChat(
|
||||
params.type === MessageType.Direct ? params.peer : 0,
|
||||
);
|
||||
const sdkMessages =
|
||||
params.type === MessageType.Broadcast
|
||||
? broadcast.messages
|
||||
: direct.messages;
|
||||
|
||||
return useMemo(() => sdkMessages.map((m) => toLegacy(m, params)), [sdkMessages, params]);
|
||||
return useMemo(
|
||||
() => sdkMessages.map((m) => toLegacy(m, params)),
|
||||
[sdkMessages, params],
|
||||
);
|
||||
}
|
||||
|
||||
function toLegacy(message: SdkMessage, params: UseChatAsLegacyMessagesParams): LegacyMessage {
|
||||
function toLegacy(
|
||||
message: SdkMessage,
|
||||
params: UseChatAsLegacyMessagesParams,
|
||||
): LegacyMessage {
|
||||
return {
|
||||
type: params.type,
|
||||
channel: message.channel,
|
||||
|
||||
@@ -30,17 +30,32 @@ export type UseChatAsLegacyMessagesParams =
|
||||
| UseChatAsLegacyMessagesBroadcast
|
||||
| UseChatAsLegacyMessagesDirect;
|
||||
|
||||
export function useChatAsLegacyMessages(params: UseChatAsLegacyMessagesParams): LegacyMessage[] {
|
||||
export function useChatAsLegacyMessages(
|
||||
params: UseChatAsLegacyMessagesParams,
|
||||
): LegacyMessage[] {
|
||||
const broadcast = useChat(
|
||||
params.type === MessageType.Broadcast ? params.channelId : (0 as Types.ChannelNumber),
|
||||
params.type === MessageType.Broadcast
|
||||
? params.channelId
|
||||
: (0 as Types.ChannelNumber),
|
||||
);
|
||||
const direct = useDirectChat(params.type === MessageType.Direct ? params.peer : 0);
|
||||
const sdkMessages = params.type === MessageType.Broadcast ? broadcast.messages : direct.messages;
|
||||
const direct = useDirectChat(
|
||||
params.type === MessageType.Direct ? params.peer : 0,
|
||||
);
|
||||
const sdkMessages =
|
||||
params.type === MessageType.Broadcast
|
||||
? broadcast.messages
|
||||
: direct.messages;
|
||||
|
||||
return useMemo(() => sdkMessages.map((m) => toLegacy(m, params)), [sdkMessages, params]);
|
||||
return useMemo(
|
||||
() => sdkMessages.map((m) => toLegacy(m, params)),
|
||||
[sdkMessages, params],
|
||||
);
|
||||
}
|
||||
|
||||
function toLegacy(message: SdkMessage, params: UseChatAsLegacyMessagesParams): LegacyMessage {
|
||||
function toLegacy(
|
||||
message: SdkMessage,
|
||||
params: UseChatAsLegacyMessagesParams,
|
||||
): LegacyMessage {
|
||||
return {
|
||||
type: params.type,
|
||||
channel: message.channel,
|
||||
|
||||
@@ -7,7 +7,10 @@ interface CookieHookResult<T> {
|
||||
removeCookie: () => void;
|
||||
}
|
||||
|
||||
function useCookie<T extends object>(cookieName: string, initialValue?: T): CookieHookResult<T> {
|
||||
function useCookie<T extends object>(
|
||||
cookieName: string,
|
||||
initialValue?: T,
|
||||
): CookieHookResult<T> {
|
||||
const [cookieValue, setCookieValue] = useState<T | undefined>(() => {
|
||||
try {
|
||||
const cookie = Cookies.get(cookieName);
|
||||
|
||||
@@ -4,7 +4,9 @@ interface UseCopyToClipboardProps {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export function useCopyToClipboard({ timeout = 2000 }: UseCopyToClipboardProps = {}) {
|
||||
export function useCopyToClipboard({
|
||||
timeout = 2000,
|
||||
}: UseCopyToClipboardProps = {}) {
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@ export type DeviceContext = {
|
||||
deviceId: number; // Unique identifier for the device, not nodeNum
|
||||
};
|
||||
|
||||
export const CurrentDeviceContext = createContext<DeviceContext | undefined>(undefined);
|
||||
export const CurrentDeviceContext = createContext<DeviceContext | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useDeviceContext(): DeviceContext {
|
||||
const ctx = useContext(CurrentDeviceContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useDeviceContext must be used within CurrentDeviceContext provider");
|
||||
throw new Error(
|
||||
"useDeviceContext must be used within CurrentDeviceContext provider",
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ export function useFavoriteNode() {
|
||||
const node = meshClient.nodes.byNum(nodeNum);
|
||||
if (!node) return;
|
||||
|
||||
void (isFavorite ? meshClient.nodes.favorite(nodeNum) : meshClient.nodes.unfavorite(nodeNum));
|
||||
void (isFavorite
|
||||
? meshClient.nodes.favorite(nodeNum)
|
||||
: meshClient.nodes.unfavorite(nodeNum));
|
||||
|
||||
toast({
|
||||
title: t("toast.favoriteNode.title", {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { type FlagKey, type Flags, featureFlags } from "@core/services/featureFlags.ts";
|
||||
import {
|
||||
type FlagKey,
|
||||
type Flags,
|
||||
featureFlags,
|
||||
} from "@core/services/featureFlags.ts";
|
||||
import * as React from "react";
|
||||
|
||||
export function useFeatureFlags(): Flags {
|
||||
|
||||
@@ -23,7 +23,9 @@ export function useIgnoreNode() {
|
||||
const node = meshClient.nodes.byNum(nodeNum);
|
||||
if (!node) return;
|
||||
|
||||
void (isIgnored ? meshClient.nodes.ignore(nodeNum) : meshClient.nodes.unignore(nodeNum));
|
||||
void (isIgnored
|
||||
? meshClient.nodes.ignore(nodeNum)
|
||||
: meshClient.nodes.unignore(nodeNum));
|
||||
|
||||
toast({
|
||||
title: t("toast.ignoreNode.title", {
|
||||
|
||||
@@ -45,10 +45,8 @@ export function useBackupReminder({
|
||||
|
||||
const { toast } = useToast();
|
||||
const toastShownRef = useRef(false);
|
||||
const [reminderState, setReminderState] = useLocalStorage<ReminderState | null>(
|
||||
STORAGE_KEY,
|
||||
null,
|
||||
);
|
||||
const [reminderState, setReminderState] =
|
||||
useLocalStorage<ReminderState | null>(STORAGE_KEY, null);
|
||||
|
||||
const setReminderExpiry = useCallback(
|
||||
(days: number) => {
|
||||
|
||||
@@ -50,7 +50,9 @@ export type UseLRUListReturn<T> = {
|
||||
*
|
||||
* MRU is index 0. Adding an existing item "touches" it (moves to front).
|
||||
*/
|
||||
export function useLRUList<T, J = unknown>(opts: UseLRUListOptions<T, J>): UseLRUListReturn<T> {
|
||||
export function useLRUList<T, J = unknown>(
|
||||
opts: UseLRUListOptions<T, J>,
|
||||
): UseLRUListReturn<T> {
|
||||
const {
|
||||
key,
|
||||
capacity,
|
||||
@@ -152,7 +154,10 @@ export function useLRUList<T, J = unknown>(opts: UseLRUListOptions<T, J>): UseLR
|
||||
}, [syncTabs, canPersist, storage, key, readPersisted]);
|
||||
|
||||
// Helpers
|
||||
const indexOf = useCallback((arr: T[], needle: T) => arr.findIndex((x) => eq(x, needle)), [eq]);
|
||||
const indexOf = useCallback(
|
||||
(arr: T[], needle: T) => arr.findIndex((x) => eq(x, needle)),
|
||||
[eq],
|
||||
);
|
||||
|
||||
const add = useCallback(
|
||||
(item: T) => {
|
||||
@@ -192,7 +197,10 @@ export function useLRUList<T, J = unknown>(opts: UseLRUListOptions<T, J>): UseLR
|
||||
|
||||
const clear = useCallback(() => setItems([]), []);
|
||||
|
||||
const has = useCallback((item: T) => indexOf(items, item) !== -1, [items, indexOf]);
|
||||
const has = useCallback(
|
||||
(item: T) => indexOf(items, item) !== -1,
|
||||
[items, indexOf],
|
||||
);
|
||||
|
||||
const replaceAll = useCallback(
|
||||
(next: T[]) => setItems(trimToCapacity([...next], effectiveCapacity)),
|
||||
|
||||
@@ -16,7 +16,10 @@ type LanguageState = {
|
||||
|
||||
function useLang() {
|
||||
const { i18n } = useTranslation();
|
||||
const [_, setLanguageInStorage] = useLocalStorage<LanguageState | null>(STORAGE_KEY, null);
|
||||
const [_, setLanguageInStorage] = useLocalStorage<LanguageState | null>(
|
||||
STORAGE_KEY,
|
||||
null,
|
||||
);
|
||||
|
||||
const currentLanguage = useMemo((): Lang | undefined => {
|
||||
const lang = supportedLanguages.find((l) => l.code === i18n.language);
|
||||
|
||||
@@ -26,7 +26,9 @@ export default function useLocalStorage<T>(
|
||||
const deserializerRef = useRef(options.deserializer ?? JSON.parse);
|
||||
|
||||
const initialValueRef = useRef<T>(
|
||||
typeof initialValue === "function" ? (initialValue as () => T)() : initialValue,
|
||||
typeof initialValue === "function"
|
||||
? (initialValue as () => T)()
|
||||
: initialValue,
|
||||
);
|
||||
|
||||
const getInitialValue = useCallback(() => initialValueRef.current, []);
|
||||
@@ -48,15 +50,22 @@ export default function useLocalStorage<T>(
|
||||
const setValue: Dispatch<SetStateAction<T>> = useCallback(
|
||||
(value) => {
|
||||
if (IS_SERVER) {
|
||||
console.warn(`Tried setting localStorage key “${key}” in a non-client environment.`);
|
||||
console.warn(
|
||||
`Tried setting localStorage key “${key}” in a non-client environment.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setStoredValue((prev) => {
|
||||
const newValue = typeof value === "function" ? (value as (prev: T) => T)(prev) : value;
|
||||
const newValue =
|
||||
typeof value === "function"
|
||||
? (value as (prev: T) => T)(prev)
|
||||
: value;
|
||||
window.localStorage.setItem(key, serializerRef.current(newValue));
|
||||
window.dispatchEvent(new CustomEvent("local-storage", { detail: { key } }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("local-storage", { detail: { key } }),
|
||||
);
|
||||
return newValue;
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -68,13 +77,17 @@ export default function useLocalStorage<T>(
|
||||
|
||||
const removeValue = useCallback(() => {
|
||||
if (IS_SERVER) {
|
||||
console.warn(`Tried removing localStorage key “${key}” in a non-client environment.`);
|
||||
console.warn(
|
||||
`Tried removing localStorage key “${key}” in a non-client environment.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
setStoredValue(getInitialValue());
|
||||
window.dispatchEvent(new CustomEvent("local-storage", { detail: { key } }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("local-storage", { detail: { key } }),
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Error removing localStorage key “${key}”:`, err);
|
||||
}
|
||||
@@ -85,13 +98,17 @@ export default function useLocalStorage<T>(
|
||||
return;
|
||||
}
|
||||
|
||||
const handleStorageChange = (event: StorageEvent | CustomEvent<{ key: string }>) => {
|
||||
const handleStorageChange = (
|
||||
event: StorageEvent | CustomEvent<{ key: string }>,
|
||||
) => {
|
||||
const eventKey = "key" in event ? event.key : event.detail?.key;
|
||||
|
||||
if (eventKey === key) {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
setStoredValue(item ? deserializerRef.current(item) : getInitialValue());
|
||||
setStoredValue(
|
||||
item ? deserializerRef.current(item) : getInitialValue(),
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Error syncing localStorage key “${key}”:`, err);
|
||||
setStoredValue(getInitialValue());
|
||||
@@ -100,11 +117,17 @@ export default function useLocalStorage<T>(
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
window.addEventListener("local-storage", handleStorageChange as EventListener);
|
||||
window.addEventListener(
|
||||
"local-storage",
|
||||
handleStorageChange as EventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
window.removeEventListener("local-storage", handleStorageChange as EventListener);
|
||||
window.removeEventListener(
|
||||
"local-storage",
|
||||
handleStorageChange as EventListener,
|
||||
);
|
||||
};
|
||||
}, [key, getInitialValue]);
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useDeviceStore } from "@core/stores";
|
||||
import type { Protobuf } from "@meshtastic/sdk";
|
||||
|
||||
export function useNewNodeNum(id: number, nodeInfo: Protobuf.Mesh.MyNodeInfo): void {
|
||||
export function useNewNodeNum(
|
||||
id: number,
|
||||
nodeInfo: Protobuf.Mesh.MyNodeInfo,
|
||||
): void {
|
||||
useDeviceStore.getState().getDevice(id)?.setHardware(nodeInfo);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ export function useNodesAsProto(): Protobuf.Mesh.NodeInfo[] {
|
||||
return useMemo(() => nodes.map(toNodeInfo), [nodes]);
|
||||
}
|
||||
|
||||
export function useNodeAsProto(nodeNum: number): Protobuf.Mesh.NodeInfo | undefined {
|
||||
export function useNodeAsProto(
|
||||
nodeNum: number,
|
||||
): Protobuf.Mesh.NodeInfo | undefined {
|
||||
const nodes = useSdkNodesSafe();
|
||||
return useMemo(() => {
|
||||
const found = nodes.find((n) => n.num === nodeNum);
|
||||
|
||||
@@ -18,7 +18,9 @@ export function useNodesAsProto(): Protobuf.Mesh.NodeInfo[] {
|
||||
return useMemo(() => nodes.map(toNodeInfo), [nodes]);
|
||||
}
|
||||
|
||||
export function useNodeAsProto(nodeNum: number): Protobuf.Mesh.NodeInfo | undefined {
|
||||
export function useNodeAsProto(
|
||||
nodeNum: number,
|
||||
): Protobuf.Mesh.NodeInfo | undefined {
|
||||
const nodes = useNodes();
|
||||
return useMemo(() => {
|
||||
const found = nodes.find((n) => n.num === nodeNum);
|
||||
|
||||
@@ -10,7 +10,9 @@ describe("usePasswordVisibilityToggle Hook", () => {
|
||||
});
|
||||
|
||||
it("should initialize with visibility set to true if initialVisible is true", () => {
|
||||
const { result } = renderHook(() => usePasswordVisibilityToggle({ initialVisible: true }));
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordVisibilityToggle({ initialVisible: true }),
|
||||
);
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
@@ -24,7 +26,9 @@ describe("usePasswordVisibilityToggle Hook", () => {
|
||||
});
|
||||
|
||||
it("should toggle visibility from true to false when toggleVisibility is called", () => {
|
||||
const { result } = renderHook(() => usePasswordVisibilityToggle({ initialVisible: true }));
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordVisibilityToggle({ initialVisible: true }),
|
||||
);
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
act(() => {
|
||||
result.current.toggleVisibility();
|
||||
@@ -50,7 +54,9 @@ describe("usePasswordVisibilityToggle Hook", () => {
|
||||
});
|
||||
|
||||
it("should return a stable toggleVisibility function reference (due to useCallback)", () => {
|
||||
const { result, rerender } = renderHook(() => usePasswordVisibilityToggle());
|
||||
const { result, rerender } = renderHook(() =>
|
||||
usePasswordVisibilityToggle(),
|
||||
);
|
||||
const initialToggleFunc = result.current.toggleVisibility;
|
||||
rerender();
|
||||
expect(result.current.toggleVisibility).toBe(initialToggleFunc);
|
||||
|
||||
@@ -2,12 +2,17 @@ import useLocalStorage from "@core/hooks/useLocalStorage.ts";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function usePinnedItems({ storageName }: { storageName: string }) {
|
||||
const [pinnedItems, setPinnedItems] = useLocalStorage<string[]>(storageName, []);
|
||||
const [pinnedItems, setPinnedItems] = useLocalStorage<string[]>(
|
||||
storageName,
|
||||
[],
|
||||
);
|
||||
|
||||
const togglePinnedItem = useCallback(
|
||||
(label: string) => {
|
||||
setPinnedItems((prev) =>
|
||||
prev.includes(label) ? prev.filter((g) => g !== label) : [...prev, label],
|
||||
prev.includes(label)
|
||||
? prev.filter((g) => g !== label)
|
||||
: [...prev, label],
|
||||
);
|
||||
},
|
||||
[setPinnedItems],
|
||||
|
||||
@@ -67,7 +67,10 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
|
||||
for (const key in FLAGS_CONFIG) {
|
||||
const flagName = key as FlagName;
|
||||
const flagConfig = FLAGS_CONFIG[flagName];
|
||||
if (flagConfig.value !== 0 && (value & flagConfig.value) === flagConfig.value) {
|
||||
if (
|
||||
flagConfig.value !== 0 &&
|
||||
(value & flagConfig.value) === flagConfig.value
|
||||
) {
|
||||
activeFlags.push(flagName);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +90,9 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
|
||||
};
|
||||
|
||||
const hasFlag = (value: number, flagName: FlagName): boolean => {
|
||||
return (value & FLAGS_CONFIG[flagName].value) === FLAGS_CONFIG[flagName].value;
|
||||
return (
|
||||
(value & FLAGS_CONFIG[flagName].value) === FLAGS_CONFIG[flagName].value
|
||||
);
|
||||
};
|
||||
|
||||
const getAllFlags = (): typeof FLAGS_CONFIG => {
|
||||
@@ -116,7 +121,9 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
|
||||
|
||||
const setFlag = useCallback((flagName: FlagName, enabled: boolean) => {
|
||||
const currentFlagValue = FLAGS_CONFIG[flagName].value;
|
||||
setFlagsValue((prev) => (enabled ? prev | currentFlagValue : prev & ~currentFlagValue));
|
||||
setFlagsValue((prev) =>
|
||||
enabled ? prev | currentFlagValue : prev & ~currentFlagValue,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const setFlags = useCallback(
|
||||
|
||||
@@ -4,7 +4,9 @@ type Theme = "light" | "dark" | "system";
|
||||
|
||||
export function useTheme() {
|
||||
const getSystemTheme = () =>
|
||||
globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
globalThis.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
const getStoredPreference = useCallback(
|
||||
(): Theme => (localStorage.getItem("theme") as Theme) || "system",
|
||||
|
||||
@@ -79,7 +79,9 @@ export const reducer = (state: State, action: Action): State => {
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useDevice, type ValidConfigType, type ValidModuleConfigType } from "@core/stores";
|
||||
import {
|
||||
useDevice,
|
||||
type ValidConfigType,
|
||||
type ValidModuleConfigType,
|
||||
} from "@core/stores";
|
||||
|
||||
type UseWaitForConfigProps =
|
||||
| { configCase: ValidConfigType; moduleConfigCase?: never }
|
||||
| { configCase?: never; moduleConfigCase: ValidModuleConfigType };
|
||||
|
||||
export function useWaitForConfig({ configCase, moduleConfigCase }: UseWaitForConfigProps): void {
|
||||
export function useWaitForConfig({
|
||||
configCase,
|
||||
moduleConfigCase,
|
||||
}: UseWaitForConfigProps): void {
|
||||
const { config, moduleConfig } = useDevice();
|
||||
|
||||
const isDataDefined = configCase
|
||||
|
||||
@@ -79,7 +79,10 @@ class FeatureFlags {
|
||||
export function createFeatureFlags(env: Record<string, unknown>): FeatureFlags {
|
||||
const parsed = EnvSchema.parse(env);
|
||||
const base = Object.fromEntries(
|
||||
(Object.keys(FLAG_ENV) as FlagKey[]).map((k) => [k, parsed[FLAG_ENV[k]] as boolean]),
|
||||
(Object.keys(FLAG_ENV) as FlagKey[]).map((k) => [
|
||||
k,
|
||||
parsed[FLAG_ENV[k]] as boolean,
|
||||
]),
|
||||
) as Flags;
|
||||
return new FeatureFlags(base);
|
||||
}
|
||||
|
||||
@@ -76,18 +76,20 @@ describe("AppStore – basic state & actions", () => {
|
||||
const c = makeRaster({ title: "c" });
|
||||
|
||||
state.setRasterSources([a, b]);
|
||||
expect(useAppStore.getState().rasterSources.map((raster) => raster.title)).toEqual(["a", "b"]);
|
||||
expect(
|
||||
useAppStore.getState().rasterSources.map((raster) => raster.title),
|
||||
).toEqual(["a", "b"]);
|
||||
|
||||
state.addRasterSource(c);
|
||||
expect(useAppStore.getState().rasterSources.map((raster) => raster.title)).toEqual([
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
]);
|
||||
expect(
|
||||
useAppStore.getState().rasterSources.map((raster) => raster.title),
|
||||
).toEqual(["a", "b", "c"]);
|
||||
|
||||
// "b"
|
||||
state.removeRasterSource(1);
|
||||
expect(useAppStore.getState().rasterSources.map((raster) => raster.title)).toEqual(["a", "c"]);
|
||||
expect(
|
||||
useAppStore.getState().rasterSources.map((raster) => raster.title),
|
||||
).toEqual(["a", "c"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,7 +105,10 @@ describe("AppStore – persistence: partialize + rehydrate", () => {
|
||||
const { useAppStore } = await freshStore(true);
|
||||
const state = useAppStore.getState();
|
||||
|
||||
state.setRasterSources([makeRaster({ title: "x" }), makeRaster({ title: "y" })]);
|
||||
state.setRasterSources([
|
||||
makeRaster({ title: "x" }),
|
||||
makeRaster({ title: "y" }),
|
||||
]);
|
||||
state.setSelectedDevice(99);
|
||||
state.setCommandPaletteOpen(true);
|
||||
// Only rasterSources should persist by partialize
|
||||
@@ -116,7 +121,10 @@ describe("AppStore – persistence: partialize + rehydrate", () => {
|
||||
const state = useAppStore.getState();
|
||||
|
||||
// persisted slice:
|
||||
expect(state.rasterSources.map((raster) => raster.title)).toEqual(["x", "y"]);
|
||||
expect(state.rasterSources.map((raster) => raster.title)).toEqual([
|
||||
"x",
|
||||
"y",
|
||||
]);
|
||||
|
||||
// ephemeral fields reset to defaults:
|
||||
expect(state.selectedDeviceId).toBe(0);
|
||||
@@ -127,16 +135,13 @@ describe("AppStore – persistence: partialize + rehydrate", () => {
|
||||
|
||||
// methods still work post-rehydrate:
|
||||
state.addRasterSource(makeRaster({ title: "z" }));
|
||||
expect(useAppStore.getState().rasterSources.map((raster) => raster.title)).toEqual([
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
]);
|
||||
expect(
|
||||
useAppStore.getState().rasterSources.map((raster) => raster.title),
|
||||
).toEqual(["x", "y", "z"]);
|
||||
state.removeRasterSource(0);
|
||||
expect(useAppStore.getState().rasterSources.map((raster) => raster.title)).toEqual([
|
||||
"y",
|
||||
"z",
|
||||
]);
|
||||
expect(
|
||||
useAppStore.getState().rasterSources.map((raster) => raster.title),
|
||||
).toEqual(["y", "z"]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -144,14 +149,21 @@ describe("AppStore – persistence: partialize + rehydrate", () => {
|
||||
{
|
||||
const { useAppStore } = await freshStore(true);
|
||||
const state = useAppStore.getState();
|
||||
state.setRasterSources([makeRaster({ title: "keep" }), makeRaster({ title: "drop" })]);
|
||||
state.setRasterSources([
|
||||
makeRaster({ title: "keep" }),
|
||||
makeRaster({ title: "drop" }),
|
||||
]);
|
||||
state.removeRasterSource(1); // drop "drop"
|
||||
expect(useAppStore.getState().rasterSources.map((raster) => raster.title)).toEqual(["keep"]);
|
||||
expect(
|
||||
useAppStore.getState().rasterSources.map((raster) => raster.title),
|
||||
).toEqual(["keep"]);
|
||||
}
|
||||
{
|
||||
const { useAppStore } = await freshStore(true);
|
||||
const state = useAppStore.getState();
|
||||
expect(state.rasterSources.map((raster) => raster.title)).toEqual(["keep"]);
|
||||
expect(state.rasterSources.map((raster) => raster.title)).toEqual([
|
||||
"keep",
|
||||
]);
|
||||
|
||||
// Now replace entirely
|
||||
state.setRasterSources([]);
|
||||
|
||||
@@ -2,7 +2,11 @@ import { featureFlags } from "@core/services/featureFlags.ts";
|
||||
import { createStorage } from "@core/stores/utils/indexDB.ts";
|
||||
import { produce } from "immer";
|
||||
import { create as createStore, type StateCreator } from "zustand";
|
||||
import { type PersistOptions, persist, subscribeWithSelector } from "zustand/middleware";
|
||||
import {
|
||||
type PersistOptions,
|
||||
persist,
|
||||
subscribeWithSelector,
|
||||
} from "zustand/middleware";
|
||||
import type { RasterSource } from "./types.ts";
|
||||
|
||||
const IDB_KEY_NAME = "meshtastic-app-store";
|
||||
@@ -105,8 +109,12 @@ const persistOptions: PersistOptions<AppState, AppData> = {
|
||||
|
||||
// Add persist middleware on the store if the feature flag is enabled
|
||||
const persistApps = featureFlags.get("persistApp");
|
||||
console.debug(`AppStore: Persisting app is ${persistApps ? "enabled" : "disabled"}`);
|
||||
console.debug(
|
||||
`AppStore: Persisting app is ${persistApps ? "enabled" : "disabled"}`,
|
||||
);
|
||||
|
||||
export const useAppStore = persistApps
|
||||
? createStore(subscribeWithSelector(persist(deviceStoreInitializer, persistOptions)))
|
||||
? createStore(
|
||||
subscribeWithSelector(persist(deviceStoreInitializer, persistOptions)),
|
||||
)
|
||||
: createStore(subscribeWithSelector(deviceStoreInitializer));
|
||||
|
||||
@@ -108,7 +108,9 @@ describe("DeviceStore – metadata, dialogs, unread counts, message draft", () =
|
||||
});
|
||||
device.addMetadata(123, metadata);
|
||||
|
||||
expect(useDeviceStore.getState().devices.get(1)?.metadata.get(123)).toEqual(metadata);
|
||||
expect(useDeviceStore.getState().devices.get(1)?.metadata.get(123)).toEqual(
|
||||
metadata,
|
||||
);
|
||||
});
|
||||
|
||||
it("dialogs set/get work and throw if device missing", async () => {
|
||||
@@ -131,7 +133,9 @@ describe("DeviceStore – metadata, dialogs, unread counts, message draft", () =
|
||||
const device = useDeviceStore.getState().addDevice(3);
|
||||
device.setMessageDraft("hello");
|
||||
|
||||
expect(useDeviceStore.getState().devices.get(3)?.messageDraft).toBe("hello");
|
||||
expect(useDeviceStore.getState().devices.get(3)?.messageDraft).toBe(
|
||||
"hello",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,7 +155,10 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa
|
||||
device.addTraceRoute(makeRoute(7, i));
|
||||
}
|
||||
|
||||
const routesFor7 = useDeviceStore.getState().devices.get(100)?.traceroutes.get(7)!;
|
||||
const routesFor7 = useDeviceStore
|
||||
.getState()
|
||||
.devices.get(100)
|
||||
?.traceroutes.get(7)!;
|
||||
expect(routesFor7.length).toBe(100);
|
||||
expect(routesFor7[0]?.rxTime).toBe(1); // first (0) evicted
|
||||
|
||||
@@ -160,7 +167,9 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa
|
||||
device.addTraceRoute(makeRoute(1000 + from));
|
||||
}
|
||||
|
||||
const keys = Array.from(useDeviceStore.getState().devices.get(100)!.traceroutes.keys());
|
||||
const keys = Array.from(
|
||||
useDeviceStore.getState().devices.get(100)!.traceroutes.keys(),
|
||||
);
|
||||
expect(keys.length).toBe(100);
|
||||
});
|
||||
|
||||
@@ -182,23 +191,37 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa
|
||||
new Date(),
|
||||
); // expired
|
||||
oldDevice.addWaypoint(makeWaypoint(2, 0), 0, 0, new Date()); // no expire
|
||||
oldDevice.addWaypoint(makeWaypoint(3, Date.parse("2026-01-01T00:00:00Z")), 0, 0, new Date()); // ok
|
||||
oldDevice.addWaypoint(
|
||||
makeWaypoint(3, Date.parse("2026-01-01T00:00:00Z")),
|
||||
0,
|
||||
0,
|
||||
new Date(),
|
||||
); // ok
|
||||
oldDevice.addTraceRoute(makeRoute(55));
|
||||
oldDevice.addTraceRoute(makeRoute(56));
|
||||
|
||||
// Upsert waypoint by id
|
||||
oldDevice.addWaypoint(makeWaypoint(2, Date.parse("2027-01-01T00:00:00Z")), 0, 0, new Date());
|
||||
oldDevice.addWaypoint(
|
||||
makeWaypoint(2, Date.parse("2027-01-01T00:00:00Z")),
|
||||
0,
|
||||
0,
|
||||
new Date(),
|
||||
);
|
||||
|
||||
const wps = useDeviceStore.getState().devices.get(1)!.waypoints;
|
||||
expect(wps.length).toBe(2);
|
||||
expect(wps.find((w) => w.id === 2)?.expire).toBe(Date.parse("2027-01-01T00:00:00Z"));
|
||||
expect(wps.find((w) => w.id === 2)?.expire).toBe(
|
||||
Date.parse("2027-01-01T00:00:00Z"),
|
||||
);
|
||||
|
||||
// Retention: push 102 total waypoints -> capped at 100. Oldest evicted
|
||||
for (let i = 3; i <= 102; i++) {
|
||||
oldDevice.addWaypoint(makeWaypoint(i), 0, 0, new Date());
|
||||
}
|
||||
|
||||
expect(useDeviceStore.getState().devices.get(1)!.waypoints.length).toBe(100);
|
||||
expect(useDeviceStore.getState().devices.get(1)!.waypoints.length).toBe(
|
||||
100,
|
||||
);
|
||||
|
||||
// Remove waypoint
|
||||
oldDevice.removeWaypoint(102, false);
|
||||
@@ -265,10 +288,14 @@ describe("DeviceStore – persistence partialize & rehydrate", () => {
|
||||
|
||||
// methods should work
|
||||
device.addWaypoint(makeWaypoint(2), 0, 0, new Date());
|
||||
expect(useDeviceStore.getState().devices.get(501)!.waypoints.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
useDeviceStore.getState().devices.get(501)!.waypoints.length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// traceroutes survived
|
||||
expect(useDeviceStore.getState().devices.get(501)!.traceroutes.size).toBeGreaterThan(0);
|
||||
expect(
|
||||
useDeviceStore.getState().devices.get(501)!.traceroutes.size,
|
||||
).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import { createStorage } from "@core/stores/utils/indexDB.ts";
|
||||
import { type MeshDevice, Protobuf, Types } from "@meshtastic/sdk";
|
||||
import { type Draft, produce } from "immer";
|
||||
import { create as createStore, type StateCreator } from "zustand";
|
||||
import { type PersistOptions, persist, subscribeWithSelector } from "zustand/middleware";
|
||||
import {
|
||||
type PersistOptions,
|
||||
persist,
|
||||
subscribeWithSelector,
|
||||
} from "zustand/middleware";
|
||||
import type {
|
||||
Connection,
|
||||
ConnectionId,
|
||||
@@ -26,11 +30,18 @@ type DeviceData = {
|
||||
// Persisted data
|
||||
id: number;
|
||||
myNodeNum: number | undefined;
|
||||
traceroutes: Map<number, Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]>;
|
||||
traceroutes: Map<
|
||||
number,
|
||||
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
|
||||
>;
|
||||
waypoints: WaypointWithMetadata[];
|
||||
neighborInfo: Map<number, Protobuf.Mesh.NeighborInfo>;
|
||||
};
|
||||
export type ConnectionPhase = "disconnected" | "connecting" | "configuring" | "configured";
|
||||
export type ConnectionPhase =
|
||||
| "disconnected"
|
||||
| "connecting"
|
||||
| "configuring"
|
||||
| "configured";
|
||||
|
||||
export interface Device extends DeviceData {
|
||||
// Ephemeral state (not persisted)
|
||||
@@ -73,16 +84,25 @@ export interface Device extends DeviceData {
|
||||
removeWaypoint: (waypointId: number, toMesh: boolean) => Promise<void>;
|
||||
getWaypoint: (waypointId: number) => WaypointWithMetadata | undefined;
|
||||
addConnection: (connection: MeshDevice) => void;
|
||||
addTraceRoute: (traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>) => void;
|
||||
addTraceRoute: (
|
||||
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>,
|
||||
) => void;
|
||||
addMetadata: (from: number, metadata: Protobuf.Mesh.DeviceMetadata) => void;
|
||||
setDialogOpen: (dialog: DialogVariant, open: boolean) => void;
|
||||
getDialogOpen: (dialog: DialogVariant) => boolean;
|
||||
setMessageDraft: (message: string) => void;
|
||||
sendAdminMessage: (message: Protobuf.Admin.AdminMessage) => void;
|
||||
addClientNotification: (clientNotificationPacket: Protobuf.Mesh.ClientNotification) => void;
|
||||
addClientNotification: (
|
||||
clientNotificationPacket: Protobuf.Mesh.ClientNotification,
|
||||
) => void;
|
||||
removeClientNotification: (index: number) => void;
|
||||
getClientNotification: (index: number) => Protobuf.Mesh.ClientNotification | undefined;
|
||||
addNeighborInfo: (nodeNum: number, neighborInfo: Protobuf.Mesh.NeighborInfo) => void;
|
||||
getClientNotification: (
|
||||
index: number,
|
||||
) => Protobuf.Mesh.ClientNotification | undefined;
|
||||
addNeighborInfo: (
|
||||
nodeNum: number,
|
||||
neighborInfo: Protobuf.Mesh.NeighborInfo,
|
||||
) => void;
|
||||
getNeighborInfo: (nodeNum: number) => Protobuf.Mesh.NeighborInfo | undefined;
|
||||
}
|
||||
|
||||
@@ -95,7 +115,10 @@ export interface deviceState {
|
||||
// Saved connections management
|
||||
savedConnections: Connection[];
|
||||
addSavedConnection: (connection: Connection) => void;
|
||||
updateSavedConnection: (id: ConnectionId, updates: Partial<Connection>) => void;
|
||||
updateSavedConnection: (
|
||||
id: ConnectionId,
|
||||
updates: Partial<Connection>,
|
||||
) => void;
|
||||
removeSavedConnection: (id: ConnectionId) => void;
|
||||
getSavedConnections: () => Connection[];
|
||||
|
||||
@@ -127,9 +150,11 @@ function deviceFactory(
|
||||
): Device {
|
||||
const myNodeNum = data?.myNodeNum;
|
||||
const traceroutes =
|
||||
data?.traceroutes ?? new Map<number, Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]>();
|
||||
data?.traceroutes ??
|
||||
new Map<number, Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]>();
|
||||
const waypoints = data?.waypoints ?? [];
|
||||
const neighborInfo = data?.neighborInfo ?? new Map<number, Protobuf.Mesh.NeighborInfo>();
|
||||
const neighborInfo =
|
||||
data?.neighborInfo ?? new Map<number, Protobuf.Mesh.NeighborInfo>();
|
||||
return {
|
||||
id,
|
||||
myNodeNum,
|
||||
@@ -257,7 +282,8 @@ function deviceFactory(
|
||||
break;
|
||||
}
|
||||
case "externalNotification": {
|
||||
device.moduleConfig.externalNotification = config.payloadVariant.value;
|
||||
device.moduleConfig.externalNotification =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "storeForward": {
|
||||
@@ -285,11 +311,13 @@ function deviceFactory(
|
||||
break;
|
||||
}
|
||||
case "ambientLighting": {
|
||||
device.moduleConfig.ambientLighting = config.payloadVariant.value;
|
||||
device.moduleConfig.ambientLighting =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "detectionSensor": {
|
||||
device.moduleConfig.detectionSensor = config.payloadVariant.value;
|
||||
device.moduleConfig.detectionSensor =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "paxcounter": {
|
||||
@@ -297,7 +325,8 @@ function deviceFactory(
|
||||
break;
|
||||
}
|
||||
case "remoteHardware": {
|
||||
device.moduleConfig.remoteHardware = config.payloadVariant.value;
|
||||
device.moduleConfig.remoteHardware =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "statusmessage": {
|
||||
@@ -305,7 +334,8 @@ function deviceFactory(
|
||||
break;
|
||||
}
|
||||
case "trafficManagement": {
|
||||
device.moduleConfig.trafficManagement = config.payloadVariant.value;
|
||||
device.moduleConfig.trafficManagement =
|
||||
config.payloadVariant.value;
|
||||
break;
|
||||
}
|
||||
case "tak": {
|
||||
@@ -390,10 +420,13 @@ function deviceFactory(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const index = device.waypoints.findIndex((wp) => wp.id === waypoint.id);
|
||||
const index = device.waypoints.findIndex(
|
||||
(wp) => wp.id === waypoint.id,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
const created = device.waypoints[index]?.metadata.created ?? new Date();
|
||||
const created =
|
||||
device.waypoints[index]?.metadata.created ?? new Date();
|
||||
const updatedWaypoint = {
|
||||
...waypoint,
|
||||
metadata: { created, updated: rxTime, from, channel },
|
||||
@@ -462,7 +495,9 @@ function deviceFactory(
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = device.waypoints.findIndex((waypoint) => waypoint.id === waypointId);
|
||||
const idx = device.waypoints.findIndex(
|
||||
(waypoint) => waypoint.id === waypointId,
|
||||
);
|
||||
if (idx >= 0) {
|
||||
device.waypoints.splice(idx, 1);
|
||||
}
|
||||
@@ -520,7 +555,10 @@ function deviceFactory(
|
||||
|
||||
// Enforce retention limit, both in terms of targets (device.traceroutes) and routes per target (routes)
|
||||
evictOldestEntries(routes, TRACEROUTE_ROUTE_RETENTION_NUM);
|
||||
evictOldestEntries(device.traceroutes, TRACEROUTE_TARGET_RETENTION_NUM);
|
||||
evictOldestEntries(
|
||||
device.traceroutes,
|
||||
TRACEROUTE_TARGET_RETENTION_NUM,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -565,7 +603,9 @@ function deviceFactory(
|
||||
);
|
||||
},
|
||||
|
||||
addClientNotification: (clientNotificationPacket: Protobuf.Mesh.ClientNotification) => {
|
||||
addClientNotification: (
|
||||
clientNotificationPacket: Protobuf.Mesh.ClientNotification,
|
||||
) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
@@ -594,7 +634,10 @@ function deviceFactory(
|
||||
}
|
||||
return device.clientNotifications[index];
|
||||
},
|
||||
addNeighborInfo: (nodeId: number, neighborInfo: Protobuf.Mesh.NeighborInfo) => {
|
||||
addNeighborInfo: (
|
||||
nodeId: number,
|
||||
neighborInfo: Protobuf.Mesh.NeighborInfo,
|
||||
) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
@@ -618,7 +661,10 @@ function deviceFactory(
|
||||
};
|
||||
}
|
||||
|
||||
export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (set, get) => ({
|
||||
export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (
|
||||
set,
|
||||
get,
|
||||
) => ({
|
||||
devices: new Map(),
|
||||
savedConnections: [],
|
||||
activeConnectionId: null,
|
||||
@@ -632,7 +678,10 @@ export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (set, ge
|
||||
const device = deviceFactory(id, get, set);
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
draft.devices = new Map(draft.devices).set(id, device as unknown as Draft<Device>);
|
||||
draft.devices = new Map(draft.devices).set(
|
||||
id,
|
||||
device as unknown as Draft<Device>,
|
||||
);
|
||||
|
||||
// Enforce retention limit
|
||||
evictOldestEntries(draft.devices, DEVICESTORE_RETENTION_NUM);
|
||||
@@ -663,11 +712,14 @@ export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (set, ge
|
||||
updateSavedConnection: (id, updates) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const conn = draft.savedConnections.find((c: Connection) => c.id === id);
|
||||
const conn = draft.savedConnections.find(
|
||||
(c: Connection) => c.id === id,
|
||||
);
|
||||
if (conn) {
|
||||
for (const key in updates) {
|
||||
if (Object.hasOwn(updates, key)) {
|
||||
(conn as Record<string, unknown>)[key] = updates[key as keyof typeof updates];
|
||||
(conn as Record<string, unknown>)[key] =
|
||||
updates[key as keyof typeof updates];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,7 +729,9 @@ export const deviceStoreInitializer: StateCreator<PrivateDeviceState> = (set, ge
|
||||
removeSavedConnection: (id) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
draft.savedConnections = draft.savedConnections.filter((c: Connection) => c.id !== id);
|
||||
draft.savedConnections = draft.savedConnections.filter(
|
||||
(c: Connection) => c.id !== id,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -744,12 +798,19 @@ const persistOptions: PersistOptions<PrivateDeviceState, DevicePersisted> = {
|
||||
useDeviceStore.setState(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const rebuilt = new Map<number, Device>();
|
||||
for (const [id, data] of (draft.devices as unknown as Map<number, DeviceData>).entries()) {
|
||||
for (const [id, data] of (
|
||||
draft.devices as unknown as Map<number, DeviceData>
|
||||
).entries()) {
|
||||
if (data.myNodeNum !== undefined) {
|
||||
// Only rebuild if there is a nodenum set otherwise orphan dbs will acumulate
|
||||
rebuilt.set(
|
||||
id,
|
||||
deviceFactory(id, useDeviceStore.getState, useDeviceStore.setState, data),
|
||||
deviceFactory(
|
||||
id,
|
||||
useDeviceStore.getState,
|
||||
useDeviceStore.setState,
|
||||
data,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ export function useSetActiveConnectionId() {
|
||||
* Hook to get a specific connection's status
|
||||
*/
|
||||
export function useConnectionStatus(id: ConnectionId): string | undefined {
|
||||
return useDeviceStore((s) => s.savedConnections.find((c) => c.id === id)?.status);
|
||||
return useDeviceStore(
|
||||
(s) => s.savedConnections.find((c) => c.id === id)?.status,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +65,9 @@ export function useDeviceForConnection(id: ConnectionId): Device | undefined {
|
||||
/**
|
||||
* Hook to get a connection for a specific device
|
||||
*/
|
||||
export function useConnectionForDevice(deviceId: number): Connection | undefined {
|
||||
export function useConnectionForDevice(
|
||||
deviceId: number,
|
||||
): Connection | undefined {
|
||||
return useDeviceStore((s) => s.getConnectionForDevice(deviceId));
|
||||
}
|
||||
|
||||
@@ -72,7 +76,9 @@ export function useConnectionForDevice(deviceId: number): Connection | undefined
|
||||
*/
|
||||
export function useIsConnecting(): boolean {
|
||||
return useDeviceStore((s) =>
|
||||
s.savedConnections.some((c) => c.status === "connecting" || c.status === "configuring"),
|
||||
s.savedConnections.some(
|
||||
(c) => c.status === "connecting" || c.status === "configuring",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,7 +86,9 @@ export function useIsConnecting(): boolean {
|
||||
* Hook to get error message for a specific connection
|
||||
*/
|
||||
export function useConnectionError(id: ConnectionId): string | null {
|
||||
return useDeviceStore((s) => s.savedConnections.find((c) => c.id === id)?.error ?? null);
|
||||
return useDeviceStore(
|
||||
(s) => s.savedConnections.find((c) => c.id === id)?.error ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Protobuf } from "@meshtastic/sdk";
|
||||
|
||||
type ValidConfigType = Exclude<keyof Protobuf.LocalOnly.LocalConfig, "version" | "$typeName">;
|
||||
type ValidConfigType = Exclude<
|
||||
keyof Protobuf.LocalOnly.LocalConfig,
|
||||
"version" | "$typeName"
|
||||
>;
|
||||
type ValidModuleConfigType = Exclude<
|
||||
keyof Protobuf.LocalOnly.LocalModuleConfig,
|
||||
"version" | "$typeName"
|
||||
|
||||
@@ -42,6 +42,8 @@ export { clear as clearAllStores } from "idb-keyval";
|
||||
export const useDevice = (): Device => {
|
||||
const { deviceId } = useDeviceContext();
|
||||
|
||||
const device = useDeviceStore((s) => s.getDevice(deviceId) ?? s.addDevice(deviceId));
|
||||
const device = useDeviceStore(
|
||||
(s) => s.getDevice(deviceId) ?? s.addDevice(deviceId),
|
||||
);
|
||||
return device;
|
||||
};
|
||||
|
||||
@@ -37,8 +37,13 @@ interface GenericMessage<T extends MessageType> extends MessageBase {
|
||||
type: T;
|
||||
}
|
||||
|
||||
export type Message = GenericMessage<MessageType.Direct> | GenericMessage<MessageType.Broadcast>;
|
||||
export type Message =
|
||||
| GenericMessage<MessageType.Direct>
|
||||
| GenericMessage<MessageType.Broadcast>;
|
||||
|
||||
export function getConversationId(node1: NodeNum, node2: NodeNum): ConversationId {
|
||||
export function getConversationId(
|
||||
node1: NodeNum,
|
||||
node2: NodeNum,
|
||||
): ConversationId {
|
||||
return [node1, node2].sort((a, b) => a - b).join(":");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ interface GenericMessage<T extends MessageType> extends MessageBase {
|
||||
type: T;
|
||||
}
|
||||
|
||||
type Message = GenericMessage<MessageType.Direct> | GenericMessage<MessageType.Broadcast>;
|
||||
type Message =
|
||||
| GenericMessage<MessageType.Direct>
|
||||
| GenericMessage<MessageType.Broadcast>;
|
||||
|
||||
type GetMessagesParams =
|
||||
| { type: MessageType.Direct; nodeA: NodeNum; nodeB: NodeNum }
|
||||
|
||||
@@ -7,9 +7,13 @@ interface SidebarContextProps {
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextProps | undefined>(undefined);
|
||||
const SidebarContext = createContext<SidebarContextProps | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
|
||||
|
||||
const toggleSidebar = useMemo(
|
||||
@@ -28,7 +32,9 @@ export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||
[isCollapsed, toggleSidebar],
|
||||
);
|
||||
|
||||
return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>;
|
||||
return (
|
||||
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSidebar = (): SidebarContextProps => {
|
||||
|
||||
@@ -29,7 +29,10 @@ export function bindStoreToDevice<S, DB>(
|
||||
function useBound<T>(selector: (db: DB) => T, opts?: DebounceOpts<T>): T;
|
||||
|
||||
// Implementation:
|
||||
function useBound<T>(selector?: (db: DB) => T, opts?: DebounceOpts<T>): DB | T {
|
||||
function useBound<T>(
|
||||
selector?: (db: DB) => T,
|
||||
opts?: DebounceOpts<T>,
|
||||
): DB | T {
|
||||
const { deviceId } = useDeviceContext();
|
||||
|
||||
// Build the store-level selector
|
||||
@@ -53,7 +56,9 @@ export function bindStoreToDevice<S, DB>(
|
||||
const snapRef = useRef<Selected>(storeSelector(store.getState()));
|
||||
snapRef.current = storeSelector(store.getState()); // this ensures rerenders with a new selector (new deviceId) see the right initial value
|
||||
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const subscribe = useMemo(() => {
|
||||
return (onChange: () => void) => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export function evictOldestEntries<T>(arr: T[], maxSize: number): void;
|
||||
export function evictOldestEntries<K, V>(map: Map<K, V>, maxSize: number): void;
|
||||
|
||||
export function evictOldestEntries<T, K, V>(collection: T[] | Map<K, V>, maxSize: number): void {
|
||||
export function evictOldestEntries<T, K, V>(
|
||||
collection: T[] | Map<K, V>,
|
||||
maxSize: number,
|
||||
): void {
|
||||
if (Array.isArray(collection)) {
|
||||
// Trim array from the front (assuming oldest entries are at the start)
|
||||
while (collection.length > maxSize) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { del, get, set } from "idb-keyval";
|
||||
import type { PersistStorage, StateStorage, StorageValue } from "zustand/middleware";
|
||||
import type {
|
||||
PersistStorage,
|
||||
StateStorage,
|
||||
StorageValue,
|
||||
} from "zustand/middleware";
|
||||
|
||||
export const zustandIndexDBStorage: StateStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
@@ -53,7 +57,9 @@ const uint8ArrayHandler: Handler<Uint8Array, "Uint8Array", number[]> = {
|
||||
|
||||
const defaultHandlers = [mapHandler, uint8ArrayHandler] as const;
|
||||
|
||||
function makeJson<H extends Handler<unknown, string, unknown>>(handlers: readonly H[]) {
|
||||
function makeJson<H extends Handler<unknown, string, unknown>>(
|
||||
handlers: readonly H[],
|
||||
) {
|
||||
const byTag = new Map<H["tag"], H>();
|
||||
for (const handler of handlers) {
|
||||
byTag.set(handler.tag, handler);
|
||||
@@ -77,7 +83,9 @@ function makeJson<H extends Handler<unknown, string, unknown>>(handlers: readonl
|
||||
if (isEnvelope(value)) {
|
||||
const handler = byTag.get(value.__datatype);
|
||||
if (handler) {
|
||||
return handler.revive((value as Envelope<H["tag"], unknown>).value as never);
|
||||
return handler.revive(
|
||||
(value as Envelope<H["tag"], unknown>).value as never,
|
||||
);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
@@ -86,10 +94,14 @@ function makeJson<H extends Handler<unknown, string, unknown>>(handlers: readonl
|
||||
return { replacer, reviver };
|
||||
}
|
||||
|
||||
export function createStorage<T, H extends Handler<unknown, string, unknown> = never>(
|
||||
extraHandlers: readonly H[] = [] as const,
|
||||
): PersistStorage<T> {
|
||||
const { replacer, reviver } = makeJson([...defaultHandlers, ...extraHandlers]);
|
||||
export function createStorage<
|
||||
T,
|
||||
H extends Handler<unknown, string, unknown> = never,
|
||||
>(extraHandlers: readonly H[] = [] as const): PersistStorage<T> {
|
||||
const { replacer, reviver } = makeJson([
|
||||
...defaultHandlers,
|
||||
...extraHandlers,
|
||||
]);
|
||||
return {
|
||||
getItem: async (name): Promise<StorageValue<T> | null> => {
|
||||
const str = await zustandIndexDBStorage.getItem(name);
|
||||
@@ -109,7 +121,10 @@ export function createStorage<T, H extends Handler<unknown, string, unknown> = n
|
||||
const str = JSON.stringify(newValue, replacer);
|
||||
await zustandIndexDBStorage.setItem(name, str);
|
||||
} catch (error) {
|
||||
console.error(`Error stringifying or setting persisted state (${name}):`, error);
|
||||
console.error(
|
||||
`Error stringifying or setting persisted state (${name}):`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
removeItem: async (name): Promise<void> => {
|
||||
|
||||
@@ -20,7 +20,9 @@ export const subscribeAll = (device: Device, connection: MeshDevice) => {
|
||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||
switch (routingPacket.data.variant.case) {
|
||||
case "errorReason": {
|
||||
if (routingPacket.data.variant.value === Protobuf.Mesh.Routing_Error.NONE) {
|
||||
if (
|
||||
routingPacket.data.variant.value === Protobuf.Mesh.Routing_Error.NONE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
console.info(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||
@@ -90,10 +92,12 @@ export const subscribeAll = (device: Device, connection: MeshDevice) => {
|
||||
// onMeshPacket → lastHeard / snr per-node updates are handled by the SDK
|
||||
// NodesClient.
|
||||
|
||||
connection.events.onClientNotificationPacket.subscribe((clientNotificationPacket) => {
|
||||
device.addClientNotification(clientNotificationPacket);
|
||||
device.setDialogOpen("clientNotification", true);
|
||||
});
|
||||
connection.events.onClientNotificationPacket.subscribe(
|
||||
(clientNotificationPacket) => {
|
||||
device.addClientNotification(clientNotificationPacket);
|
||||
device.setDialogOpen("clientNotification", true);
|
||||
},
|
||||
);
|
||||
|
||||
connection.events.onNeighborInfoPacket.subscribe((neighborInfo) => {
|
||||
device.addNeighborInfo(neighborInfo.from, neighborInfo.data);
|
||||
|
||||
@@ -6,7 +6,10 @@ export const bitwiseEncode = (enumValues: number[]): number => {
|
||||
return enumValues.reduce((acc, curr) => acc | curr, 0);
|
||||
};
|
||||
|
||||
export const bitwiseDecode = (value: number, decodeEnum: EnumLike): number[] => {
|
||||
export const bitwiseDecode = (
|
||||
value: number,
|
||||
decodeEnum: EnumLike,
|
||||
): number[] => {
|
||||
const enumValues = Object.keys(decodeEnum).map(Number).filter(Boolean);
|
||||
return enumValues.map((b) => value & b).filter(Boolean);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getColorFromNodeNum, hexToRgb, isLightColor, type RGBColor, rgbToHex } from "./color.ts";
|
||||
import {
|
||||
getColorFromNodeNum,
|
||||
hexToRgb,
|
||||
isLightColor,
|
||||
type RGBColor,
|
||||
rgbToHex,
|
||||
} from "./color.ts";
|
||||
|
||||
describe("hexToRgb", () => {
|
||||
it.each([
|
||||
@@ -23,7 +29,9 @@ describe("rgbToHex", () => {
|
||||
});
|
||||
|
||||
it("rounds component values before packing", () => {
|
||||
expect(rgbToHex({ r: 12.2, g: 12.8, b: 99.5 })).toBe((12 << 16) | (13 << 8) | 100);
|
||||
expect(rgbToHex({ r: 12.2, g: 12.8, b: 99.5 })).toBe(
|
||||
(12 << 16) | (13 << 8) | 100,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function deepCompareConfig(a: unknown, b: unknown, allowUndefined = false): boolean {
|
||||
export function deepCompareConfig(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
allowUndefined = false,
|
||||
): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,13 @@ describe("dotPaths", () => {
|
||||
a: [{ b: 1, c: [2, 3] }, { d: { e: 4 } }],
|
||||
f: 5,
|
||||
};
|
||||
expect(dotPaths(obj)).toEqual(["a.0.b", "a.0.c.0", "a.0.c.1", "a.1.d.e", "f"]);
|
||||
expect(dotPaths(obj)).toEqual([
|
||||
"a.0.b",
|
||||
"a.0.c.0",
|
||||
"a.0.c.1",
|
||||
"a.1.d.e",
|
||||
"f",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles prefix argument", () => {
|
||||
|
||||
@@ -3,10 +3,14 @@ export type DotPath = { [key: string]: unknown } | unknown[];
|
||||
export const dotPaths = <T extends DotPath>(obj: T, prefix = ""): string[] => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.flatMap((v, i) =>
|
||||
v && typeof v === "object" ? dotPaths(v as DotPath, `${prefix}${i}.`) : [`${prefix}${i}`],
|
||||
v && typeof v === "object"
|
||||
? dotPaths(v as DotPath, `${prefix}${i}.`)
|
||||
: [`${prefix}${i}`],
|
||||
);
|
||||
}
|
||||
return Object.entries(obj).flatMap(([k, v]) =>
|
||||
v && typeof v === "object" ? dotPaths(v as DotPath, `${prefix}${k}.`) : [`${prefix}${k}`],
|
||||
v && typeof v === "object"
|
||||
? dotPaths(v as DotPath, `${prefix}${k}.`)
|
||||
: [`${prefix}${k}`],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,10 @@ export type EventCallback<T extends EventName> = (data: EventMap[T]) => void;
|
||||
class EventBus {
|
||||
private listeners: { [K in EventName]?: Array<EventCallback<K>> } = {};
|
||||
|
||||
public on<T extends EventName>(event: T, callback: EventCallback<T>): () => void {
|
||||
public on<T extends EventName>(
|
||||
event: T,
|
||||
callback: EventCallback<T>,
|
||||
): () => void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
|
||||
@@ -7,12 +7,18 @@ export type Bounds = [[number, number], [number, number]];
|
||||
const INT_DEG = 1e7;
|
||||
const EARTH_RADIUS = 6378137;
|
||||
|
||||
export const toLngLat = (position?: { latitudeI?: number; longitudeI?: number }): LngLat => [
|
||||
export const toLngLat = (position?: {
|
||||
latitudeI?: number;
|
||||
longitudeI?: number;
|
||||
}): LngLat => [
|
||||
(position?.longitudeI ?? 0) / INT_DEG,
|
||||
(position?.latitudeI ?? 0) / INT_DEG,
|
||||
];
|
||||
|
||||
export const hasPos = (position?: { latitudeI?: number; longitudeI?: number }) =>
|
||||
export const hasPos = (position?: {
|
||||
latitudeI?: number;
|
||||
longitudeI?: number;
|
||||
}) =>
|
||||
Number.isFinite(position?.latitudeI) &&
|
||||
Number.isFinite(position?.longitudeI) &&
|
||||
!(position?.latitudeI === 0 && position?.longitudeI === 0);
|
||||
@@ -74,7 +80,8 @@ export function bearingDegrees(from: LngLat, to: LngLat): number {
|
||||
|
||||
const y = Math.sin(deltaLambda) * Math.cos(phi2);
|
||||
const x =
|
||||
Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(deltaLambda);
|
||||
Math.cos(phi1) * Math.sin(phi2) -
|
||||
Math.sin(phi1) * Math.cos(phi2) * Math.cos(deltaLambda);
|
||||
|
||||
return (rad2deg(Math.atan2(y, x)) + 360) % 360;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ const VALID_PARAMS = [
|
||||
* @throws {Error} If repository information is missing or invalid
|
||||
* @throws {TypeError} If labels or projects are not arrays when provided
|
||||
*/
|
||||
export default function newGithubIssueUrl(options: GithubIssueUrlOptions = {}): string {
|
||||
export default function newGithubIssueUrl(
|
||||
options: GithubIssueUrlOptions = {},
|
||||
): string {
|
||||
const validatedOptions = validateOptions(options);
|
||||
const url = new URL(`${validatedOptions.repoUrl}/issues/new`);
|
||||
|
||||
|
||||
@@ -49,7 +49,13 @@ describe("IP Address Conversion Functions", () => {
|
||||
|
||||
describe("bidirectional conversion", () => {
|
||||
it("can convert back and forth", () => {
|
||||
const testIps = ["0.0.0.0", "127.0.0.1", "192.168.1.1", "10.0.0.1", "255.255.255.255"];
|
||||
const testIps = [
|
||||
"0.0.0.0",
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"255.255.255.255",
|
||||
];
|
||||
|
||||
for (const ip of testIps) {
|
||||
const int = convertIpAddressToInt(ip);
|
||||
|
||||
@@ -15,10 +15,16 @@ export const LINE_COLOR = {
|
||||
};
|
||||
|
||||
export const getSignalColor = (snr: number, rssi?: number): string => {
|
||||
if (snr > SNR_THRESHOLD.GOOD && (rssi == null || rssi > RSSI_THRESHOLD.GOOD)) {
|
||||
if (
|
||||
snr > SNR_THRESHOLD.GOOD &&
|
||||
(rssi == null || rssi > RSSI_THRESHOLD.GOOD)
|
||||
) {
|
||||
return LINE_COLOR.GOOD;
|
||||
}
|
||||
if (snr > SNR_THRESHOLD.FAIR && (rssi == null || rssi > RSSI_THRESHOLD.FAIR)) {
|
||||
if (
|
||||
snr > SNR_THRESHOLD.FAIR &&
|
||||
(rssi == null || rssi > RSSI_THRESHOLD.FAIR)
|
||||
) {
|
||||
return LINE_COLOR.FAIR;
|
||||
}
|
||||
return LINE_COLOR.BAD;
|
||||
|
||||
@@ -10,7 +10,11 @@ interface FormatOptions {
|
||||
numberFormat?: Intl.NumberFormatOptions;
|
||||
}
|
||||
|
||||
export function formatQuantity(value: number, forms: PluralForms, options: FormatOptions = {}) {
|
||||
export function formatQuantity(
|
||||
value: number,
|
||||
forms: PluralForms,
|
||||
options: FormatOptions = {},
|
||||
) {
|
||||
const {
|
||||
locale = "en-US",
|
||||
pluralRules: pluralOptions = { type: "cardinal" },
|
||||
@@ -36,8 +40,14 @@ export function validateMaxByteLength(
|
||||
maxByteLength: number,
|
||||
): LengthValidationResult {
|
||||
// Ensure maxByteLength is valid
|
||||
if (typeof maxByteLength !== "number" || !Number.isInteger(maxByteLength) || maxByteLength < 0) {
|
||||
console.warn("validateMaxByteLength: maxByteLength must be a non-negative integer.");
|
||||
if (
|
||||
typeof maxByteLength !== "number" ||
|
||||
!Number.isInteger(maxByteLength) ||
|
||||
maxByteLength < 0
|
||||
) {
|
||||
console.warn(
|
||||
"validateMaxByteLength: maxByteLength must be a non-negative integer.",
|
||||
);
|
||||
return { isValid: false, currentLength: null }; // Cannot validate with invalid limit
|
||||
}
|
||||
|
||||
@@ -48,7 +58,9 @@ export function validateMaxByteLength(
|
||||
|
||||
// Check for TextEncoder availability
|
||||
if (typeof TextEncoder === "undefined") {
|
||||
console.error("validateMaxByteLength: TextEncoder API is not available in this environment.");
|
||||
console.error(
|
||||
"validateMaxByteLength: TextEncoder API is not available in this environment.",
|
||||
);
|
||||
return { isValid: false, currentLength: null }; // Cannot determine byte length
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user