1import * as Orama from "@orama/orama";
2import { xxh32 } from "xxh32";
3// import { pluginQPS } from "@orama/plugin-qps";
4
5import { SCHEMA } from "./constants.js";
6import { announce, ostiary, rpc } from "@common/worker.js";
7import { effect, signal } from "@common/signal.js";
8
9/**
10 * @import {SearchParams} from "@orama/orama";
11 *
12 * @import {Track} from "@definitions/types.d.ts"
13 * @import {Actions, Schema} from "./types.d.ts"
14 */
15
16////////////////////////////////////////////
17// STATE
18////////////////////////////////////////////
19
20export const $inserted = signal(/** @type {Set<string>} */ (new Set()), {
21 eager: true,
22});
23
24// Communicated state
25export const $cacheId = signal(/** @type {string | undefined} */ (undefined));
26
27////////////////////////////////////////////
28// DATABASE
29////////////////////////////////////////////
30
31// TODO:
32// * pluginEmbeddings
33// * pluginQPS
34
35/**
36 * @type {Orama.OramaPlugin[]}
37 */
38const PLUGINS = [];
39
40const db = Orama.create({
41 schema: SCHEMA,
42 plugins: PLUGINS,
43 // components: {
44 // TODO:
45 // https://docs.orama.com/open-source/usage/insert#remote-document-storing
46 // documentStore: { ... }
47 // },
48});
49
50////////////////////////////////////////////
51// ACTIONS
52////////////////////////////////////////////
53
54/**
55 * @type {Actions['search']}
56 */
57export async function search(params) {
58 return await _search(
59 "term" in params && typeof params.term === "string"
60 ? { ...params, term: params.term.trim() }
61 : params,
62 [],
63 );
64}
65
66/**
67 * @type {Actions['supply']}
68 */
69export async function supply({ tracks }) {
70 // TODO: Generate a hash based on the track itself,
71 // so we can detect changes to tags or other data.
72
73 /** @type {string[]} */
74 const ids = [];
75
76 /** @type {Record<string, Track>} */
77 const tracksMap = {};
78
79 tracks.forEach((track) => {
80 ids.push(track.id);
81 tracksMap[track.id] = track;
82 });
83
84 const currentSet = $inserted.value;
85 const newSet = new Set(ids);
86
87 $inserted.value = newSet;
88
89 const removedIds = currentSet.difference(newSet);
90 const newIds = newSet.difference(currentSet);
91 const newTracks = Array.from(newIds).map((id) => tracksMap[id]);
92
93 await Orama.removeMultiple(db, Array.from(removedIds));
94 await Orama.insertMultiple(db, newTracks);
95
96 $cacheId.value = ids.length === 0
97 ? undefined
98 : xxh32(ids.sort().join("")).toString();
99}
100
101////////////////////////////////////////////
102// ⚡️
103////////////////////////////////////////////
104
105ostiary((context) => {
106 rpc(context, {
107 search,
108 supply,
109
110 // State
111 cacheId: $cacheId.get,
112 });
113
114 // Effects
115
116 // Communicate state
117 effect(() => announce("cacheId", $cacheId.value, context));
118});
119
120////////////////////////////////////////////
121// ⛔️
122////////////////////////////////////////////
123
124/**
125 * @param {SearchParams<Schema>} params
126 * @param {Track[]} tracks
127 */
128async function _search(params, tracks) {
129 const results = await Orama.search(db, {
130 // @ts-ignore: No clue what the correct type is for this one
131 sortBy,
132 ...params,
133 // mode: "hybrid",
134 limit: 10000,
135 offset: tracks.length,
136 });
137
138 const allTracks = tracks.concat(
139 results.hits.map((
140 hit,
141 ) => /** @type {Track} */ (/** @type {unknown} */ (hit.document))),
142 );
143
144 if (allTracks.length < results.count) {
145 return await _search(params, allTracks);
146 } else {
147 return allTracks;
148 }
149}
150
151/**
152 * @type {Orama.CustomSorterFunction<Orama.TypedDocument<Schema>>}
153 */
154function sortBy(a, b) {
155 const artist = (a[2].tags?.artist ?? "").localeCompare(
156 b[2].tags?.artist ?? "",
157 );
158 if (artist != 0) return artist;
159
160 const album = (a[2].tags?.album ?? "").localeCompare(
161 b[2].tags?.album ?? "",
162 );
163 if (album != 0) return album;
164
165 const discNo = (a[2].tags?.disc?.no ?? 0) - (b[2].tags?.disc?.no ?? 0);
166 if (discNo != 0) return discNo;
167
168 const trackNo = (a[2].tags?.track?.no ?? 0) - (b[2].tags?.track?.no ?? 0);
169 if (trackNo != 0) return trackNo;
170
171 const title = (a[2].tags?.title ?? "").localeCompare(
172 b[2].tags?.title ?? "",
173 );
174 if (title != 0) return title;
175 return 0;
176}