Ran pnpm oxfmt . This should get the web repo aligned so that we can better enforce oxfmt going forward.
148 lines
4.1 KiB
TypeScript
148 lines
4.1 KiB
TypeScript
import type { Node, NodesRepository } from "@meshtastic/sdk";
|
|
import { fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
import * as Protobuf from "@meshtastic/protobufs";
|
|
import { and, eq } from "drizzle-orm";
|
|
import type { SqlocalDb } from "../db.ts";
|
|
import { nodes } from "../schema/nodes.ts";
|
|
|
|
export interface SqlocalNodesRepositoryOptions {
|
|
deviceId: number;
|
|
}
|
|
|
|
/**
|
|
* Persists the snapshot of the device's NodeDB. user/position/metrics are
|
|
* stored as base64-encoded protobuf bytes — round-trip safe across schema
|
|
* additions because the wire shape is the source of truth.
|
|
*/
|
|
export class SqlocalNodesRepository implements NodesRepository {
|
|
private readonly db: SqlocalDb;
|
|
private readonly deviceId: number;
|
|
|
|
constructor(db: SqlocalDb, options: SqlocalNodesRepositoryOptions) {
|
|
this.db = db;
|
|
this.deviceId = options.deviceId;
|
|
}
|
|
|
|
async loadAll(): Promise<Node[]> {
|
|
const rows = await this.db
|
|
.select()
|
|
.from(nodes)
|
|
.where(eq(nodes.deviceId, this.deviceId));
|
|
return rows.map(rowToNode);
|
|
}
|
|
|
|
async get(nodeNum: number): Promise<Node | undefined> {
|
|
const rows = await this.db
|
|
.select()
|
|
.from(nodes)
|
|
.where(and(eq(nodes.deviceId, this.deviceId), eq(nodes.num, nodeNum))!)
|
|
.limit(1);
|
|
return rows[0] ? rowToNode(rows[0]) : undefined;
|
|
}
|
|
|
|
async upsert(node: Node): Promise<void> {
|
|
await this.upsertBatch([node]);
|
|
}
|
|
|
|
async upsertBatch(input: ReadonlyArray<Node>): Promise<void> {
|
|
if (input.length === 0) return;
|
|
for (const node of input) {
|
|
const row = nodeToRow(this.deviceId, node);
|
|
await this.db
|
|
.insert(nodes)
|
|
.values(row)
|
|
.onConflictDoUpdate({
|
|
target: [nodes.deviceId, nodes.num],
|
|
set: {
|
|
lastHeard: row.lastHeard,
|
|
snr: row.snr,
|
|
isFavorite: row.isFavorite,
|
|
isIgnored: row.isIgnored,
|
|
userJson: row.userJson,
|
|
positionJson: row.positionJson,
|
|
metricsJson: row.metricsJson,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async remove(nodeNum: number): Promise<void> {
|
|
await this.db
|
|
.delete(nodes)
|
|
.where(and(eq(nodes.deviceId, this.deviceId), eq(nodes.num, nodeNum))!);
|
|
}
|
|
|
|
async clear(): Promise<void> {
|
|
await this.db.delete(nodes).where(eq(nodes.deviceId, this.deviceId));
|
|
}
|
|
}
|
|
|
|
interface NodeRow {
|
|
deviceId: number;
|
|
num: number;
|
|
lastHeard: number | null;
|
|
snr: number | null;
|
|
isFavorite: boolean;
|
|
isIgnored: boolean;
|
|
userJson: string | null;
|
|
positionJson: string | null;
|
|
metricsJson: string | null;
|
|
}
|
|
|
|
function rowToNode(row: NodeRow): Node {
|
|
return {
|
|
num: row.num,
|
|
user: row.userJson
|
|
? fromBinary(Protobuf.Mesh.UserSchema, base64Decode(row.userJson))
|
|
: undefined,
|
|
position: row.positionJson
|
|
? fromBinary(Protobuf.Mesh.PositionSchema, base64Decode(row.positionJson))
|
|
: undefined,
|
|
deviceMetrics: row.metricsJson
|
|
? fromBinary(
|
|
Protobuf.Telemetry.DeviceMetricsSchema,
|
|
base64Decode(row.metricsJson),
|
|
)
|
|
: undefined,
|
|
lastHeard: row.lastHeard ?? undefined,
|
|
snr: row.snr ?? undefined,
|
|
isFavorite: row.isFavorite,
|
|
isIgnored: row.isIgnored,
|
|
};
|
|
}
|
|
|
|
function nodeToRow(deviceId: number, node: Node): NodeRow {
|
|
return {
|
|
deviceId,
|
|
num: node.num,
|
|
lastHeard: node.lastHeard ?? null,
|
|
snr: node.snr ?? null,
|
|
isFavorite: node.isFavorite,
|
|
isIgnored: node.isIgnored,
|
|
userJson: node.user
|
|
? base64Encode(toBinary(Protobuf.Mesh.UserSchema, node.user))
|
|
: null,
|
|
positionJson: node.position
|
|
? base64Encode(toBinary(Protobuf.Mesh.PositionSchema, node.position))
|
|
: null,
|
|
metricsJson: node.deviceMetrics
|
|
? base64Encode(
|
|
toBinary(Protobuf.Telemetry.DeviceMetricsSchema, node.deviceMetrics),
|
|
)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function base64Encode(bytes: Uint8Array): string {
|
|
let s = "";
|
|
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!);
|
|
return btoa(s);
|
|
}
|
|
|
|
function base64Decode(s: string): Uint8Array {
|
|
const bin = atob(s);
|
|
const out = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
return out;
|
|
}
|