A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: various perf issues

+68 -37
+6 -1
src/common/element.js
··· 3 3 import { html, render } from "lit-html"; 4 4 5 5 import { effect, signal } from "@common/signal.js"; 6 - import { rpc, workerLink, workerProxy, workerTunnel } from "./worker.js"; 6 + import { 7 + rpc, 8 + workerLink, 9 + workerProxy, 10 + workerTunnel, 11 + } from "./worker.js"; 7 12 import { BrowserPostMessageIo } from "./worker/rpc.js"; 8 13 9 14 /**
+1 -1
src/components/configurator/input/worker.js
··· 89 89 ); 90 90 91 91 return consultations.reduce((acc, c) => { 92 - return { ...acc, ...c }; 92 + return Object.assign(acc, c); 93 93 }, {}); 94 94 } 95 95
-1
src/components/input/opensubsonic/common.js
··· 57 57 export async function consultServer(server) { 58 58 const client = createClient(server); 59 59 const resp = await client.ping().catch(() => undefined); 60 - 61 60 return resp?.status?.toLowerCase() === "ok"; 62 61 } 63 62
+17 -11
src/components/orchestrator/scoped-tracks/element.js
··· 57 57 ); 58 58 }); 59 59 60 + #tracksAvailable = signal(/** @type {Track[]} */ ([])); 60 61 #tracksSearch = signal(/** @type {Track[]} */ ([])); 61 62 #tracksFinal = signal(/** @type {Track[]} */ ([])); 62 63 ··· 118 119 this.effect(async () => { 119 120 const collection = output.tracks.collection(); 120 121 if ((await this.isLeader()) === false) return; 121 - this.#proxy.supplyAvailable(collection); 122 + const { availableTracks } = await this.#proxy.supplyAvailable(collection); 123 + this.#tracksAvailable.value = availableTracks; 122 124 }); 123 125 124 126 // Watch search supply 125 127 this.effect(async () => { 126 128 const _trigger = search.supplyFingerprint(); 129 + const availableTracks = this.#tracksAvailable.value; 127 130 const searchTerm = this.#scope.value?.searchTerm(); 128 131 129 132 if ((await this.isLeader()) === false) return; 130 133 131 - const searchResults = searchTerm 132 - ? await this.#proxy.searchTracks({ term: searchTerm }) 133 - : untracked(() => output.tracks.collection()); 134 - 135 - this.#tracksSearch.set(searchResults); 134 + if (searchTerm?.length) { 135 + const searchResults = await this.#proxy.searchTracks({ 136 + term: searchTerm, 137 + }); 138 + this.#tracksSearch.set(searchResults); 139 + } else { 140 + this.#tracksSearch.set(availableTracks); 141 + } 136 142 }); 137 143 138 144 // Watch `#tracksSearch` + Playlist ··· 142 148 143 149 if ((await this.isLeader()) === false) return; 144 150 145 - this.#tracksFinal.set( 146 - playlist 147 - ? await this.#proxy.filterByPlaylist({ tracks, playlist }) 148 - : tracks, 149 - ); 151 + const final = playlist 152 + ? await this.#proxy.filterByPlaylist({ tracks, playlist }) 153 + : tracks; 154 + 155 + this.#tracksFinal.set(final); 150 156 }); 151 157 } 152 158
+1 -1
src/components/orchestrator/scoped-tracks/types.d.ts
··· 6 6 export type Actions = { 7 7 filterByPlaylist(args: { tracks: Track[]; playlist: Playlist }): Promise<Track[]>; 8 8 searchTracks(params: SearchParams<Schema>): Promise<Track[]>; 9 - supplyAvailable(tracks: Track[]): Promise<void>; 9 + supplyAvailable(tracks: Track[]): Promise<{ availableTracks: Track[] }>; 10 10 };
+12 -6
src/components/orchestrator/scoped-tracks/worker.js
··· 32 32 const groups = await input.groupConsult(cachedTracks); 33 33 34 34 /** @type {Track[]} */ 35 - let availableTracks = []; 35 + const availableTracks = []; 36 36 37 37 Object.values(groups).forEach((value) => { 38 38 if (value.available === false) return; 39 - availableTracks = availableTracks.concat(value.tracks); 39 + for (const track of value.tracks) { 40 + availableTracks.push(track); 41 + } 40 42 }, []); 41 43 42 44 // Set pool 43 - await search.supply({ tracks: availableTracks }); 45 + search.supply({ tracks: availableTracks }); 46 + 47 + // Fin 48 + return { availableTracks }; 44 49 } 45 50 46 51 /** ··· 48 53 */ 49 54 export async function searchTracks({ data, ports }) { 50 55 /** @type {ProxiedActions<SearchProcessorActions>} */ 51 - const search = workerProxy(() => ports.search); 52 - 53 - ports.search.start(); 56 + const search = workerProxy(() => { 57 + ports.search.start(); 58 + return ports.search; 59 + }); 54 60 55 61 return await search.search(data); 56 62 }
+4
src/components/processor/search/constants.js
··· 1 + import { xxh32 } from "xxh32"; 2 + 3 + export const EMPTY_FINGERPRINT = xxh32([].join("")).toString(); 4 + 1 5 /** 2 6 * Maps directly on the `Track` definition 3 7 * (ie. `definitions/output/tracks.json`)
+25 -15
src/components/processor/search/worker.js
··· 17 17 // STATE 18 18 //////////////////////////////////////////// 19 19 20 - export const $inserted = signal(/** @type {Set<string>} */ (new Set()), { 21 - eager: true, 22 - }); 20 + /** @type {Set<string>} */ 21 + export let inserted = new Set(); 22 + export let insertedFingerprint = ""; 23 23 24 24 // Communicated state 25 25 export const $supplyFingerprint = signal( ··· 72 72 // TODO: Generate a hash based on the track itself, 73 73 // so we can detect changes to tags or other data. 74 74 75 - /** @type {string[]} */ 76 - const ids = []; 75 + /** @type {Map<string, Track>} */ 76 + const tracksMap = new Map(); 77 77 78 - /** @type {Record<string, Track>} */ 79 - const tracksMap = {}; 78 + for (const track of tracks) { 79 + tracksMap.set(track.id, track); 80 + } 80 81 81 - tracks.forEach((track) => { 82 - ids.push(track.id); 83 - tracksMap[track.id] = track; 84 - }); 82 + const ids = Array.from(tracksMap.keys()); 83 + const idsString = ids.sort().join(""); 84 + const fingerprint = xxh32(idsString).toString(); 85 85 86 - const currentSet = $inserted.value; 86 + if (fingerprint === insertedFingerprint) return; 87 + 88 + const currentSet = inserted; 87 89 const newSet = new Set(ids); 88 90 89 - $inserted.value = newSet; 91 + inserted = newSet; 92 + insertedFingerprint = fingerprint; 90 93 91 94 const removedIds = currentSet.difference(newSet); 92 95 const newIds = newSet.difference(currentSet); 93 - const newTracks = Array.from(newIds).map((id) => tracksMap[id]); 96 + 97 + /** @type {Track[]} */ 98 + const newTracks = []; 99 + 100 + for (const id of newIds) { 101 + const track = tracksMap.get(id); 102 + if (track) newTracks.push(track); 103 + } 94 104 95 105 await Orama.removeMultiple(db, Array.from(removedIds)); 96 106 await Orama.insertMultiple(db, newTracks); 97 107 98 - $supplyFingerprint.value = xxh32(ids.sort().join("")).toString(); 108 + $supplyFingerprint.value = fingerprint; 99 109 } 100 110 101 111 ////////////////////////////////////////////
+2 -1
src/themes/webamp/browser/element.js
··· 223 223 const isLoading = this.$output.value?.tracks?.state() !== "loaded"; 224 224 const tracks = this.$provider.value?.tracks() ?? []; 225 225 const playlistId = this.$scope.value?.playlistId(); 226 + const searchTerm = this.$scope.value?.searchTerm() ?? ""; 226 227 227 228 // Virtual list 228 229 const totalTracks = tracks.length; ··· 314 315 <search class="field-row"> 315 316 <label for="search-input">Search:</label> 316 317 <input id="search-input" type="search" @change="${this 317 - .setSearchTerm}" /> 318 + .setSearchTerm}" value="${searchTerm}" /> 318 319 <label for="playlist-select">Playlist:</label> 319 320 <select id="playlist-select" @change="${this.setSelectedPlaylistId}"> 320 321 <option value="" ?selected="${!playlistId ||