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.

feat: refactor output element types to eliminate impossible states

+355 -309
+4 -3
src/common/loader.js
··· 24 24 * @typedef {object} LoaderConfig 25 25 * @property {string} $type - The atproto $type 26 26 * @property {string} label - Human-readable label for error messages (e.g. "Facet", "Theme") 27 - * @property {() => { collection: SignalReader<LoadableItem[]>; state: SignalReader<"loading" | "loaded" | "sleeping"> }} source - The collection source 27 + * @property {() => { collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: LoadableItem[] }> }} source - The collection source 28 28 * @property {(item: LoadableItem) => void} render - Renders the loaded item 29 29 */ 30 30 ··· 79 79 loader = "uri"; 80 80 } else { 81 81 const source = config.source(); 82 - const collection = source.collection(); 83 - if (source.state() !== "loaded") return; 82 + const col = source.collection(); 83 + if (col.state !== "loaded") return; 84 + const collection = col.data; 84 85 85 86 if (id) { 86 87 item = collection.find((c) => c.id === id);
+8 -6
src/common/output.js
··· 5 5 */ 6 6 7 7 /** 8 - * @param {{ collection: SignalReader<any>; state: SignalReader<"loading" | "loaded" | "sleeping"> }} output 8 + * @template T 9 + * @param {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: T }> }} output 10 + * @returns {Promise<T>} 9 11 */ 10 - export async function waitUntilLoaded(output) { 12 + export async function data(output) { 11 13 return await new Promise((resolve) => { 12 14 let resolved = false; 13 15 ··· 17 19 return; 18 20 } 19 21 20 - if (output.state() === "loaded") { 22 + const col = output.collection(); 23 + 24 + if (col.state === "loaded") { 21 25 resolved = true; 22 - resolve(void 0); 23 - } else if (output.state() === "sleeping") { 24 - output.collection(); 26 + resolve(col.data); 25 27 } 26 28 }); 27 29 });
+12 -40
src/components/configurator/output/element.js
··· 38 38 const def = this.#defaultOutput.value; 39 39 if (def) return def.facets.collection(); 40 40 41 - return this.#memory.facets.value; 41 + return this.#setupFinished.value 42 + ? { state: "loaded", data: this.#memory.facets.value } 43 + : { state: "loading" }; 42 44 }), 43 45 reload: () => { 44 46 const def = this.#defaultOutput.value; ··· 58 60 59 61 this.#memory.facets.value = newFacets; 60 62 }, 61 - state: computed(() => { 62 - const out = this.#selected.value; 63 - if (out) return out.facets.state(); 64 - 65 - const def = this.#defaultOutput.value; 66 - if (def) return def.facets.state(); 67 - 68 - return this.#setupFinished.value ? "loaded" : "sleeping"; 69 - }), 70 63 }, 71 64 playlistItems: { 72 65 collection: computed(() => { ··· 76 69 const def = this.#defaultOutput.value; 77 70 if (def) return def.playlistItems.collection(); 78 71 79 - return this.#memory.playlistItems.value; 72 + return this.#setupFinished.value 73 + ? { state: "loaded", data: this.#memory.playlistItems.value } 74 + : { state: "loading" }; 80 75 }), 81 76 reload: () => { 82 77 const def = this.#defaultOutput.value; ··· 96 91 97 92 this.#memory.playlistItems.value = newPlaylistItems; 98 93 }, 99 - state: computed(() => { 100 - const out = this.#selected.value; 101 - if (out) return out.playlistItems.state(); 102 - 103 - const def = this.#defaultOutput.value; 104 - if (def) return def.playlistItems.state(); 105 - 106 - return this.#setupFinished.value ? "loaded" : "sleeping"; 107 - }), 108 94 }, 109 95 themes: { 110 96 collection: computed(() => { ··· 114 100 const def = this.#defaultOutput.value; 115 101 if (def) return def.themes.collection(); 116 102 117 - return this.#memory.themes.value; 103 + return this.#setupFinished.value 104 + ? { state: "loaded", data: this.#memory.themes.value } 105 + : { state: "loading" }; 118 106 }), 119 107 reload: () => { 120 108 const def = this.#defaultOutput.value; ··· 134 122 135 123 this.#memory.themes.value = newThemes; 136 124 }, 137 - state: computed(() => { 138 - const out = this.#selected.value; 139 - if (out) return out.themes.state(); 140 - 141 - const def = this.#defaultOutput.value; 142 - if (def) return def.themes.state(); 143 - 144 - return this.#setupFinished.value ? "loaded" : "sleeping"; 145 - }), 146 125 }, 147 126 tracks: { 148 127 collection: computed(() => { ··· 152 131 const def = this.#defaultOutput.value; 153 132 if (def) return def.tracks.collection(); 154 133 155 - return this.#memory.tracks.value; 134 + return this.#setupFinished.value 135 + ? { state: "loaded", data: this.#memory.tracks.value } 136 + : { state: "loading" }; 156 137 }), 157 138 reload: () => { 158 139 const def = this.#defaultOutput.value; ··· 172 153 173 154 this.#memory.tracks.value = newTracks; 174 155 }, 175 - state: computed(() => { 176 - const out = this.#selected.value; 177 - if (out) return out.tracks.state(); 178 - 179 - const def = this.#defaultOutput.value; 180 - if (def) return def.tracks.state(); 181 - 182 - return this.#setupFinished.value ? "loaded" : "sleeping"; 183 - }), 184 156 }, 185 157 186 158 // Other
+9 -5
src/components/orchestrator/favourites/element.js
··· 45 45 const output = this.#output.value; 46 46 if (!output) return []; 47 47 48 - const playlistItems = output.playlistItems.collection(); 49 - return filterFavourites(playlistItems ?? []); 48 + const col = output.playlistItems.collection(); 49 + if (col.state !== "loaded") return []; 50 + return filterFavourites(col.data); 50 51 }); 51 52 52 53 // LIFECYCLE ··· 98 99 return; 99 100 } 100 101 101 - const playlistItems = output.playlistItems.collection(); 102 + const col = output.playlistItems.collection(); 103 + const playlistItems = col.state === "loaded" ? col.data : []; 102 104 const result = await this.#proxy.include({ 103 105 playlistItems, 104 106 tracks: tracksArray, ··· 121 123 return; 122 124 } 123 125 124 - const playlistItems = output.playlistItems.collection(); 126 + const col = output.playlistItems.collection(); 127 + const playlistItems = col.state === "loaded" ? col.data : []; 125 128 const result = await this.#proxy.expel({ 126 129 playlistItems, 127 130 tracks: tracksArray, ··· 145 148 return; 146 149 } 147 150 148 - const playlistItems = output.playlistItems.collection(); 151 + const col = output.playlistItems.collection(); 152 + const playlistItems = col.state === "loaded" ? col.data : []; 149 153 const result = await this.#proxy.toggle({ 150 154 playlistItems, 151 155 tracks: tracksArray,
+3 -2
src/components/orchestrator/media-session/element.js
··· 71 71 if (!this.queue) return; 72 72 73 73 const now = this.queue.now(); 74 - const track = now && this.output 75 - ? this.output.tracks.collection().find((t) => t.id === now.id) 74 + const tracksCol = this.output?.tracks.collection(); 75 + const track = now && tracksCol?.state === "loaded" 76 + ? tracksCol.data.find((t) => t.id === now.id) 76 77 : undefined; 77 78 78 79 if (!track) {
+5 -3
src/components/orchestrator/path-collections/element.js
··· 18 18 const base = this.base(); 19 19 20 20 const ephemeralItems = computed(() => { 21 - return createEphemeralItems(base.tracks.collection() ?? []); 21 + const col = base.tracks.collection(); 22 + return createEphemeralItems(col.state === "loaded" ? col.data : []); 22 23 }); 23 24 24 25 this.facets = base.facets; 25 26 this.playlistItems = { 26 27 ...base.playlistItems, 27 28 collection: computed(() => { 28 - const stored = base.playlistItems.collection() ?? []; 29 - return [...stored, ...ephemeralItems()]; 29 + const col = base.playlistItems.collection(); 30 + if (col.state !== "loaded") return col; 31 + return { state: "loaded", data: [...col.data, ...ephemeralItems()] }; 30 32 }), 31 33 /** @type {(typeof base.playlistItems.save)} */ 32 34 save: async (items) => {
+4 -3
src/components/orchestrator/process-tracks/element.js
··· 110 110 // unless already done so (possibly through another instance of this element) 111 111 if (this.hasAttribute("process-when-ready")) { 112 112 const unregister = this.effect(() => { 113 - const state = output.tracks.state(); 114 - if (state !== "loaded") return; 113 + const col = output.tracks.collection(); 114 + if (col.state !== "loaded") return; 115 115 116 116 if (this.#performedInitialProcess.value) { 117 117 unregister(); ··· 157 157 this.#isProcessing.value = true; 158 158 console.log("🪵 Processing initiated"); 159 159 160 - const cachedTracks = this.output.tracks.collection(); 160 + const tracksCol = this.output.tracks.collection(); 161 + const cachedTracks = tracksCol.state === "loaded" ? tracksCol.data : []; 161 162 const result = await this.#proxy.process(cachedTracks); 162 163 163 164 // Save if collection changed
+2 -1
src/components/orchestrator/queue-audio/element.js
··· 74 74 75 75 const activeItem = queue.now(); 76 76 const nextItem = queue.future()[0] ?? null; 77 - const tracks = this.output?.tracks.collection(); 77 + const tracksCol = this.output?.tracks.collection(); 78 + const tracks = tracksCol?.state === "loaded" ? tracksCol.data : undefined; 78 79 79 80 const activeTrack = activeItem 80 81 ? tracks?.find((t) => t.id === activeItem.id)
+7 -6
src/components/orchestrator/scoped-tracks/element.js
··· 36 36 const playlist = this.#scope.value?.playlist(); 37 37 if (!playlist) return undefined; 38 38 39 - return this.#output.value?.playlistItems.collection().filter((p) => 40 - p.playlist === playlist 41 - ); 39 + const col = this.#output.value?.playlistItems.collection(); 40 + if (!col || col.state !== "loaded") return undefined; 41 + return col.data.filter((p) => p.playlist === playlist); 42 42 }); 43 43 44 44 #tracksAvailable = signal(/** @type {Track[]} */ ([])); ··· 132 132 133 133 // Watch tracks collection 134 134 this.effect(async () => { 135 - const collection = output.tracks.collection(); 135 + const tracksCol = output.tracks.collection(); 136 136 if ((await this.isLeader()) === false) return; 137 + if (tracksCol.state !== "loaded") return; 137 138 138 139 /** @type {string[]} */ 139 140 const uris = []; 140 - const tracks = collection.filter((t) => { 141 + const tracks = tracksCol.data.filter((t) => { 141 142 uris.push(t.uri); 142 143 return t.kind !== "placeholder"; 143 144 }); 144 145 145 146 // Consult inputs 146 - const groups = collection.length ? await input.groupConsult(uris) : {}; 147 + const groups = tracksCol.data.length ? await input.groupConsult(uris) : {}; 147 148 148 149 /** @type {Set<string>} */ 149 150 const availableUris = new Set();
+2 -1
src/components/orchestrator/sources/element.js
··· 48 48 49 49 // Effects 50 50 this.effect(() => { 51 - const tracks = output.tracks.collection(); 51 + const col = output.tracks.collection(); 52 + const tracks = col.state === "loaded" ? col.data : []; 52 53 const groups = groupTracksPerScheme(tracks); 53 54 54 55 /** @type {{ [scheme: string]: Source[] }} */
+12 -15
src/components/output/common.js
··· 141 141 facets: { 142 142 collection: computed(() => { 143 143 if (untracked(() => cs.value === "sleeping")) loadFacets(); 144 - return c.value; 144 + return cs.value === "loading" 145 + ? { state: "loading" } 146 + : { state: "loaded", data: c.value }; 145 147 }), 146 148 reload: loadFacets, 147 149 save: async (newFacets) => { ··· 151 153 }); 152 154 await facets.put(newFacets); 153 155 }, 154 - state: cs.get, 155 156 }, 156 157 playlistItems: { 157 158 collection: computed(() => { 158 159 if (untracked(() => pls.value === "sleeping")) loadPlaylistItems(); 159 - return pl.value; 160 + return pls.value === "loading" 161 + ? { state: "loading" } 162 + : { state: "loaded", data: pl.value }; 160 163 }), 161 164 reload: loadPlaylistItems, 162 165 save: async (newPlaylistItems) => { ··· 166 169 }); 167 170 await playlistItems.put(newPlaylistItems); 168 171 }, 169 - state: pls.get, 170 172 }, 171 173 themes: { 172 174 collection: computed(() => { 173 175 if (untracked(() => ths.value === "sleeping")) loadThemes(); 174 - return th.value; 176 + return ths.value === "loading" 177 + ? { state: "loading" } 178 + : { state: "loaded", data: th.value }; 175 179 }), 176 180 reload: loadThemes, 177 181 save: async (newThemes) => { ··· 181 185 }); 182 186 await themes.put(newThemes); 183 187 }, 184 - state: ths.get, 185 188 }, 186 189 tracks: { 187 190 collection: computed(() => { 188 191 if (untracked(() => ts.value === "sleeping")) loadTracks(); 189 - return t.value; 192 + return ts.value === "loading" 193 + ? { state: "loading" } 194 + : { state: "loaded", data: t.value }; 190 195 }), 191 196 reload: loadTracks, 192 197 save: async (newTracks) => { ··· 196 201 }); 197 202 await tracks.put(newTracks); 198 203 }, 199 - state: ts.get, 200 204 }, 201 205 signals: { 202 206 facets: c, 203 207 playlistItems: pl, 204 208 themes: th, 205 209 tracks: t, 206 - 207 - states: { 208 - facets: cs, 209 - playlistItems: pls, 210 - themes: ths, 211 - tracks: ts, 212 - }, 213 210 }, 214 211 }; 215 212 }
+16 -15
src/components/output/types.d.ts
··· 17 17 18 18 export type OutputManager<Encoding = null> = { 19 19 facets: { 20 - collection: SignalReader<Encoding extends null ? Facet[] : Encoding>; 20 + collection: SignalReader< 21 + | { state: "loading" } 22 + | { state: "loaded"; data: Encoding extends null ? Facet[] : Encoding } 23 + >; 21 24 reload: () => Promise<void>; 22 25 save: ( 23 26 facets: Encoding extends null ? Facet[] : Encoding, 24 27 ) => Promise<void>; 25 - state: SignalReader<"loading" | "loaded" | "sleeping">; 26 28 }; 27 29 playlistItems: { 28 - collection: SignalReader<Encoding extends null ? PlaylistItem[] : Encoding>; 30 + collection: SignalReader< 31 + | { state: "loading" } 32 + | { state: "loaded"; data: Encoding extends null ? PlaylistItem[] : Encoding } 33 + >; 29 34 reload: () => Promise<void>; 30 35 save: ( 31 36 playlistItems: Encoding extends null ? PlaylistItem[] : Encoding, 32 37 ) => Promise<void>; 33 - state: SignalReader<"loading" | "loaded" | "sleeping">; 34 38 }; 35 39 signals: { 36 40 facets: Signal<Encoding extends null ? Facet[] : Encoding>; 37 41 playlistItems: Signal<Encoding extends null ? PlaylistItem[] : Encoding>; 38 42 themes: Signal<Encoding extends null ? Theme[] : Encoding>; 39 43 tracks: Signal<Encoding extends null ? Track[] : Encoding>; 40 - 41 - states: { 42 - facets: Signal<"loading" | "loaded" | "sleeping">; 43 - playlistItems: Signal<"loading" | "loaded" | "sleeping">; 44 - themes: Signal<"loading" | "loaded" | "sleeping">; 45 - tracks: Signal<"loading" | "loaded" | "sleeping">; 46 - }; 47 44 }; 48 45 themes: { 49 - collection: SignalReader<Encoding extends null ? Theme[] : Encoding>; 46 + collection: SignalReader< 47 + | { state: "loading" } 48 + | { state: "loaded"; data: Encoding extends null ? Theme[] : Encoding } 49 + >; 50 50 reload: () => Promise<void>; 51 51 save: ( 52 52 themes: Encoding extends null ? Theme[] : Encoding, 53 53 ) => Promise<void>; 54 - state: SignalReader<"loading" | "loaded" | "sleeping">; 55 54 }; 56 55 tracks: { 57 - collection: SignalReader<Encoding extends null ? Track[] : Encoding>; 56 + collection: SignalReader< 57 + | { state: "loading" } 58 + | { state: "loaded"; data: Encoding extends null ? Track[] : Encoding } 59 + >; 58 60 reload: () => Promise<void>; 59 61 save: (tracks: Encoding extends null ? Track[] : Encoding) => Promise<void>; 60 - state: SignalReader<"loading" | "loaded" | "sleeping">; 61 62 }; 62 63 }; 63 64
+9 -21
src/components/transformer/output/base.js
··· 40 40 // MANAGER 41 41 42 42 base() { 43 - /** @type {OutputManagerDeputy<T | undefined>} */ 43 + /** @type {OutputManagerDeputy<T>} */ 44 44 const m = { 45 45 facets: { 46 46 collection: computed(() => { 47 - return this.output.signal()?.facets?.collection(); 47 + return this.output.signal()?.facets?.collection() ?? 48 + { state: "loading" }; 48 49 }), 49 50 reload: () => { 50 51 return this.output.signal()?.facets?.reload() ?? 51 52 Promise.resolve(); 52 53 }, 53 54 save: async (newFacets) => { 54 - if (newFacets === undefined) return; 55 55 await this.output.whenDefined; 56 56 await this.output.signal()?.facets.save(newFacets); 57 57 }, 58 - state: computed(() => { 59 - return this.output.signal()?.facets.state() ?? "sleeping"; 60 - }), 61 58 }, 62 59 playlistItems: { 63 60 collection: computed(() => { 64 - return this.output.signal()?.playlistItems?.collection(); 61 + return this.output.signal()?.playlistItems?.collection() ?? 62 + { state: "loading" }; 65 63 }), 66 64 reload: () => { 67 65 return this.output.signal()?.playlistItems?.reload() ?? 68 66 Promise.resolve(); 69 67 }, 70 68 save: async (newPlaylistItems) => { 71 - if (newPlaylistItems === undefined) return; 72 69 await this.output.whenDefined; 73 70 await this.output.signal()?.playlistItems.save(newPlaylistItems); 74 71 }, 75 - state: computed(() => { 76 - return this.output.signal()?.playlistItems.state() ?? "sleeping"; 77 - }), 78 72 }, 79 73 themes: { 80 74 collection: computed(() => { 81 - return this.output.signal()?.themes?.collection(); 75 + return this.output.signal()?.themes?.collection() ?? 76 + { state: "loading" }; 82 77 }), 83 78 reload: () => { 84 79 return this.output.signal()?.themes?.reload() ?? 85 80 Promise.resolve(); 86 81 }, 87 82 save: async (newThemes) => { 88 - if (newThemes === undefined) return; 89 83 await this.output.whenDefined; 90 84 await this.output.signal()?.themes.save(newThemes); 91 85 }, 92 - state: computed(() => { 93 - return this.output.signal()?.themes.state() ?? "sleeping"; 94 - }), 95 86 }, 96 87 tracks: { 97 88 collection: computed(() => { 98 - return this.output.signal()?.tracks?.collection(); 89 + return this.output.signal()?.tracks?.collection() ?? 90 + { state: "loading" }; 99 91 }), 100 92 reload: () => { 101 93 return this.output.signal()?.tracks?.reload() ?? Promise.resolve(); 102 94 }, 103 95 save: async (newTracks) => { 104 - if (newTracks === undefined) return; 105 96 await this.output.whenDefined; 106 97 await this.output.signal()?.tracks.save(newTracks); 107 98 }, 108 - state: computed(() => { 109 - return this.output.signal()?.tracks.state() ?? "sleeping"; 110 - }), 111 99 }, 112 100 113 101 // Other non-data related state
+53 -25
src/components/transformer/output/bytes/automerge/element.js
··· 35 35 36 36 /** 37 37 * @template T 38 - * @param {SignalReader<Uint8Array | undefined>} localCollection 39 - * @param {SignalReader<Uint8Array | undefined>} remoteCollection 38 + * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} localCollection 39 + * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} remoteCollection 40 40 * @param {Automerge.Doc<T>} initial 41 - * @returns {SignalReader<{ doc: Automerge.Doc<T>; diverged: boolean; local: boolean; remote: boolean; }>} 41 + * @returns {SignalReader<{ doc: Automerge.Doc<T>; diverged: boolean; local: boolean; remote: boolean; remoteLoaded: boolean; }>} 42 42 */ 43 43 const state = (localCollection, remoteCollection, initial) => 44 44 computed(() => { 45 - const l = loadDocument(localCollection); 46 - const r = remote.ready() ? loadDocument(remoteCollection) : undefined; 45 + const lc = localCollection(); 46 + const rc = remote.ready() ? remoteCollection() : undefined; 47 + 48 + const l = loadDocument(lc?.state === "loaded" ? lc.data : undefined); 49 + const r = rc?.state === "loaded" ? loadDocument(rc.data) : undefined; 50 + const remoteLoaded = rc?.state === "loaded"; 47 51 48 52 if (!r) { 49 53 return l 50 - ? { doc: l, diverged: true, local: false, remote: true } 51 - : { doc: initial, diverged: false, local: false, remote: false }; 54 + ? { 55 + doc: l, 56 + diverged: true, 57 + local: false, 58 + remote: true, 59 + remoteLoaded, 60 + } 61 + : { 62 + doc: initial, 63 + diverged: false, 64 + local: false, 65 + remote: false, 66 + remoteLoaded, 67 + }; 52 68 } else if (!l) { 53 - return { doc: r, diverged: true, local: true, remote: false }; 69 + return { 70 + doc: r, 71 + diverged: true, 72 + local: true, 73 + remote: false, 74 + remoteLoaded, 75 + }; 54 76 } 55 77 56 78 const lh = Automerge.getHeads(l)[0]; ··· 64 86 diverged, 65 87 local: Automerge.hasHeads(r, [lh]), 66 88 remote: Automerge.hasHeads(l, [rh]), 89 + remoteLoaded, 67 90 }; 68 91 }); 69 92 70 93 const facets = state( 71 - computed(() => local()?.facets?.collection()), 94 + computed(() => local()?.facets?.collection() ?? { state: "loading" }), 72 95 remote.facets.collection, 73 96 INITIAL_FACETS_DOCUMENT, 74 97 ); 75 98 76 99 const playlistItems = state( 77 - computed(() => local()?.playlistItems?.collection()), 100 + computed(() => 101 + local()?.playlistItems?.collection() ?? { state: "loading" } 102 + ), 78 103 remote.playlistItems.collection, 79 104 INITIAL_PLAYLIST_ITEMS_DOCUMENT, 80 105 ); 81 106 82 107 const themes = state( 83 - computed(() => local()?.themes?.collection()), 108 + computed(() => local()?.themes?.collection() ?? { state: "loading" }), 84 109 remote.themes.collection, 85 110 INITIAL_THEMES_DOCUMENT, 86 111 ); 87 112 88 113 const tracks = state( 89 - computed(() => local()?.tracks?.collection()), 114 + computed(() => local()?.tracks?.collection() ?? { state: "loading" }), 90 115 remote.tracks.collection, 91 116 INITIAL_TRACKS_DOCUMENT, 92 117 ); ··· 129 154 if (!l) return; 130 155 131 156 this.effect(() => { 132 - if (remote.facets.state() !== "loaded") return; 157 + if (!facets().remoteLoaded) return; 133 158 const s = facets(); 134 159 if (s.diverged) { 135 160 const bytes = Automerge.save(s.doc); ··· 139 164 }); 140 165 141 166 this.effect(() => { 142 - if (remote.playlistItems.state() !== "loaded") return; 167 + if (!playlistItems().remoteLoaded) return; 143 168 const s = playlistItems(); 144 169 if (s.diverged) { 145 170 const bytes = Automerge.save(s.doc); ··· 149 174 }); 150 175 151 176 this.effect(() => { 152 - if (remote.themes.state() !== "loaded") return; 177 + if (!themes().remoteLoaded) return; 153 178 const s = themes(); 154 179 if (s.diverged) { 155 180 const bytes = Automerge.save(s.doc); ··· 159 184 }); 160 185 161 186 this.effect(() => { 162 - if (remote.tracks.state() !== "loaded") return; 187 + if (!tracks().remoteLoaded) return; 163 188 const s = tracks(); 164 189 if (s.diverged) { 165 190 const bytes = Automerge.save(s.doc); ··· 216 241 217 242 /** 218 243 * @template T 219 - * @param {SignalReader<Uint8Array | undefined>} source 244 + * @param {Uint8Array | undefined} value 220 245 * @returns {Automerge.Doc<T> | undefined} 221 246 */ 222 - export function loadDocument(source) { 223 - const value = source(); 224 - 247 + export function loadDocument(value) { 225 248 if (isUint8Array(value)) { 226 249 return Automerge.load(value); 227 250 } else if (value == undefined) { ··· 233 256 234 257 /** 235 258 * @template {Record<string, any>} T 236 - * @param {SignalReader<{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> } | undefined>} local 237 - * @param {{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} remote 259 + * @param {SignalReader<{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void> } | undefined>} local 260 + * @param {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void> }} remote 238 261 * @param {SignalReader<Automerge.Doc<{ collection: T[] }>>} document 239 262 * @param {{ stripUndefined?: boolean }} [opts] 240 - * @returns {{ collection: SignalReader<T[]>, reload: () => Promise<void>, save: (items: T[]) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} 263 + * @returns {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: T[] }>, reload: () => Promise<void>, save: (items: T[]) => Promise<void> }} 241 264 */ 242 265 export function automergeEntry(local, remote, document, opts) { 243 266 return { 244 - collection: computed(() => document().collection), 267 + collection: computed(() => { 268 + const col = local()?.collection(); 269 + if (!col || col.state !== "loaded") { 270 + return { state: col?.state ?? "loading" }; 271 + } 272 + return { state: "loaded", data: document().collection }; 273 + }), 245 274 reload: remote.reload, 246 275 save: async (/** @type {T[]} */ newItems) => { 247 276 const doc = Automerge.change(document(), (d) => { ··· 256 285 const bytes = Automerge.save(doc); 257 286 await local()?.save(bytes); 258 287 }, 259 - state: computed(() => local()?.state() ?? "sleeping"), 260 288 }; 261 289 } 262 290
+19 -22
src/components/transformer/output/bytes/dasl-sync/element.js
··· 40 40 /** 41 41 * @template {{ id: string; updatedAt: string }} T 42 42 * @param {string} kind 43 - * @param {SignalReader<Uint8Array | undefined>} localCollection 44 - * @param {SignalReader<Uint8Array | undefined>} remoteCollection 45 - * @param {SignalReader<"loading" | "loaded" | "sleeping">} remoteState 43 + * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} localCollection 44 + * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} remoteCollection 46 45 * @param {{ saveLocal: (bytes: Uint8Array) => Promise<void>; saveRemote: (bytes: Uint8Array) => Promise<void> }} sync 47 46 */ 48 47 const state = ( 49 48 kind, 50 49 localCollection, 51 50 remoteCollection, 52 - remoteState, 53 51 { saveLocal, saveRemote }, 54 52 ) => { 55 53 const container = signal( ··· 66 64 if (!isReady.value) return; 67 65 if (merging.value.isBusy) return; 68 66 69 - const lb = localCollection(); 70 - const rb = remote.ready() ? remoteCollection() : undefined; 71 - const rs = remoteState(); 67 + const lc = localCollection(); 68 + const rc = remote.ready() ? remoteCollection() : undefined; 69 + 70 + const lb = lc?.state === "loaded" ? lc.data : undefined; 71 + const rb = rc?.state === "loaded" ? rc.data : undefined; 72 + const rs = rc?.state; 72 73 73 74 /** @type {Container<T> | undefined} */ 74 75 const l = lb ? decode(lb) : undefined; ··· 137 138 // Container signals 138 139 const facets = state( 139 140 "facets", 140 - computed(() => local()?.facets.collection()), 141 + computed(() => local()?.facets.collection() ?? { state: "loading" }), 141 142 remote.facets.collection, 142 - remote.facets.state, 143 143 { 144 144 saveLocal: async (v) => local()?.facets.save(v), 145 145 saveRemote: remote.facets.save, ··· 148 148 149 149 const playlistItems = state( 150 150 "playlistItems", 151 - computed(() => local()?.playlistItems.collection()), 151 + computed(() => 152 + local()?.playlistItems.collection() ?? { state: "loading" } 153 + ), 152 154 remote.playlistItems.collection, 153 - remote.playlistItems.state, 154 155 { 155 156 saveLocal: async (v) => local()?.playlistItems.save(v), 156 157 saveRemote: remote.playlistItems.save, ··· 159 160 160 161 const themes = state( 161 162 "themes", 162 - computed(() => local()?.themes.collection()), 163 + computed(() => local()?.themes.collection() ?? { state: "loading" }), 163 164 remote.themes.collection, 164 - remote.themes.state, 165 165 { 166 166 saveLocal: async (v) => local()?.themes.save(v), 167 167 saveRemote: remote.themes.save, ··· 170 170 171 171 const tracks = state( 172 172 "tracks", 173 - computed(() => local()?.tracks.collection()), 173 + computed(() => local()?.tracks.collection() ?? { state: "loading" }), 174 174 remote.tracks.collection, 175 - remote.tracks.state, 176 175 { 177 176 saveLocal: async (v) => local()?.tracks.save(v), 178 177 saveRemote: remote.tracks.save, ··· 407 406 /** 408 407 * @template {{ id: string; updatedAt: string }} T 409 408 * @param {{ save: (bytes: Uint8Array) => Promise<void> | void }} local 410 - * @param {{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} remote 409 + * @param {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void> }} remote 411 410 * @param {SignalReader<Container<T>>} container 412 - * @returns {{ collection: SignalReader<T[]>, reload: () => Promise<void>, save: (items: T[]) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} 411 + * @returns {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: T[] }>, reload: () => Promise<void>, save: (items: T[]) => Promise<void> }} 413 412 */ 414 413 managerProp(local, remote, container) { 415 414 return { 416 415 collection: computed(() => { 417 - return container().data; 416 + const c = container(); 417 + if (!c.cid) return { state: "loading" }; 418 + return { state: "loaded", data: c.data }; 418 419 }), 419 420 reload: remote.reload, 420 421 save: async (/** @type {T[]} */ newItems) => { ··· 426 427 const bytes = this.save(adjustedContainer); 427 428 await local.save(bytes); 428 429 }, 429 - state: computed(() => { 430 - if (container().cid) return "loaded"; 431 - return "loading"; 432 - }), 433 430 }; 434 431 } 435 432
+16 -12
src/components/transformer/output/bytes/json/element.js
··· 20 20 facets: { 21 21 ...base.facets, 22 22 collection: computed(() => { 23 - const data = base.facets.collection(); 23 + const col = base.facets.collection(); 24 + if (col.state !== "loaded") return col; 24 25 /** @type {Facet[]} */ 25 - const c = parseArray(data); 26 - return c; 26 + const data = parseArray(col.data); 27 + return { state: "loaded", data }; 27 28 }), 28 29 save: async (newFacets) => { 29 30 const json = JSON.stringify(newFacets); ··· 35 36 playlistItems: { 36 37 ...base.playlistItems, 37 38 collection: computed(() => { 38 - const data = base.playlistItems.collection(); 39 + const col = base.playlistItems.collection(); 40 + if (col.state !== "loaded") return col; 39 41 /** @type {PlaylistItem[]} */ 40 - const c = parseArray(data); 41 - return c; 42 + const data = parseArray(col.data); 43 + return { state: "loaded", data }; 42 44 }), 43 45 save: async (newPlaylistItems) => { 44 46 const json = JSON.stringify(newPlaylistItems); ··· 50 52 themes: { 51 53 ...base.themes, 52 54 collection: computed(() => { 53 - const data = base.themes.collection(); 55 + const col = base.themes.collection(); 56 + if (col.state !== "loaded") return col; 54 57 /** @type {Theme[]} */ 55 - const c = parseArray(data); 56 - return c; 58 + const data = parseArray(col.data); 59 + return { state: "loaded", data }; 57 60 }), 58 61 save: async (newThemes) => { 59 62 const json = JSON.stringify(newThemes); ··· 65 68 tracks: { 66 69 ...base.tracks, 67 70 collection: computed(() => { 68 - const data = base.tracks.collection(); 71 + const col = base.tracks.collection(); 72 + if (col.state !== "loaded") return col; 69 73 /** @type {Track[]} */ 70 - const c = parseArray(data); 71 - return c; 74 + const data = parseArray(col.data); 75 + return { state: "loaded", data }; 72 76 }), 73 77 save: async (newTracks) => { 74 78 const json = JSON.stringify(newTracks);
+25 -17
src/components/transformer/output/raw/atproto-sync/element.js
··· 51 51 this[name] = { 52 52 collection: computed(() => { 53 53 const l = local(); 54 - if (!l) return []; 55 - const data = l[name].collection(); 56 - return Array.isArray(data) ? data : []; 54 + if (!l) return { state: "loading" }; 55 + return l[name].collection(); 57 56 }), 58 57 reload: async () => { 59 58 await this.#sync(); ··· 64 63 65 64 // Track deletions: any id present in local but absent in 66 65 // newData has been deleted by the user. 67 - const oldData = l[name].collection(); 68 - if (Array.isArray(oldData)) { 66 + const oldCol = l[name].collection(); 67 + if (oldCol.state === "loaded" && Array.isArray(oldCol.data)) { 69 68 const newIds = new Set(newData.map((/** @type {any} */ r) => r.id)); 70 - for (const record of oldData) { 69 + for (const record of oldCol.data) { 71 70 if (!newIds.has(record.id)) { 72 71 this.#addTombstone(name, record.id); 73 72 } ··· 88 87 this.#markDirty(); 89 88 } 90 89 }, 91 - state: computed(() => local()?.[name].state() ?? "sleeping"), 92 90 }; 93 91 } 94 92 ··· 150 148 } 151 149 152 150 const localHasData = COLLECTIONS.some((name) => { 153 - const data = l[name].collection(); 154 - return Array.isArray(data) && data.length > 0; 151 + const col = l[name].collection(); 152 + return col.state === "loaded" && Array.isArray(col.data) && 153 + col.data.length > 0; 155 154 }); 156 155 157 156 if (!localHasData && !dirty) { 158 157 // Local is empty and clean — just pull remote 159 158 for (const name of COLLECTIONS) { 160 - const remoteData = remote[name].collection(); 161 - if (Array.isArray(remoteData) && remoteData.length > 0) { 162 - this.#trackIds(name, remoteData); 163 - await l[name].save(remoteData); 159 + const remoteCol = remote[name].collection(); 160 + if ( 161 + remoteCol.state === "loaded" && Array.isArray(remoteCol.data) && 162 + remoteCol.data.length > 0 163 + ) { 164 + this.#trackIds(name, remoteCol.data); 165 + await l[name].save(remoteCol.data); 164 166 } 165 167 } 166 168 } else { 167 169 // Union merge 168 170 for (const name of COLLECTIONS) { 169 - const localData = l[name].collection(); 170 - const remoteData = remote[name].collection(); 171 - const localArr = Array.isArray(localData) ? localData : []; 172 - const remoteArr = Array.isArray(remoteData) ? remoteData : []; 171 + const localCol = l[name].collection(); 172 + const remoteCol = remote[name].collection(); 173 + const localArr = 174 + localCol.state === "loaded" && Array.isArray(localCol.data) 175 + ? localCol.data 176 + : []; 177 + const remoteArr = 178 + remoteCol.state === "loaded" && Array.isArray(remoteCol.data) 179 + ? remoteCol.data 180 + : []; 173 181 174 182 const merged = this.#mergeRecords(name, localArr, remoteArr); 175 183
+18 -10
src/components/transformer/output/refiner/default/element.js
··· 40 40 facets: { 41 41 ...base.facets, 42 42 collection: computed(() => { 43 - return base.facets.collection() ?? []; 43 + const col = base.facets.collection(); 44 + if (col.state !== "loaded") return col; 45 + return { state: "loaded", data: col.data }; 44 46 }), 45 47 }, 46 48 playlistItems: { 47 49 ...base.playlistItems, 48 50 collection: computed(() => { 49 - return [ 50 - ...(base.playlistItems.collection() ?? []), 51 - ...ephemeralPlaylistItems.get(), 52 - ]; 51 + const col = base.playlistItems.collection(); 52 + if (col.state !== "loaded") return col; 53 + return { 54 + state: "loaded", 55 + data: [...col.data, ...ephemeralPlaylistItems.get()], 56 + }; 53 57 }), 54 58 save: async (newPlaylists) => { 55 59 /** @type {PlaylistItem[]} */ ··· 73 77 themes: { 74 78 ...base.themes, 75 79 collection: computed(() => { 76 - return base.themes.collection() ?? []; 80 + const col = base.themes.collection(); 81 + if (col.state !== "loaded") return col; 82 + return { state: "loaded", data: col.data }; 77 83 }), 78 84 }, 79 85 tracks: { 80 86 ...base.tracks, 81 87 collection: computed(() => { 82 - return [ 83 - ...(base.tracks.collection() ?? []), 84 - ...ephemeralTracks.get(), 85 - ]; 88 + const col = base.tracks.collection(); 89 + if (col.state !== "loaded") return col; 90 + return { 91 + state: "loaded", 92 + data: [...col.data, ...ephemeralTracks.get()], 93 + }; 86 94 }), 87 95 save: async (newTracks) => { 88 96 /** @type {Track[]} */
+21 -16
src/components/transformer/output/refiner/track-uri-passkey/element.js
··· 15 15 16 16 /** 17 17 * @import { Track } from "~/definitions/types.d.ts" 18 + * @import { OutputManager } from "~/components/output/types.d.ts" 18 19 */ 19 20 20 21 /** ··· 49 50 50 51 // Tracks 51 52 this.#tracks = () => { 52 - const raw = base.tracks.collection(); 53 - if (!raw) return { locked: [], unlocked: [] }; 53 + const col = base.tracks.collection(); 54 + if (col.state === "loading") { 55 + return { state: "loading", locked: [], unlocked: [] }; 56 + } 54 57 55 58 const key = encryptionKey.get(); 56 59 ··· 60 63 /** @type {Track[]} */ 61 64 const locked = []; 62 65 63 - for (const track of raw) { 66 + for (const track of col.data) { 64 67 if (!isEncryptedUri(track.uri)) { 65 68 unlocked.push(track); 66 69 } else if (key) { ··· 74 77 } 75 78 } 76 79 77 - return { locked, unlocked }; 80 + return { state: "loaded", locked, unlocked }; 78 81 }; 79 82 83 + /** @type {OutputManager["tracks"]} */ 80 84 this.tracks = { 81 85 ...base.tracks, 82 86 83 87 collection: computed(() => { 84 - const { locked, unlocked } = this.#tracks(); 85 - lockedTracks.set(locked); 86 - return unlocked; 88 + const result = this.#tracks(); 89 + if (result.state === "loading") return { state: "loading" }; 90 + lockedTracks.set(result.locked); 91 + return { state: "loaded", data: result.unlocked }; 87 92 }), 88 93 89 94 save: async (/** @type {Track[]} */ newTracks) => { ··· 179 184 return; 180 185 } 181 186 182 - const unlocked = this.tracks.collection(); 183 - if (this.tracks.state() !== "loaded") return; 187 + const col = this.tracks.collection(); 188 + if (col.state === "loading") return; 184 189 185 190 saved = true; 186 - this.tracks.save(unlocked); 191 + this.tracks.save(col.data); 187 192 }); 188 193 } 189 194 ··· 211 216 return; 212 217 } 213 218 214 - const unlocked = this.tracks.collection(); 215 - if (this.tracks.state() !== "loaded") return; 219 + const col = this.tracks.collection(); 220 + if (col.state !== "loaded") return; 216 221 217 222 saved = true; 218 - this.tracks.save(unlocked); 223 + this.tracks.save(col.data); 219 224 }); 220 225 } 221 226 ··· 234 239 return; 235 240 } 236 241 237 - const unlocked = this.tracks.collection(); 238 - if (this.tracks.state() !== "loaded") return; 242 + const col = this.tracks.collection(); 243 + if (col.state !== "loaded") return; 239 244 240 245 removed = true; 241 246 242 247 this.#encryptionKey.value = null; 243 - this.tracks.save(unlocked); 248 + this.tracks.save(col.data); 244 249 }); 245 250 } 246 251 }
+24 -8
src/components/transformer/output/string/json/element.js
··· 19 19 facets: { 20 20 ...base.facets, 21 21 collection: computed(() => { 22 - const json = base.facets.collection(); 23 - return typeof json === "string" ? parseArray(json) : []; 22 + const col = base.facets.collection(); 23 + if (col.state !== "loaded") return col; 24 + return { 25 + state: "loaded", 26 + data: typeof col.data === "string" ? parseArray(col.data) : [], 27 + }; 24 28 }), 25 29 save: async (newFacets) => { 26 30 const json = JSON.stringify(newFacets); ··· 30 34 playlistItems: { 31 35 ...base.playlistItems, 32 36 collection: computed(() => { 33 - const json = base.playlistItems.collection(); 34 - return typeof json === "string" ? parseArray(json) : []; 37 + const col = base.playlistItems.collection(); 38 + if (col.state !== "loaded") return col; 39 + return { 40 + state: "loaded", 41 + data: typeof col.data === "string" ? parseArray(col.data) : [], 42 + }; 35 43 }), 36 44 save: async (newPlaylistItems) => { 37 45 const json = JSON.stringify(newPlaylistItems); ··· 41 49 themes: { 42 50 ...base.themes, 43 51 collection: computed(() => { 44 - const json = base.themes.collection(); 45 - return typeof json === "string" ? parseArray(json) : []; 52 + const col = base.themes.collection(); 53 + if (col.state !== "loaded") return col; 54 + return { 55 + state: "loaded", 56 + data: typeof col.data === "string" ? parseArray(col.data) : [], 57 + }; 46 58 }), 47 59 save: async (newThemes) => { 48 60 const json = JSON.stringify(newThemes); ··· 52 64 tracks: { 53 65 ...base.tracks, 54 66 collection: computed(() => { 55 - const json = base.tracks.collection(); 56 - return typeof json === "string" ? parseArray(json) : []; 67 + const col = base.tracks.collection(); 68 + if (col.state !== "loaded") return col; 69 + return { 70 + state: "loaded", 71 + data: typeof col.data === "string" ? parseArray(col.data) : [], 72 + }; 57 73 }), 58 74 save: async (newTracks) => { 59 75 const json = JSON.stringify(newTracks);
+2 -4
src/facets/common/build.js
··· 233 233 //////////////////////////////////////////// 234 234 235 235 export async function editFacetFromURL() { 236 - await Output.waitUntilLoaded(output.facets); 237 - 238 - // Load facet from url 239 236 const idParam = new URLSearchParams(location.search).get("id"); 240 237 241 238 if (idParam) { 242 - const facet = output.facets.collection().find((f) => f.id === idParam); 239 + const col = await Output.data(output.facets); 240 + const facet = col.find((f) => f.id === idParam); 243 241 if (facet) await editFacet(facet); 244 242 } 245 243 }
+3 -7
src/facets/common/crud.js
··· 14 14 if (!c) return; 15 15 16 16 const output = foundation.orchestrator.output(); 17 - await Output.waitUntilLoaded(output.facets); 17 + const col = await Output.data(output.facets); 18 18 19 - output.facets.save( 20 - output.facets.collection().filter((c) => !(c.id === id)), 21 - ); 19 + output.facets.save(col.filter((c) => !(c.id === id))); 22 20 }; 23 21 } 24 22 ··· 27 25 */ 28 26 export async function saveFacet(facet) { 29 27 const output = foundation.orchestrator.output(); 30 - await Output.waitUntilLoaded(output.facets); 31 - 32 - const col = output.facets.collection(); 28 + const col = await Output.data(output.facets); 33 29 const colWithoutId = col.filter((c) => c.id !== facet.id); 34 30 await output.facets.save([...colWithoutId, { 35 31 ...facet,
+3 -6
src/facets/common/grid.js
··· 29 29 if (!uri || !name) return; 30 30 31 31 const out = foundation.orchestrator.output(); 32 - await Output.waitUntilLoaded(out.facets); 33 - 34 - const collection = out.facets.collection(); 32 + const collection = await Output.data(out.facets); 35 33 const isActive = collection.some((f) => f.uri === uri); 36 34 37 35 if (isActive) { ··· 55 53 56 54 export async function monitorToggleButtonStates() { 57 55 if (stopMonitor) stopMonitor(); 58 - 59 56 const out = foundation.orchestrator.output(); 60 - await Output.waitUntilLoaded(out.facets); 61 57 62 58 stopMonitor = effect(() => { 63 59 const gridItems = /** @type {NodeListOf<HTMLLIElement>} */ ( 64 60 document.querySelectorAll(".grid li") 65 61 ); 66 62 67 - const collection = out.facets.collection(); 63 + const col = out.facets.collection(); 64 + const collection = col.state === "loaded" ? col.data : []; 68 65 const activeURIs = new Set(collection.map((f) => f.uri)); 69 66 70 67 for (const li of gridItems) {
+10 -5
src/facets/common/you.js
··· 167 167 } 168 168 169 169 const output = foundation.orchestrator.output(); 170 - await Output.waitUntilLoaded(output.facets); 171 170 172 171 stopMonitor = effect(() => { 173 172 _renderList(output, listEl); ··· 179 178 * @param {HTMLElement} listEl 180 179 */ 181 180 function _renderList(output, listEl) { 182 - if (output.facets.state() !== "loaded") { 181 + const facetsCol = output.facets.collection(); 182 + 183 + if (facetsCol.state !== "loaded") { 183 184 const loading = html` 184 185 <div class="with-icon"> 185 186 <i class="ph-bold ph-spinner animate-spin"></i> ··· 190 191 render(loading, listEl); 191 192 } 192 193 193 - const col = output.facets.collection().sort((a, b) => { 194 - return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()); 195 - }); 194 + const col = facetsCol.state === "loaded" 195 + ? [...facetsCol.data].sort((a, b) => { 196 + return a.name.toLocaleLowerCase().localeCompare( 197 + b.name.toLocaleLowerCase(), 198 + ); 199 + }) 200 + : []; 196 201 197 202 const h = col.length 198 203 ? html`
+3 -1
src/facets/examples/generate-playlist/index.inline.js
··· 13 13 ...queue.future().filter((i) => i.manualEntry), 14 14 ]; 15 15 16 + const tracksCol = output.tracks.collection(); 17 + const tracksList = tracksCol.state === "loaded" ? tracksCol.data : []; 16 18 const playlist = queueItems 17 - .map((item) => output.tracks.collection().find((t) => t.id === item?.id)) 19 + .map((item) => tracksList.find((t) => t.id === item?.id)) 18 20 .filter((t) => t); 19 21 20 22 const element = document.querySelector("main ol");
+6 -8
src/facets/examples/now-playing/index.inline.js
··· 1 1 import foundation from "~/common/facets/foundation.js"; 2 - import { computed, effect } from "~/common/signal.js"; 2 + import { effect } from "~/common/signal.js"; 3 3 4 4 foundation.features.processInputs(); 5 5 foundation.features.fillQueueAutomatically(); ··· 7 7 const output = foundation.orchestrator.output(); 8 8 const queue = foundation.engine.queue(); 9 9 10 - const isLoadingTracks = computed(() => { 11 - return output.tracks.state() !== "loaded"; 12 - }); 13 - 14 10 effect(() => { 15 11 const now = queue.now(); 16 - const currentlyPlaying = now 17 - ? output.tracks.collection().find((t) => t.id === now.id) 12 + const tracksCol = output.tracks.collection(); 13 + const isLoadingTracks = tracksCol.state !== "loaded"; 14 + const currentlyPlaying = now && tracksCol.state === "loaded" 15 + ? tracksCol.data.find((t) => t.id === now.id) 18 16 : undefined; 19 17 const tags = currentlyPlaying?.tags; 20 18 ··· 26 24 element.innerText = `${tags?.artist ?? "Unknown artist"} - ${ 27 25 tags?.title ?? "Unknown title" 28 26 }`; 29 - } else if (isLoadingTracks()) { 27 + } else if (isLoadingTracks) { 30 28 // Keep original text 31 29 } else { 32 30 element.innerText = "Nothing is playing yet";
+2 -1
src/facets/tools/auto-queue/index.inline.js
··· 55 55 56 56 // Playlist state 57 57 effect(() => { 58 - const items = output.playlistItems.collection(); 58 + const col = output.playlistItems.collection(); 59 + const items = col.state === "loaded" ? col.data : []; 59 60 const currentPlaylist = scope.playlist(); 60 61 61 62 // Group items by playlist name
+8 -8
src/facets/tools/export-import/index.inline.js
··· 43 43 44 44 // Export all data as a JSON snapshot 45 45 exportBtn.onclick = async () => { 46 - await Output.waitUntilLoaded(output.facets); 47 - await Output.waitUntilLoaded(output.playlistItems); 48 - await Output.waitUntilLoaded(output.themes); 49 - await Output.waitUntilLoaded(output.tracks); 46 + const facets = await Output.data(output.facets); 47 + const playlistItems = await Output.data(output.playlistItems); 48 + const themes = await Output.data(output.themes); 49 + const tracks = await Output.data(output.tracks); 50 50 51 51 const data = { 52 52 exportedAt: new Date().toISOString(), 53 - tracks: output.tracks.collection() ?? [], 54 - playlistItems: output.playlistItems.collection() ?? [], 55 - facets: output.facets.collection() ?? [], 56 - themes: output.themes.collection() ?? [], 53 + facets, 54 + playlistItems, 55 + themes, 56 + tracks, 57 57 }; 58 58 59 59 const blob = new Blob([JSON.stringify(data, null, 2)], {
+1 -3
src/facets/tools/split-view/index.inline.js
··· 425 425 426 426 async function saveSimplifiedCopy() { 427 427 const output = foundation.orchestrator.output(); 428 - await Output.waitUntilLoaded(output.facets); 429 - 430 428 const id = crypto.randomUUID(); 431 429 const html = generateSimplifiedHTML(id); 432 430 const now = new Date().toISOString(); 433 431 434 432 await output.facets.save([ 435 - ...output.facets.collection(), 433 + ...(await Output.data(output.facets)), 436 434 { 437 435 $type: "sh.diffuse.output.facet", 438 436 id,
+2 -1
src/facets/tools/v3-import/index.inline.js
··· 104 104 try { 105 105 const now = new Date().toISOString(); 106 106 107 + const existingCol = output.playlistItems.collection(); 107 108 /** @type {any[]} */ 108 - const existing = output.playlistItems.collection() ?? []; 109 + const existing = existingCol.state === "loaded" ? existingCol.data : []; 109 110 const existingPlaylistNames = new Set(existing.map((p) => p.playlist)); 110 111 111 112 const newPlaylistItems = items
+5 -3
src/index.js
··· 42 42 </span>`; 43 43 44 44 const demo = await s3.demo(); 45 - await Output.waitUntilLoaded(output.tracks); 45 + 46 + await output.tracks.save([ 47 + ...(await Output.data(output.tracks)), 48 + demo.track, 49 + ]); 46 50 47 51 addDemoBtn.innerHTML = `<span> 48 52 <i class="ph-fill ph-hourglass-medium"></i> 49 53 Processing source 50 54 </span>`; 51 - 52 - await output.tracks.save([...output.tracks.collection(), demo.track]); 53 55 54 56 await pto.process(); 55 57
+4 -5
src/testing/output.js
··· 6 6 document.body.append(output); 7 7 8 8 effect(() => { 9 - console.log(output.tracks.state()); 10 - }); 11 - 12 - effect(() => { 13 - console.log(output.tracks.collection()); 9 + const col = output.tracks.collection(); 10 + if (col.state === "loaded") { 11 + console.log(col.data); 12 + } 14 13 });
+3 -3
src/themes/blur/artwork-controller/element.js
··· 72 72 currentTrack = computed(() => { 73 73 const item = this.$queue.value?.now(); 74 74 if (!item) return undefined; 75 - return this.$output.value?.tracks.collection().find((t) => 76 - t.id === item.id 77 - ); 75 + const col = this.$output.value?.tracks.collection(); 76 + if (!col || col.state !== "loaded") return undefined; 77 + return col.data.find((t) => t.id === item.id); 78 78 }); 79 79 80 80 isPlaying = computed(() => {
+15 -12
src/themes/index.js
··· 50 50 const theme = await themeFromURI({ name, uri }, { fetchHTML: false }); 51 51 const out = foundation.orchestrator.output(); 52 52 53 + const col = out.themes.collection(); 53 54 out.themes.save([ 54 - ...out.themes.collection(), 55 + ...(col.state === "loaded" ? col.data : []), 55 56 theme, 56 57 ]); 57 58 break; ··· 73 74 const output = foundation.orchestrator.output(); 74 75 75 76 effect(() => { 76 - const col = output.themes.collection().sort((a, b) => { 77 - return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()); 78 - }); 77 + const themesCol = output.themes.collection(); 78 + const col = themesCol.state === "loaded" 79 + ? [...themesCol.data].sort((a, b) => { 80 + return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()); 81 + }) 82 + : []; 79 83 80 - const state = output.themes.state(); 81 - 82 - const h = col.length && state === "loaded" 84 + const h = col.length && themesCol.state === "loaded" 83 85 ? html` 84 86 <ul> 85 87 ${col.map((c) => ··· 146 148 )} 147 149 </ul> 148 150 ` 149 - : state === "loaded" 151 + : themesCol.state === "loaded" 150 152 ? emptyThemesList 151 153 : html` 152 154 <div class="with-icon" style="font-size: var(--fs-sm);"> ··· 172 174 const c = confirm("Are you sure you want to delete this theme?"); 173 175 if (!c) return; 174 176 175 - output.themes.save( 176 - output.themes.collection().filter((c) => !(c.id === id)), 177 - ); 177 + const col = output.themes.collection(); 178 + if (col.state !== "loaded") return; 179 + output.themes.save(col.data.filter((c) => !(c.id === id))); 178 180 }; 179 181 } 180 182 ··· 280 282 */ 281 283 async function saveTheme(theme) { 282 284 const col = output.themes.collection(); 283 - const colWithoutId = col.filter((c) => c.id !== theme.id); 285 + const data = col.state === "loaded" ? col.data : []; 286 + const colWithoutId = data.filter((c) => c.id !== theme.id); 284 287 await output.themes.save([...colWithoutId, { 285 288 ...theme, 286 289 updatedAt: new Date().toISOString(),
+7 -3
src/themes/webamp/browser/element.js
··· 50 50 $highlightedTrack = signal(/** @type {string | null} */ (null)); 51 51 52 52 $groupedPlaylists = computed(() => { 53 - const items = this.$output.value?.playlistItems.collection(); 54 - if (!items?.length) return []; 53 + const col = this.$output.value?.playlistItems.collection(); 54 + if (!col || col.state !== "loaded" || !col.data.length) return []; 55 + const items = col.data; 55 56 56 57 // Group items by playlist name 57 58 /** @type {Map<string, { name: string, unordered: boolean }>} */ ··· 256 257 */ 257 258 render({ html }) { 258 259 const highlighted = this.$highlightedTrack.value; 259 - const isLoading = this.$output.value?.tracks?.state() !== "loaded"; 260 + const isLoading = 261 + this.$output.value?.tracks?.collection().state !== "loaded"; 262 + 260 263 const tracks = this.$provider.value?.tracks() ?? []; 261 264 const playlist = this.$scope.value?.playlist(); 262 265 const searchTerm = this.$scope.value?.searchTerm() ?? ""; ··· 265 268 const sortedColumn = Object.entries(COLUMN_SORT).find( 266 269 ([, v]) => JSON.stringify(v) === JSON.stringify(sortBy), 267 270 )?.[0]; 271 + 268 272 const ariaSort = /** @param {string} col */ (col) => 269 273 sortedColumn === col 270 274 ? (sortDirection === "desc" ? "descending" : "ascending")
+6 -5
src/themes/webamp/configurators/input/element.js
··· 107 107 } 108 108 109 109 const _trigger = sourcesOrchestrator.sources(); 110 - if (output.tracks.state() !== "loaded") return; 110 + if (output.tracks.collection().state !== "loaded") return; 111 111 processTracksOrchestrator?.process(); 112 112 }); 113 113 } ··· 276 276 const uri = selected.getAttribute("data-uri"); 277 277 if (!uri) throw new Error("Missing `uri` attribute"); 278 278 279 + const tracksCol = this.$output.value?.tracks.collection(); 279 280 const detachedTracks = await this.$input.value?.detach({ 280 281 fileUriOrScheme: uri, 281 - tracks: this.$output.value?.tracks.collection() ?? [], 282 + tracks: tracksCol?.state === "loaded" ? tracksCol.data : [], 282 283 }); 283 284 284 285 if (detachedTracks) this.$output.value?.tracks.save(detachedTracks); ··· 316 317 uri, 317 318 }; 318 319 319 - await this.$output.value?.tracks.save( 320 - [...(this.$output.value?.tracks.collection() ?? []), track], 321 - ); 320 + const existingTracksCol = this.$output.value?.tracks.collection(); 321 + const existingTracks = existingTracksCol?.state === "loaded" ? existingTracksCol.data : []; 322 + await this.$output.value?.tracks.save([...existingTracks, track]); 322 323 323 324 this.$tab.value = "overview"; 324 325 }
+6 -3
src/themes/webamp/index.js
··· 107 107 /** @type {Track[]} */ 108 108 const tracksToAdd = []; 109 109 110 + const tracksCol = output.tracks.collection(); 111 + const tracksList = tracksCol.state === "loaded" ? tracksCol.data : []; 112 + 110 113 Object.entries(newIdx).forEach(([id, n]) => { 111 114 const x = index[id] ?? 0; 112 115 if (n > x) { 113 - const track = output.tracks.collection().find((t) => t.id === id); 116 + const track = tracksList.find((t) => t.id === id); 114 117 if (track) tracksToAdd.push(track); 115 118 index[id] = x + 1; 116 119 } ··· 138 141 const tracksPromise = Promise.withResolvers(); 139 142 140 143 effect(() => { 141 - const state = output.tracks.state(); 142 - if (state !== "loaded") return; 144 + const col = output.tracks.collection(); 145 + if (col.state !== "loaded") return; 143 146 144 147 const fingerprintSearch = search.supplyFingerprint(); 145 148 if (fingerprintSearch === undefined) return;