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: finish up passkey transformer and remove broadcast transformer

+135 -130
+5 -11
src/components/orchestrator/output/element.js
··· 3 3 4 4 import "@components/configurator/output/element.js"; 5 5 import "@components/transformer/output/refiner/default/element.js"; 6 - import "@components/transformer/output/replicator/broadcast/element.js"; 7 6 8 7 /** 9 8 * @import {RenderArg} from "@common/element.d.ts" ··· 152 151 <dop-indexed-db 153 152 id="do-output__dop-indexed-db__json" 154 153 namespace="json" 154 + group="${ifDefined(group)}" 155 155 ></dop-indexed-db> 156 156 157 157 <!-- ⚙️ S3 --> 158 - <dob-s3 id="do-output__dob-s3"></dob-s3> 158 + <dob-s3 id="do-output__dob-s3" group="${ifDefined(group)}"></dob-s3> 159 159 160 160 <!-- ⚙️ ATPROTO --> 161 161 <dtor-atproto-sync 162 162 id="do-output__dtor-atproto-sync" 163 163 namespace="atproto" 164 164 output-selector="#do-output__dor-atproto" 165 + group="${ifDefined(group)}" 165 166 ></dtor-atproto-sync> 166 167 167 168 <dor-atproto id="do-output__dor-atproto"></dor-atproto> ··· 198 199 ></dtob-dasl-sync> 199 200 </dc-output> 200 201 201 - <!-- REFINERS --> 202 + <!-- ENTRY ⬆️ --> 202 203 <dtor-default 203 - id="do-output__dtor-default" 204 + id="do-output__output" 204 205 output-selector="#do-output__dc-output" 205 206 ></dtor-default> 206 - 207 - <!-- ENTRY ⬆️ --> 208 - <dtor-broadcast 209 - id="do-output__output" 210 - output-selector="#do-output__dtor-default" 211 - group="${ifDefined(group)}" 212 - ></dtor-broadcast> 213 207 `; 214 208 } 215 209 }
+4 -3
src/components/output/bytes/s3/element.js
··· 1 1 import * as IDB from "idb-keyval"; 2 2 3 - import { DiffuseElement } from "@common/element.js"; 4 3 import { computed, signal } from "@common/signal.js"; 5 - import { outputManager } from "../../common.js"; 4 + import { BroadcastedOutputElement, outputManager } from "../../common.js"; 6 5 7 6 const STORAGE_PREFIX = "diffuse/output/bytes/s3"; 8 7 ··· 21 20 * @implements {OutputElement<Uint8Array | undefined>} 22 21 * @implements {S3OutputElement} 23 22 */ 24 - class S3Output extends DiffuseElement { 23 + class S3Output extends BroadcastedOutputElement { 25 24 static NAME = "diffuse/output/bytes/s3"; 26 25 static WORKER_URL = "components/output/bytes/s3/worker.js"; 27 26 ··· 80 79 * @override 81 80 */ 82 81 async connectedCallback() { 82 + this.replicateSavedData(this.#manager); 83 + 83 84 super.connectedCallback(); 84 85 85 86 /** @type {Bucket | undefined} */
+61 -2
src/components/output/common.js
··· 1 - import deepDiff from "@fry69/deep-diff"; 1 + import { BroadcastableDiffuseElement } from "@common/element.js"; 2 2 import { batch, computed, effect, signal, untracked } from "@common/signal.js"; 3 3 4 4 /** 5 5 * @import {Facet, PlaylistItem, Theme, Track} from "@definitions/types.d.ts" 6 - * @import {SignalReader} from "@common/signal.d.ts"; 6 + * @import {SignalReader, SignalWriter} from "@common/signal.d.ts"; 7 7 * @import {OutputManager, OutputManagerProperties} from "./types.d.ts" 8 8 */ 9 + 10 + export class BroadcastedOutputElement extends BroadcastableDiffuseElement { 11 + /** 12 + * @param {OutputManager<any>} manager 13 + */ 14 + replicateSavedData(manager) { 15 + // Broadcast if needed 16 + if (!this.hasAttribute("group")) return; 17 + 18 + /** 19 + * @template T 20 + * @param {{ save: (data: T) => Promise<void>; set: SignalWriter<T> }} _ 21 + * @returns {(data: T) => Promise<void>} 22 + */ 23 + const fn = ({ save, set }) => async (data) => { 24 + if (await this.isLeader()) { 25 + return save(data); 26 + } else { 27 + return set(data); 28 + } 29 + }; 30 + 31 + const actions = this.broadcast(this.nameWithGroup, { 32 + saveFacets: { 33 + strategy: "replicate", 34 + fn: fn({ save: manager.facets.save, set: manager.signals.facets.set }), 35 + }, 36 + savePlaylistItems: { 37 + strategy: "replicate", 38 + fn: fn({ 39 + save: manager.playlistItems.save, 40 + set: manager.signals.playlistItems.set, 41 + }), 42 + }, 43 + saveThemes: { 44 + strategy: "replicate", 45 + fn: fn({ save: manager.themes.save, set: manager.signals.themes.set }), 46 + }, 47 + saveTracks: { 48 + strategy: "replicate", 49 + fn: fn({ save: manager.tracks.save, set: manager.signals.tracks.set }), 50 + }, 51 + }); 52 + 53 + if (actions) { 54 + manager.facets.save = actions.saveFacets; 55 + manager.playlistItems.save = actions.savePlaylistItems; 56 + manager.themes.save = actions.saveThemes; 57 + manager.tracks.save = actions.saveTracks; 58 + } 59 + } 60 + } 9 61 10 62 /** 11 63 * @param {() => void} loader ··· 177 229 playlistItems: pl, 178 230 themes: th, 179 231 tracks: t, 232 + 233 + states: { 234 + facets: cs, 235 + playlistItems: pls, 236 + themes: ths, 237 + tracks: ts, 238 + }, 180 239 }, 181 240 }; 182 241 }
+11 -3
src/components/output/polymorphic/indexed-db/element.js
··· 1 1 import * as IDB from "idb-keyval"; 2 2 3 3 import { IDB_PREFIX } from "./constants.js"; 4 - import { DiffuseElement } from "@common/element.js"; 5 - import { outputManager } from "../../common.js"; 4 + import { BroadcastedOutputElement, outputManager } from "../../common.js"; 6 5 7 6 /** 8 7 * @import {OutputElement, OutputManager, OutputWorkerActions} from "../../types.d.ts" ··· 16 15 /** 17 16 * @implements {OutputElement<SupportedDataTypes>} 18 17 */ 19 - class IndexedDBOutput extends DiffuseElement { 18 + class IndexedDBOutput extends BroadcastedOutputElement { 20 19 static NAME = "diffuse/output/polymorphic/indexed-db"; 21 20 static WORKER_URL = "components/output/polymorphic/indexed-db/worker.js"; 22 21 ··· 56 55 this.tracks = this.#manager.tracks; 57 56 58 57 this.ready = () => true; 58 + } 59 + 60 + // LIFECYCLE 61 + 62 + /** @override */ 63 + connectedCallback() { 64 + this.replicateSavedData(this.#manager); 65 + 66 + super.connectedCallback(); 59 67 } 60 68 61 69 // GET & PUT
+4 -3
src/components/output/raw/atproto/element.js
··· 1 1 import { Client, ClientResponseError, ok } from "@atcute/client"; 2 2 3 - import { DiffuseElement } from "@common/element.js"; 4 3 import { computed, signal } from "@common/signal.js"; 5 - import { outputManager } from "../../common.js"; 4 + import { BroadcastedOutputElement, outputManager } from "../../common.js"; 6 5 7 6 import { 8 7 clearStoredSession, ··· 25 24 /** 26 25 * @implements {ATProtoOutputElement} 27 26 */ 28 - class ATProtoOutput extends DiffuseElement { 27 + class ATProtoOutput extends BroadcastedOutputElement { 29 28 static NAME = "diffuse/output/raw/atproto"; 30 29 31 30 #manager; ··· 89 88 90 89 /** @override */ 91 90 connectedCallback() { 91 + this.replicateSavedData(this.#manager); 92 + 92 93 super.connectedCallback(); 93 94 94 95 this.#tryRestore();
+7
src/components/output/types.d.ts
··· 39 39 playlistItems: Signal<Encoding extends null ? PlaylistItem[] : Encoding>; 40 40 themes: Signal<Encoding extends null ? Theme[] : Encoding>; 41 41 tracks: Signal<Encoding extends null ? Track[] : Encoding>; 42 + 43 + states: { 44 + facets: Signal<"loading" | "loaded" | "sleeping">; 45 + playlistItems: Signal<"loading" | "loaded" | "sleeping">; 46 + themes: Signal<"loading" | "loaded" | "sleeping">; 47 + tracks: Signal<"loading" | "loaded" | "sleeping">; 48 + }; 42 49 }; 43 50 themes: { 44 51 collection: SignalReader<Encoding extends null ? Theme[] : Encoding>;
+43 -6
src/components/transformer/output/refiner/track-uri-passkey/element.js
··· 175 175 const key = await deriveCipherKey(result.prfSecond); 176 176 await storeCipherKey(this.namespace, key); 177 177 this.#encryptionKey.value = key; 178 + 179 + let saved = false; 180 + 181 + const stop = this.effect(() => { 182 + if (saved) { 183 + stop(); 184 + return; 185 + } 186 + 187 + const unlocked = this.tracks.collection(); 188 + if (this.tracks.state() !== "loaded") return; 189 + 190 + saved = true; 191 + this.tracks.save(unlocked); 192 + }); 178 193 } 179 194 180 195 /** ··· 191 206 const key = await deriveCipherKey(result.prfSecond); 192 207 await storeCipherKey(this.namespace, key); 193 208 this.#encryptionKey.value = key; 209 + 210 + let saved = false; 211 + 212 + const stop = this.effect(() => { 213 + if (saved) { 214 + stop(); 215 + return; 216 + } 217 + 218 + const unlocked = this.tracks.collection(); 219 + if (this.tracks.state() !== "loaded") return; 220 + 221 + saved = true; 222 + this.tracks.save(unlocked); 223 + }); 194 224 } 195 225 196 226 /** ··· 199 229 async removePasskey() { 200 230 await removeStoredPasskey(this.namespace); 201 231 202 - // Capture decrypted tracks while encryption key is still in memory 203 - const unlocked = this.tracks.collection(); 232 + let removed = false; 204 233 205 - this.#encryptionKey.value = null; 234 + const stop = this.effect(() => { 235 + if (removed) { 236 + stop(); 237 + return; 238 + } 206 239 207 - console.log(unlocked, this.tracks.state(), this.lockedTracks()); 240 + const unlocked = this.tracks.collection(); 241 + if (this.tracks.state() !== "loaded") return; 208 242 209 - // Re-save as plaintext (key is now null, so save skips encryption) 210 - await this.tracks.save(unlocked); 243 + removed = true; 244 + 245 + this.#encryptionKey.value = null; 246 + this.tracks.save(unlocked); 247 + }); 211 248 } 212 249 } 213 250
-99
src/components/transformer/output/replicator/broadcast/element.js
··· 1 - import { computed } from "@common/signal.js"; 2 - import { OutputTransformer } from "../../base.js"; 3 - 4 - /** 5 - * @import { OutputManagerDeputy } from "../../../../output/types.d.ts" 6 - */ 7 - 8 - /** 9 - * @extends {OutputTransformer} 10 - */ 11 - class BroadcastOutputReplicatorTransformer extends OutputTransformer { 12 - static NAME = "diffuse/transformer/output/replicator/broadcast"; 13 - 14 - constructor() { 15 - super(); 16 - 17 - const base = this.base(); 18 - 19 - /** @type {OutputManagerDeputy} */ 20 - const manager = { 21 - facets: { 22 - ...base.facets, 23 - collection: computed(() => { 24 - return base.facets.collection() ?? []; 25 - }), 26 - }, 27 - playlistItems: { 28 - ...base.playlistItems, 29 - collection: computed(() => { 30 - return base.playlistItems.collection() ?? []; 31 - }), 32 - }, 33 - themes: { 34 - ...base.themes, 35 - collection: computed(() => { 36 - return base.themes.collection() ?? []; 37 - }), 38 - }, 39 - tracks: { 40 - ...base.tracks, 41 - collection: computed(() => { 42 - return base.tracks.collection() ?? []; 43 - }), 44 - }, 45 - 46 - // Other 47 - ready: base.ready, 48 - }; 49 - 50 - // Assign manager properties to class 51 - this.facets = manager.facets; 52 - this.playlistItems = manager.playlistItems; 53 - this.themes = manager.themes; 54 - this.tracks = manager.tracks; 55 - 56 - this.ready = manager.ready; 57 - } 58 - 59 - // LIFECYCLE 60 - 61 - /** 62 - * @override 63 - */ 64 - connectedCallback() { 65 - // Broadcast if needed 66 - if (this.hasAttribute("group")) { 67 - const actions = this.broadcast(this.nameWithGroup, { 68 - saveFacets: { strategy: "replicate", fn: this.facets.save }, 69 - savePlaylistItems: { 70 - strategy: "replicate", 71 - fn: this.playlistItems.save, 72 - }, 73 - saveThemes: { strategy: "replicate", fn: this.themes.save }, 74 - saveTracks: { strategy: "replicate", fn: this.tracks.save }, 75 - }); 76 - 77 - if (actions) { 78 - this.facets.save = actions.saveFacets; 79 - this.playlistItems.save = actions.savePlaylistItems; 80 - this.themes.save = actions.saveThemes; 81 - this.tracks.save = actions.saveTracks; 82 - } 83 - } 84 - 85 - // Super 86 - super.connectedCallback(); 87 - } 88 - } 89 - 90 - export default BroadcastOutputReplicatorTransformer; 91 - 92 - //////////////////////////////////////////// 93 - // REGISTER 94 - //////////////////////////////////////////// 95 - 96 - export const CLASS = BroadcastOutputReplicatorTransformer; 97 - export const NAME = "dtor-broadcast"; 98 - 99 - customElements.define(NAME, CLASS);
-3
src/index.vto
··· 142 142 - title: "Output / Refiner / Default" 143 143 desc: "The task of a refiner transformer is to remove the output state that is not meant to be saved to storage. For example, ephemeral tracks; this transformer will keep them in memory, but they will not be present in the output. **Ideally this is part of every theme, but you may swap it out with another transformer that might provide better defaults.**" 144 144 url: "components/transformer/output/refiner/default/element.js" 145 - - title: "Output / Replicator / Broadcast" 146 - desc: "Replicates output `save` actions across browser tabs using a broadcast channel, keeping groups in sync." 147 - url: "components/transformer/output/replicator/broadcast/element.js" 148 145 - title: "Output / String / JSON" 149 146 desc: "Raw data schema output ⇄ JSON UTF8 string." 150 147 url: "components/transformer/output/string/json/element.js"