A decentralized music tracking and discovery platform built on AT Protocol 🎵

Persist CLI KV and cache matchTrack results

Store sqlite KV under OS-specific data path using env-paths
(rocksky/rocksky-kv.sqlite) and ensure directory exists. Export kv from
context so the CLI uses persistent storage.

Replace the previous DB query in matchTrack with a kv-backed cache. On
cache hit return cached result and trigger an async background refresh;
on miss fetch from RockskyClient and save to kv. Remove now-unused
imports and schema usage.

+19 -40
+7 -1
apps/cli/src/context.ts
··· 2 2 import sqliteKv from "sqliteKv"; 3 3 import { createBidirectionalResolver, createIdResolver } from "lib/idResolver"; 4 4 import { createStorage } from "unstorage"; 5 + import envpaths from "env-paths"; 6 + import fs from "node:fs"; 7 + 8 + fs.mkdirSync(envpaths("rocksky", { suffix: "" }).data, { recursive: true }); 9 + const kvPath = `${envpaths("rocksky", { suffix: "" }).data}/rocksky-kv.sqlite`; 5 10 6 11 const kv = createStorage({ 7 - driver: sqliteKv({ location: ":memory:", table: "kv" }), 12 + driver: sqliteKv({ location: kvPath, table: "kv" }), 8 13 }); 9 14 10 15 const baseIdResolver = createIdResolver(kv); ··· 13 18 db: drizzle.db, 14 19 resolver: createBidirectionalResolver(baseIdResolver), 15 20 baseIdResolver, 21 + kv, 16 22 }; 17 23 18 24 export type Context = typeof ctx;
+12 -39
apps/cli/src/lib/matchTrack.ts
··· 1 1 import { RockskyClient } from "client"; 2 2 import { ctx } from "context"; 3 - import { eq, and, or } from "drizzle-orm"; 4 3 import { logger } from "logger"; 5 - import schema from "schema"; 6 4 import { SelectTrack } from "schema/tracks"; 7 5 8 6 export type MatchTrackResult = SelectTrack & { ··· 16 14 track: string, 17 15 artist: string, 18 16 ): Promise<MatchTrackResult | null> { 19 - const [result] = await ctx.db 20 - .select() 21 - .from(schema.tracks) 22 - .leftJoin( 23 - schema.albumTracks, 24 - eq(schema.albumTracks.trackId, schema.tracks.id), 25 - ) 26 - .leftJoin(schema.albums, eq(schema.albumTracks.albumId, schema.albums.id)) 27 - .leftJoin( 28 - schema.artistAlbums, 29 - eq(schema.artistAlbums.albumId, schema.albums.id), 30 - ) 31 - .leftJoin( 32 - schema.artists, 33 - eq(schema.artistAlbums.artistId, schema.artists.id), 34 - ) 35 - .where( 36 - or( 37 - and(eq(schema.tracks.title, track), eq(schema.tracks.artist, artist)), 38 - and( 39 - eq(schema.tracks.title, track), 40 - eq(schema.tracks.albumArtist, artist), 41 - ), 42 - ), 43 - ) 44 - .execute(); 17 + let match; 18 + const cached = await ctx.kv.getItem(`${track} - ${artist}`); 19 + const client = new RockskyClient(); 45 20 46 - let match = null; 47 - 48 - if (result) { 49 - match = { 50 - ...result.tracks, 51 - genres: result.artists?.genres, 52 - artistPicture: result.artists?.picture, 53 - releaseDate: result.albums?.releaseDate, 54 - year: result.albums?.year, 55 - }; 21 + if (cached) { 22 + match = cached; 23 + client.matchSong(track, artist).then((newMatch) => { 24 + if (newMatch) { 25 + ctx.kv.setItem(`${track} - ${artist}`, newMatch); 26 + } 27 + }); 56 28 } else { 57 - const client = new RockskyClient(); 58 29 match = await client.matchSong(track, artist); 30 + await ctx.kv.setItem(`${track} - ${artist}`, match); 59 31 } 32 + 60 33 logger.info`>> matchTrack ${track}, ${artist}`; 61 34 logger.info`${match}`; 62 35