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

Configure Feed

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

chore: input perf improvements

+84 -58
+31 -6
src/components/input/common.js
··· 1 - import { base64url } from "iso-base/rfc4648"; 2 - 3 1 /** 4 2 * @import {Track} from "@definitions/types.d.ts" 5 3 */ 6 4 7 5 /** 6 + * Creates a time-cached version of an async consult function. 7 + * Results are cached per key for the given TTL. 8 + * 9 + * @template T 10 + * @param {(arg: T) => Promise<boolean>} fn 11 + * @param {(arg: T) => string} keyFn 12 + * @param {number} ttl - Cache TTL in milliseconds 13 + * @returns {(arg: T) => Promise<boolean>} 14 + */ 15 + export function cachedConsult(fn, keyFn, ttl = 60_000 * 5) { 16 + /** @type {Map<string, { value: boolean; expiry: number }>} */ 17 + const cache = new Map(); 18 + 19 + return async (arg) => { 20 + const key = keyFn(arg); 21 + const now = Date.now(); 22 + const cached = cache.get(key); 23 + 24 + if (cached && cached.expiry > now) { 25 + return cached.value; 26 + } 27 + 28 + const value = await fn(arg); 29 + cache.set(key, { value, expiry: now + ttl }); 30 + return value; 31 + }; 32 + } 33 + 34 + /** 8 35 * @param {{ fileUriOrScheme: string; handleFileUri: (args: { fileURI: string; tracks: Track[] }) => Track[]; inputScheme: string; tracks: Track[] }} _ 9 36 */ 10 37 export function detach( ··· 23 50 * @param {string} scheme 24 51 * @param {string} groupId 25 52 */ 26 - export async function groupKeyHash(scheme, groupId) { 27 - const rawBytes = new TextEncoder().encode(`${scheme}://${groupId}`); 28 - const hashedBytes = await crypto.subtle.digest("SHA-256", rawBytes); 29 - return base64url.encode(new Uint8Array(hashedBytes)); 53 + export function groupKey(scheme, groupId) { 54 + return `${scheme}://${groupId}`; 30 55 } 31 56 32 57 /**
+23
src/components/input/https/common.js
··· 1 + import { cachedConsult } from "@components/input/common.js"; 2 + 1 3 /** 2 4 * @import {Track} from "@definitions/types.d.ts" 3 5 */ ··· 236 238 return undefined; 237 239 } 238 240 } 241 + 242 + /** @param {string} uri */ 243 + async function consultHost(uri) { 244 + try { 245 + const controller = new AbortController(); 246 + const timeoutId = setTimeout(() => controller.abort(), 5000); 247 + const response = await fetch(uri, { 248 + method: "HEAD", 249 + signal: controller.signal, 250 + }); 251 + clearTimeout(timeoutId); 252 + return response.ok; 253 + } catch { 254 + return false; 255 + } 256 + } 257 + 258 + export const consultHostCached = cachedConsult( 259 + consultHost, 260 + (uri) => new URL(uri).host, 261 + );
+11 -40
src/components/input/https/worker.js
··· 1 1 import { ostiary, rpc } from "@common/worker.js"; 2 - import { 3 - detach as detachUtil, 4 - groupKeyHash, 5 - } from "@components/input/common.js"; 2 + import { detach as detachUtil, groupKey } from "@components/input/common.js"; 6 3 7 - import { groupTracksByHost, groupUrisByHost, parseURI } from "./common.js"; 4 + import { 5 + consultHostCached, 6 + groupTracksByHost, 7 + groupUrisByHost, 8 + parseURI, 9 + } from "./common.js"; 8 10 import { SCHEME } from "./constants.js"; 9 11 10 12 /** ··· 28 30 return { supported: false, reason: "Invalid HTTPS URL" }; 29 31 } 30 32 31 - // Ping the URL to check if it's reachable 32 - try { 33 - const controller = new AbortController(); 34 - const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout 35 - 36 - const response = await fetch(parsed.url, { 37 - method: "HEAD", 38 - signal: controller.signal, 39 - }); 40 - 41 - clearTimeout(timeoutId); 42 - return { supported: true, consult: response.ok }; 43 - } catch (error) { 44 - return { supported: true, consult: false }; 45 - } 33 + const consult = await consultHostCached(parsed.url); 34 + return { supported: true, consult }; 46 35 } 47 36 48 37 /** ··· 75 64 76 65 const promises = Object.entries(groups).map( 77 66 async ([_domainId, { host, uris }]) => { 78 - // Pick one URI to test reachability 79 67 const testUri = uris[0]; 80 - let available = false; 81 - 82 - if (testUri) { 83 - try { 84 - const controller = new AbortController(); 85 - const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout 86 - 87 - const response = await fetch(testUri, { 88 - method: "HEAD", 89 - signal: controller.signal, 90 - }); 91 - 92 - clearTimeout(timeoutId); 93 - available = response.ok; 94 - } catch { 95 - available = false; 96 - } 97 - } 68 + const available = testUri ? await consultHostCached(testUri) : false; 98 69 99 70 /** @type {ConsultGrouping} */ 100 71 const grouping = available ··· 102 73 : { available, reason: "Host unreachable", scheme: SCHEME, uris }; 103 74 104 75 return { 105 - key: await groupKeyHash(SCHEME, host), 76 + key: groupKey(SCHEME, host), 106 77 grouping, 107 78 }; 108 79 },
+3
src/components/input/opensubsonic/common.js
··· 2 2 import QS from "query-string"; 3 3 4 4 import { SCHEME } from "./constants.js"; 5 + import { cachedConsult } from "@components/input/common.js"; 5 6 import { SubsonicAPIWithoutFetch } from "./class.js"; 6 7 7 8 /** ··· 59 60 const resp = await client.ping().catch(() => undefined); 60 61 return resp?.status?.toLowerCase() === "ok"; 61 62 } 63 + 64 + export const consultServerCached = cachedConsult(consultServer, serverId); 62 65 63 66 /** 64 67 * @param {Server} server
+5 -5
src/components/input/opensubsonic/worker.js
··· 3 3 4 4 import { SCHEME } from "./constants.js"; 5 5 import { removeUndefinedValuesFromRecord } from "@common/utils.js"; 6 - import { detach as detachUtil, groupKeyHash } from "../common.js"; 6 + import { detach as detachUtil, groupKey } from "../common.js"; 7 7 import { 8 8 autoTypeToTrackKind, 9 9 buildURI, 10 - consultServer, 10 + consultServerCached, 11 11 createClient, 12 12 groupTracksByServer, 13 13 groupUrisByServer, ··· 37 37 const parsed = parseURI(fileUriOrScheme); 38 38 if (!parsed) return { supported: true, consult: "undetermined" }; 39 39 40 - const consult = await consultServer(parsed.server); 40 + const consult = await consultServerCached(parsed.server); 41 41 return { supported: true, consult }; 42 42 } 43 43 ··· 71 71 72 72 const promises = Object.entries(groups).map( 73 73 async ([serverId, { server, uris }]) => { 74 - const available = await consultServer(server); 74 + const available = await consultServerCached(server); 75 75 76 76 /** @type {ConsultGrouping} */ 77 77 const grouping = available ··· 79 79 : { available, reason: "Server ping failed", scheme: SCHEME, uris }; 80 80 81 81 return { 82 - key: await groupKeyHash(SCHEME, serverId), 82 + key: groupKey(SCHEME, serverId), 83 83 grouping, 84 84 }; 85 85 },
+3
src/components/input/s3/common.js
··· 3 3 import * as URI from "uri-js"; 4 4 import QS from "query-string"; 5 5 6 + import { cachedConsult } from "@components/input/common.js"; 6 7 import { ENCODINGS, IDB_BUCKETS, SCHEME } from "./constants.js"; 7 8 8 9 /** ··· 66 67 const client = createClient(bucket); 67 68 return await client.bucketExists(bucket.bucketName); 68 69 } 70 + 71 + export const consultBucketCached = cachedConsult(consultBucket, bucketId); 69 72 70 73 /** 71 74 * @param {Bucket} bucket
+5 -5
src/components/input/s3/worker.js
··· 1 1 import { ostiary, rpc } from "@common/worker.js"; 2 2 import { 3 3 detach as detachUtil, 4 - groupKeyHash, 4 + groupKey, 5 5 isAudioFile, 6 6 } from "@components/input/common.js"; 7 7 import { 8 8 bucketId, 9 9 buildURI, 10 - consultBucket, 10 + consultBucketCached, 11 11 createClient, 12 12 groupTracksByBucket, 13 13 groupUrisByBucket, ··· 36 36 const parsed = parseURI(fileUriOrScheme); 37 37 if (!parsed) return { supported: true, consult: "undetermined" }; 38 38 39 - const consult = await consultBucket(parsed.bucket); 39 + const consult = await consultBucketCached(parsed.bucket); 40 40 return { supported: true, consult }; 41 41 } 42 42 ··· 70 70 71 71 const promises = Object.entries(groups).map( 72 72 async ([bucketId, { bucket, uris }]) => { 73 - const available = await consultBucket(bucket); 73 + const available = await consultBucketCached(bucket); 74 74 75 75 /** @type {ConsultGrouping} */ 76 76 const grouping = available ··· 78 78 : { available, reason: "Bucket unavailable", scheme: SCHEME, uris }; 79 79 80 80 return { 81 - key: await groupKeyHash(SCHEME, bucketId), 81 + key: groupKey(SCHEME, bucketId), 82 82 grouping, 83 83 }; 84 84 },
+3 -2
src/components/orchestrator/scoped-tracks/element.js
··· 99 99 await customElements.whenDefined(output.localName); 100 100 if (scope) await customElements.whenDefined(scope.localName); 101 101 102 - const startTime = performance.now(); 102 + let startTime = performance.now(); 103 103 104 104 // Watch tracks collection 105 105 this.effect(async () => { 106 106 const collection = output.tracks.collection(); 107 107 if ((await this.isLeader()) === false) return; 108 108 109 - console.log("🫠", collection.length); 109 + const endTime = performance.now(); 110 + console.log("🫠", collection.length, endTime - startTime); 110 111 111 112 // Consult input 112 113 const groups = await input.groupConsult(