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:
Austin
2026-06-16 20:30:08 -04:00
committed by GitHub
parent c5690d1edb
commit d2cb51d489
378 changed files with 5548 additions and 1974 deletions
+8 -2
View File
@@ -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) => {
+35 -12
View File
@@ -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, {
+31 -9
View File
@@ -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;
}
+4 -2
View File
@@ -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,
+21 -6
View File
@@ -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,
+4 -1
View File
@@ -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);
+6 -2
View File
@@ -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;
}
+3 -1
View File
@@ -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", {
+5 -1
View File
@@ -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 {
+3 -1
View File
@@ -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) => {
+11 -3
View File
@@ -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)),
+4 -1
View File
@@ -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);
+33 -10
View File
@@ -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]);
+4 -1
View File
@@ -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);
}
+3 -1
View File
@@ -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);
+3 -1
View File
@@ -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);
+7 -2
View File
@@ -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],
+10 -3
View File
@@ -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(
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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": {
+9 -2
View File
@@ -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
+4 -1
View File
@@ -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([]);
+11 -3
View File
@@ -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);
}
});
+89 -28
View File
@@ -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"
+3 -1
View File
@@ -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) {
+23 -8
View File
@@ -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> => {
+9 -5
View File
@@ -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);
+4 -1
View File
@@ -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);
};
+10 -2
View File
@@ -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,
);
});
});
+5 -1
View File
@@ -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;
}
+7 -1
View File
@@ -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", () => {
+6 -2
View File
@@ -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}`],
);
};
+4 -1
View File
@@ -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] = [];
}
+10 -3
View File
@@ -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;
}
+3 -1
View File
@@ -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`);
+7 -1
View File
@@ -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);
+8 -2
View File
@@ -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;
+16 -4
View File
@@ -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
}