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.

fix: freezes

+92 -66
+2
src/components/output/bytes/s3/element.js
··· 56 56 get: () => this.#get("tracks"), 57 57 put: (data) => this.#put("tracks", data), 58 58 }, 59 + }, { 60 + eager: true, 59 61 }); 60 62 61 63 this.facets = this.#manager.facets;
+26 -10
src/components/output/common.js
··· 1 - import { computed, signal, untracked } from "@common/signal.js"; 1 + import { batch, computed, signal, untracked } from "@common/signal.js"; 2 2 3 3 /** 4 4 * @import {Facet, PlaylistItem, Theme, Track} from "@definitions/types.d.ts" ··· 8 8 /** 9 9 * @template [Encoding=null] 10 10 * @param {OutputManagerProperties<Encoding>} _ 11 + * @param {{ eager?: boolean }} [signalOpts] 11 12 * @returns {OutputManager<Encoding>} 12 13 */ 13 - export function outputManager({ init, facets, playlistItems, themes, tracks }) { 14 + export function outputManager( 15 + { init, facets, playlistItems, themes, tracks }, 16 + signalOpts, 17 + ) { 14 18 const c = signal( 15 19 /** @type {Encoding extends null ? Facet[] : Encoding} */ (facets 16 20 .empty()), 21 + { eager: signalOpts?.eager ?? false }, 17 22 ); 18 23 const cs = signal( 19 24 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"), ··· 22 27 const pl = signal( 23 28 /** @type {Encoding extends null ? PlaylistItem[] : Encoding} */ (playlistItems 24 29 .empty()), 30 + { eager: signalOpts?.eager ?? false }, 25 31 ); 26 32 const pls = signal( 27 33 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"), ··· 29 35 30 36 const th = signal( 31 37 /** @type {Encoding extends null ? Theme[] : Encoding} */ (themes.empty()), 38 + { eager: signalOpts?.eager ?? false }, 32 39 ); 33 40 const ths = signal( 34 41 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"), ··· 36 43 37 44 const t = signal( 38 45 /** @type {Encoding extends null ? Track[] : Encoding} */ (tracks.empty()), 46 + { eager: signalOpts?.eager ?? false }, 39 47 ); 40 48 const ts = signal( 41 49 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"), ··· 77 85 }), 78 86 reload: loadFacets, 79 87 save: async (newFacets) => { 80 - if (untracked(() => cs.value === "sleeping")) loadFacets(); 81 - c.value = newFacets; 88 + batch(() => { 89 + if (untracked(() => cs.value === "sleeping")) cs.value = "loaded"; 90 + c.value = newFacets; 91 + }); 82 92 await facets.put(newFacets); 83 93 }, 84 94 state: cs.get, ··· 90 100 }), 91 101 reload: loadPlaylistItems, 92 102 save: async (newPlaylistItems) => { 93 - if (untracked(() => pls.value === "sleeping")) loadPlaylistItems(); 94 - pl.value = newPlaylistItems; 103 + batch(() => { 104 + if (untracked(() => pls.value === "sleeping")) pls.value = "loaded"; 105 + pl.value = newPlaylistItems; 106 + }); 95 107 await playlistItems.put(newPlaylistItems); 96 108 }, 97 109 state: pls.get, ··· 103 115 }), 104 116 reload: loadThemes, 105 117 save: async (newThemes) => { 106 - if (untracked(() => ths.value === "sleeping")) loadThemes(); 107 - th.value = newThemes; 118 + batch(() => { 119 + if (untracked(() => ths.value === "sleeping")) ths.value = "loaded"; 120 + th.value = newThemes; 121 + }); 108 122 await themes.put(newThemes); 109 123 }, 110 124 state: ths.get, ··· 116 130 }), 117 131 reload: loadTracks, 118 132 save: async (newTracks) => { 119 - if (untracked(() => ts.value === "sleeping")) loadTracks(); 120 - t.value = newTracks; 133 + batch(() => { 134 + if (untracked(() => ts.value === "sleeping")) ts.value = "loaded"; 135 + t.value = newTracks; 136 + }); 121 137 await tracks.put(newTracks); 122 138 }, 123 139 state: ts.get,
+3 -1
src/components/transformer/output/base.js
··· 11 11 export class OutputTransformer extends BroadcastableDiffuseElement { 12 12 // SIGNALS 13 13 14 - #output = signal(/** @type {OutputElement<T> | undefined} */ (undefined)); 14 + #output = signal(/** @type {OutputElement<T> | undefined} */ (undefined), { 15 + eager: true, 16 + }); 15 17 #outputWhenDefined = Promise.withResolvers(); 16 18 17 19 output = {
+61 -55
src/components/transformer/output/bytes/dasl-sync/element.js
··· 5 5 import "@components/output/polymorphic/indexed-db/element.js"; 6 6 7 7 import * as CID from "@common/cid.js"; 8 - import { computed, signal } from "@common/signal.js"; 8 + import { computed, signal, untracked } from "@common/signal.js"; 9 9 import { compareTimestamps } from "@common/utils.js"; 10 10 import { OutputTransformer } from "../../base.js"; 11 11 import { IDB_PREFIX } from "./constants.js"; ··· 15 15 * @import { Container } from "./types.d.ts" 16 16 */ 17 17 18 + /** @type {Container<any>} */ 19 + const EMPTY = { 20 + cid: undefined, 21 + data: [], 22 + inventory: { current: {}, removed: [] }, 23 + }; 24 + 18 25 /** 19 26 * @extends {OutputTransformer<Uint8Array>} 20 27 */ ··· 30 37 * @param {SignalReader<Uint8Array | undefined>} localCollection 31 38 * @param {SignalReader<Uint8Array | undefined>} remoteCollection 32 39 * @param {SignalReader<"loading" | "loaded" | "sleeping">} remoteState 33 - * @param {{ saveLocal: (bytes: Uint8Array) => void, saveRemote: (bytes: Uint8Array) => Promise<void> }} sync 40 + * @param {{ saveLocal: (bytes: Uint8Array) => Promise<void>; saveRemote: (bytes: Uint8Array) => Promise<void> }} sync 34 41 */ 35 42 const state = ( 36 43 kind, 37 44 localCollection, 38 45 remoteCollection, 39 46 remoteState, 40 - sync, 47 + { saveLocal, saveRemote }, 41 48 ) => { 42 - /** 43 - * @typedef {Container<T>} State 44 - */ 49 + const container = signal(/** @type {Container<T>} */ (EMPTY), { 50 + eager: true, 51 + }); 45 52 46 - /** @returns {State} */ 47 - const determine = () => { 53 + const isReady = signal(false); 54 + 55 + let isMerging = false; 56 + 57 + this.effect(() => { 58 + if (!isReady.value) return; 59 + if (isMerging) return; 60 + 48 61 const lb = localCollection(); 49 62 const rb = remote.ready() ? remoteCollection() : undefined; 50 63 const rs = remoteState(); ··· 57 70 58 71 if (!r) { 59 72 if (l) { 73 + container.value = l; 74 + 60 75 if (remote.ready() && rs === "loaded") { 61 76 const bytes = this.save(l); 62 - sync.saveRemote(bytes); 77 + saveRemote(bytes); 63 78 } 64 - 65 - return l; 66 79 } 80 + } else if (!l) { 81 + container.value = r; 67 82 68 - return { 69 - cid: undefined, 70 - data: [], 71 - inventory: { current: {}, removed: [] }, 72 - }; 73 - } else if (!l) { 74 83 const bytes = this.save(r); 75 - sync.saveLocal(bytes); 76 - return r; 77 - } 84 + saveLocal(bytes); 85 + } else { 86 + container.value = l; 78 87 79 - const diverged = this.hasDiverged({ local: l, remote: r }); 88 + if (this.hasDiverged({ local: l, remote: r })) { 89 + isMerging = true; 80 90 81 - if (diverged.local || diverged.remote) { 82 - this.merge(l, r).then((c) => { 83 - console.log("Merged:", c); 84 - const bytes = this.save(c); 85 - if (diverged.local) sync.saveLocal(bytes); 86 - if (diverged.remote) sync.saveRemote(bytes); 87 - }); 91 + this.merge(l, r).then(async (c) => { 92 + container.value = c; 93 + 94 + const bytes = this.save(c); 95 + await saveLocal(bytes); 96 + 97 + if (remote.ready() && rs === "loaded") { 98 + await saveRemote(bytes); 99 + } 100 + 101 + isMerging = false; 102 + }); 103 + } 88 104 } 105 + }); 89 106 90 - return l; 91 - }; 92 - 93 - return computed(determine); 107 + return computed(() => { 108 + if (!untracked(isReady.get)) isReady.value = true; 109 + return container.get(); 110 + }); 94 111 }; 95 112 96 113 // Local ··· 235 252 /** 236 253 * @template {{ id: string; updatedAt: string }} T 237 254 * @param {{ local: Container<T>, remote: Container<T> }} _ 238 - * @returns {{ local: boolean, remote: boolean }} Which store needs updating? 239 255 */ 240 256 hasDiverged({ local, remote }) { 241 - const diverged = local.cid !== remote.cid; 242 - 243 - if (!diverged) { 244 - return { 245 - local: false, 246 - remote: false, 247 - }; 248 - } 249 - 250 - // TODO: Could be improved. 251 - // We might not need to save on both ends. 252 - return { 253 - local: true, 254 - remote: true, 255 - }; 257 + return local.cid !== remote.cid; 256 258 } 257 259 258 260 /** ··· 262 264 * @returns {Promise<Container<T>>} 263 265 */ 264 266 async merge(a, b) { 265 - console.log("MERGE", a, b); 267 + console.log("Merging:", a, b); 266 268 267 269 const removedA = new Set(a.inventory.removed); 268 270 const removedB = new Set(b.inventory.removed); ··· 298 300 const itemB = mapB.get(id); 299 301 300 302 if (!itemA || !itemB) { 301 - console.warn("Should have found item but didn't!"); 303 + console.warn("Should have found both items but didn't!"); 304 + continue; 305 + } 306 + 307 + // Items are identical, no merge or CID recomputation needed 308 + if (currentA[id] === currentB[id]) { 309 + data.push(itemA); 310 + current[id] = currentA[id]; 302 311 continue; 303 312 } 304 313 ··· 364 373 managerProp(local, remote, container) { 365 374 return { 366 375 collection: computed(() => { 367 - return container().data; 376 + return container()?.data ?? []; 368 377 }), 369 378 reload: remote.reload, 370 379 save: async (/** @type {T[]} */ newItems) => { ··· 373 382 previous: container(), 374 383 }); 375 384 376 - console.log("Save:", newItems); 377 385 const bytes = this.save(adjustedContainer); 378 - 379 - console.log("Bytes:", bytes); 380 386 await local.save(bytes); 381 387 }, 382 388 state: computed(() => { 383 - if (container().cid) return "loaded"; 389 + if (container()?.cid) return "loaded"; 384 390 return "loading"; 385 391 }), 386 392 };