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.

refactor: a bunch of output related things

+317 -561
-5
src/common/loader.js
··· 79 79 } else { 80 80 const source = config.source(); 81 81 const collection = source.collection(); 82 - console.log(source.state(), collection); 83 82 if (source.state() !== "loaded") return; 84 83 85 84 if (id) { ··· 143 142 * @returns {Promise<T>} 144 143 */ 145 144 export async function ensureHTML(item) { 146 - console.log(item.html, item.uri); 147 - 148 145 if (!item.html && item.uri) { 149 146 const html = await loadURI(item.uri); 150 147 const cid = await CID.create(0x55, new TextEncoder().encode(html)); 151 - 152 - console.log(html); 153 148 154 149 item.html = html; 155 150 item.cid = cid;
+2 -1
src/common/worker/rpc-channel.js
··· 120 120 const fn = this.#actions?.[/** @type {keyof LocalAPI} */ (method)]; 121 121 122 122 if (typeof fn !== "function") { 123 - console.log(method, id, fn, this.#actions); 124 123 this.#port.postMessage({ 125 124 __rpc: true, 126 125 id, ··· 136 135 this.#port.postMessage({ __rpc: true, id, type: "response", result }); 137 136 }, 138 137 (err) => { 138 + console.error(err) 139 139 this.#port.postMessage({ 140 140 __rpc: true, 141 141 id, ··· 145 145 }, 146 146 ); 147 147 } catch (err) { 148 + console.error(err) 148 149 this.#port.postMessage({ 149 150 __rpc: true, 150 151 id,
-214
src/components/configurator/output-fallback/element.js
··· 1 - import { DiffuseElement } from "@common/element.js"; 2 - import { computed, signal } from "@common/signal.js"; 3 - 4 - /** 5 - * @import {OutputManagerDeputy, OutputElement} from "@components/output/types.d.ts" 6 - * @import {OutputFallbackConfiguratorElement} from "./types.d.ts" 7 - */ 8 - 9 - //////////////////////////////////////////// 10 - // ELEMENT 11 - //////////////////////////////////////////// 12 - 13 - /** 14 - * Output fallback configurator. 15 - * 16 - * Checks child output elements in order and delegates 17 - * to the first one whose `.ready()` signal returns `true`. 18 - * 19 - * @template [Encoding=null] 20 - * @implements {OutputManagerDeputy<Encoding | undefined>} 21 - * @implements {OutputFallbackConfiguratorElement<Encoding>} 22 - */ 23 - class OutputFallbackConfigurator extends DiffuseElement { 24 - static NAME = "diffuse/configurator/output-fallback"; 25 - 26 - constructor() { 27 - super(); 28 - 29 - /** @type {OutputManagerDeputy<Encoding | undefined>} */ 30 - const manager = { 31 - facets: { 32 - collection: computed(() => { 33 - return this.#activeOutput.value?.facets.collection(); 34 - }), 35 - reload: () => { 36 - const out = this.#activeOutput.value; 37 - if (out) return out.facets.reload(); 38 - return Promise.resolve(); 39 - }, 40 - save: async (newFacets) => { 41 - if (newFacets !== undefined) { 42 - await Promise.all( 43 - this.#outputs.map((o) => o.facets.save(newFacets)), 44 - ); 45 - } 46 - }, 47 - state: computed(() => { 48 - return this.#activeOutput.value?.facets.state() ?? "sleeping"; 49 - }), 50 - }, 51 - playlistItems: { 52 - collection: computed(() => { 53 - return this.#activeOutput.value?.playlistItems.collection(); 54 - }), 55 - reload: () => { 56 - const out = this.#activeOutput.value; 57 - if (out) return out.playlistItems.reload(); 58 - return Promise.resolve(); 59 - }, 60 - save: async (newPlaylistItems) => { 61 - if (newPlaylistItems !== undefined) { 62 - await Promise.all( 63 - this.#outputs.map((o) => o.playlistItems.save(newPlaylistItems)), 64 - ); 65 - } 66 - }, 67 - state: computed(() => { 68 - return this.#activeOutput.value?.playlistItems.state() ?? "sleeping"; 69 - }), 70 - }, 71 - themes: { 72 - collection: computed(() => { 73 - return this.#activeOutput.value?.themes.collection(); 74 - }), 75 - reload: () => { 76 - const out = this.#activeOutput.value; 77 - if (out) return out.themes.reload(); 78 - return Promise.resolve(); 79 - }, 80 - save: async (newThemes) => { 81 - if (newThemes !== undefined) { 82 - await Promise.all( 83 - this.#outputs.map((o) => o.themes.save(newThemes)), 84 - ); 85 - } 86 - }, 87 - state: computed(() => { 88 - return this.#activeOutput.value?.themes.state() ?? "sleeping"; 89 - }), 90 - }, 91 - tracks: { 92 - collection: computed(() => { 93 - return this.#activeOutput.value?.tracks.collection(); 94 - }), 95 - reload: () => { 96 - const out = this.#activeOutput.value; 97 - if (out) return out.tracks.reload(); 98 - return Promise.resolve(); 99 - }, 100 - save: async (newTracks) => { 101 - if (newTracks !== undefined) { 102 - await Promise.all( 103 - this.#outputs.map((o) => o.tracks.save(newTracks)), 104 - ); 105 - } 106 - }, 107 - state: computed(() => { 108 - return this.#activeOutput.value?.tracks.state() ?? "sleeping"; 109 - }), 110 - }, 111 - 112 - // Other 113 - ready: computed(() => { 114 - if (this.#activeOutput.value) return true; 115 - return this.#setupFinished.value; 116 - }), 117 - }; 118 - 119 - this.facets = manager.facets; 120 - this.playlistItems = manager.playlistItems; 121 - this.themes = manager.themes; 122 - this.tracks = manager.tracks; 123 - this.ready = manager.ready; 124 - 125 - this.effect(this.#setActiveOutput); 126 - } 127 - 128 - #setActiveOutput = () => { 129 - const _trigger = this.#setupFinished.value; 130 - 131 - /** @type {OutputElement<Encoding> | null} */ 132 - let activeOutput = null; 133 - 134 - for (const output of this.#outputs) { 135 - if (output.ready()) { 136 - activeOutput = output; 137 - break; 138 - } 139 - } 140 - 141 - this.#activeOutput.value = activeOutput; 142 - }; 143 - 144 - // SIGNALS 145 - 146 - #activeOutput = signal(/** @type {OutputElement<Encoding> | null} */ (null)); 147 - #setupFinished = signal(false); 148 - 149 - // STATE 150 - 151 - #outputs = /** @type {OutputElement<Encoding>[]} */ ([]); 152 - 153 - activeOutput = this.#activeOutput.get; 154 - 155 - // LIFECYCLE 156 - 157 - /** 158 - * @override 159 - */ 160 - async connectedCallback() { 161 - super.connectedCallback(); 162 - 163 - const children = Array.from(this.root().children); 164 - 165 - /** @type {OutputElement<Encoding>[]} */ 166 - const outputs = []; 167 - 168 - for (const el of children) { 169 - await customElements.whenDefined(el.localName); 170 - 171 - if ("nameWithGroup" in el && "tracks" in el) { 172 - outputs.push( 173 - /** @type {OutputElement<Encoding>} */ (/** @type {unknown} */ (el)), 174 - ); 175 - } 176 - } 177 - 178 - this.#outputs = outputs; 179 - this.#setupFinished.value = true; 180 - } 181 - 182 - // MISC 183 - 184 - /** 185 - * @override 186 - */ 187 - dependencies = () => { 188 - return Object.fromEntries( 189 - Array.from(this.root().children).flatMap((element) => { 190 - if (element.hasAttribute("id") === false) { 191 - console.warn( 192 - "Missing `id` for output-fallback configurator child element with `localName` '" + 193 - element.localName + "'", 194 - ); 195 - return []; 196 - } 197 - 198 - const d = /** @type {DiffuseElement} */ (element); 199 - return [[d.id, d]]; 200 - }), 201 - ); 202 - }; 203 - } 204 - 205 - export default OutputFallbackConfigurator; 206 - 207 - //////////////////////////////////////////// 208 - // REGISTER 209 - //////////////////////////////////////////// 210 - 211 - export const CLASS = OutputFallbackConfigurator; 212 - export const NAME = "dc-output-fallback"; 213 - 214 - customElements.define(NAME, CLASS);
-8
src/components/configurator/output-fallback/types.d.ts
··· 1 - import type { OutputElement } from "@components/output/types.d.ts"; 2 - import type { SignalReader } from "@common/signal.d.ts"; 3 - 4 - export type OutputFallbackConfiguratorElement<Encoding = null> = 5 - & OutputElement<Encoding | undefined> 6 - & { 7 - activeOutput: SignalReader<OutputElement<Encoding> | null>; 8 - };
+1 -1
src/components/input/s3/common.js
··· 49 49 return URI.serialize({ 50 50 scheme: SCHEME, 51 51 userinfo: `${bucket.accessKey}:${bucket.secretKey}`, 52 - host: bucket.host.replace(/^https?:\/\//, ""), 52 + host: bucket.host.replace(/^\w+:\/\//, ""), 53 53 path: path, 54 54 query: QS.stringify({ 55 55 bucketName: bucket.bucketName,
+19 -31
src/components/orchestrator/output/element.js
··· 2 2 import { DEFAULT_GROUP, DiffuseElement } from "@common/element.js"; 3 3 4 4 import "@components/configurator/output/element.js"; 5 - import "@components/configurator/output-fallback/element.js"; 6 5 import "@components/output/bytes/s3/element.js"; 7 6 import "@components/output/polymorphic/indexed-db/element.js"; 8 7 import "@components/output/raw/atproto/element.js"; 9 8 import "@components/transformer/output/bytes/automerge/element.js"; 10 9 import "@components/transformer/output/refiner/default/element.js"; 10 + import "@components/transformer/output/replicator/broadcast/element.js"; 11 11 import "@components/transformer/output/string/json/element.js"; 12 12 13 13 /** ··· 103 103 const group = this.group === DEFAULT_GROUP ? undefined : this.group; 104 104 105 105 return html` 106 - <!-- DEFAULT --> 106 + <!-- IDB-ONLY #2 --> 107 107 <dop-indexed-db 108 108 id="do-output__dop-indexed-db__json" 109 - group="${ifDefined(group)}" 110 109 namespace="json" 111 110 ></dop-indexed-db> 112 111 113 112 <!-- S3 #2 --> 114 - <dc-output-fallback 115 - id="do-output__dob-s3-fallback" 116 - > 117 - <dob-s3 118 - id="do-output__dob-s3" 119 - group="${ifDefined(group)}" 120 - ></dob-s3> 121 - <dop-indexed-db 122 - id="do-output__dop-indexed-db__s3" 123 - group="${ifDefined(group)}" 124 - namespace="s3" 125 - ></dop-indexed-db> 126 - </dc-output-fallback> 113 + <dob-s3 114 + id="do-output__dob-s3" 115 + ></dob-s3> 127 116 128 117 <!-- OUTPUT CONFIGURATOR --> 129 118 <dc-output ··· 131 120 default="do-output__dc-output__local" 132 121 group="${ifDefined(group)}" 133 122 > 123 + <!-- IDB-ONLY #1 --> 134 124 <dtos-json 135 125 id="do-output__dc-output__local" 136 126 output-selector="#do-output__dop-indexed-db__json" ··· 138 128 ></dtos-json> 139 129 140 130 <!-- ATProto --> 141 - <dc-output-fallback 131 + <dor-atproto 142 132 id="do-output__dc-output__atproto" 143 133 label="AT Protocol" 144 - > 145 - <dor-atproto 146 - id="do-output__dor-atproto" 147 - group="${ifDefined(group)}" 148 - ></dor-atproto> 149 - <dop-indexed-db 150 - id="do-output__dop-indexed-db__atproto" 151 - group="${ifDefined(group)}" 152 - namespace="atproto" 153 - ></dop-indexed-db> 154 - </dc-output-fallback> 134 + ></dor-atproto> 155 135 156 136 <!-- S3 #1 --> 157 137 <dtob-automerge 158 138 id="do-output__dc-output__s3" 159 - output-selector="#do-output__dob-s3-fallback" 139 + namespace="s3" 140 + output-selector="#do-output__dob-s3" 160 141 label="S3" 161 142 ></dtob-automerge> 162 143 </dc-output> 163 144 164 - <!-- Entry --> 145 + <!-- Refiner --> 165 146 <dtor-default 166 - id="do-output__output" 147 + id="do-output__dtor-default" 167 148 output-selector="#do-output__dc-output" 168 149 ></dtor-default> 150 + 151 + <!-- Entry ⬆️ --> 152 + <dtor-broadcast 153 + id="do-output__output" 154 + output-selector="#do-output__dtor-default" 155 + group="${ifDefined(group)}" 156 + ></dtor-broadcast> 169 157 `; 170 158 } 171 159 }
+21 -50
src/components/output/bytes/s3/element.js
··· 1 1 import * as IDB from "idb-keyval"; 2 2 3 - import { BroadcastableDiffuseElement } from "@common/element.js"; 3 + import { DiffuseElement } from "@common/element.js"; 4 4 import { computed, signal } from "@common/signal.js"; 5 5 import { outputManager } from "../../common.js"; 6 6 ··· 21 21 * @implements {OutputElement<Uint8Array | undefined>} 22 22 * @implements {S3OutputElement} 23 23 */ 24 - class S3Output extends BroadcastableDiffuseElement { 24 + class S3Output extends DiffuseElement { 25 25 static NAME = "diffuse/output/bytes/s3"; 26 26 static WORKER_URL = "components/output/bytes/s3/worker.js"; 27 27 ··· 64 64 this.tracks = this.#manager.tracks; 65 65 } 66 66 67 + // SIGNALS 68 + 69 + #isOnline = signal(navigator.onLine); 70 + 67 71 // STATE 68 72 69 73 ready = computed(() => { 70 - return this.#bucket.value !== undefined; 74 + return this.#bucket.value !== undefined && this.#isOnline.value; 71 75 }); 72 76 73 77 // LIFECYCLE ··· 76 80 * @override 77 81 */ 78 82 async connectedCallback() { 79 - // Broadcast if needed 80 - if (this.hasAttribute("group")) { 81 - // TODO: Get via leader? 82 - const actions = this.broadcast(this.nameWithGroup, { 83 - put: { strategy: "replicate", fn: this.#putIncoming }, 84 - }); 85 - 86 - if (actions) { 87 - this.#put = this.#putOutgoing(actions.put); 88 - } 89 - } 90 - 91 - // Super 92 83 super.connectedCallback(); 93 84 94 85 /** @type {Bucket | undefined} */ 95 86 const stored = await IDB.get(`${STORAGE_PREFIX}/bucket`); 96 87 if (stored) this.#bucket.value = stored; 88 + 89 + globalThis.addEventListener("online", this.#online); 90 + globalThis.addEventListener("offline", this.#offline); 97 91 } 92 + 93 + /** @override */ 94 + disconnectedCallback() { 95 + globalThis.removeEventListener("online", this.#online); 96 + globalThis.removeEventListener("offline", this.#offline); 97 + } 98 + 99 + #offline = () => this.#isOnline.set(false); 100 + #online = () => this.#isOnline.set(true); 98 101 99 102 // BUCKET 100 103 ··· 130 133 // GET & PUT 131 134 132 135 /** @param {string} name */ 133 - #getProxy = async (name) => { 136 + #get = async (name) => { 134 137 const bucket = await this.getBucket(); 135 138 if (!bucket) return undefined; 136 139 return this.proxy.get({ bucket, name: this.#cat(name) }); 137 140 }; 138 141 139 - #get = this.#getProxy; 140 - 141 142 /** @param {string} name; @param {any} data */ 142 - #putProxy = async (name, data) => { 143 + #put = async (name, data) => { 143 144 const bucket = await this.getBucket(); 144 145 if (!bucket) return undefined; 145 146 return this.proxy.put({ bucket, data, name: this.#cat(name) }); 146 147 }; 147 - 148 - #put = this.#putProxy; 149 - 150 - /** 151 - * @param {(uuidSender: ReturnType<typeof crypto.randomUUID>, name: string, data: any) => Promise<void>} action 152 - * @returns {(name: string, data: any) => Promise<void>} 153 - */ 154 - #putOutgoing = (action) => async (name, data) => { 155 - return await action(this.uuid, name, data); 156 - }; 157 - 158 - /** 159 - * @param {ReturnType<typeof crypto.randomUUID>} uuidSender 160 - * @param {string} name 161 - * @param {any} data 162 - */ 163 - #putIncoming(uuidSender, name, data) { 164 - if (uuidSender === this.uuid) { 165 - // Initiator 166 - this.#putProxy(name, data); 167 - } else { 168 - // Listener 169 - if (name === "facets") this.#manager.signals.facets.value = data; 170 - if (name === "playlistItems") { 171 - this.#manager.signals.playlistItems.value = data; 172 - } 173 - if (name === "themes") this.#manager.signals.themes.value = data; 174 - if (name === "tracks") this.#manager.signals.tracks.value = data; 175 - } 176 - } 177 148 178 149 // 🛠️ 179 150
+2 -2
src/components/output/bytes/s3/worker.js
··· 17 17 export async function get({ bucket, name }) { 18 18 const client = createClient(bucket); 19 19 const path = bucket.path.replace(/(^\/+|\/+$)/g, ""); 20 - const key = `${path}/${OBJECT_PREFIX}${name}`; 20 + const key = path ? `${path}/${OBJECT_PREFIX}${name}` : `${OBJECT_PREFIX}${name}`; 21 21 22 22 try { 23 23 const response = await client.getObject(key); ··· 34 34 export async function put({ bucket, data, name }) { 35 35 const client = createClient(bucket); 36 36 const path = bucket.path.replace(/(^\/+|\/+$)/g, ""); 37 - const key = `${path}/${OBJECT_PREFIX}${name}`; 37 + const key = path ? `${path}/${OBJECT_PREFIX}${name}` : `${OBJECT_PREFIX}${name}`; 38 38 39 39 await client.putObject(key, data); 40 40 }
+5 -60
src/components/output/polymorphic/indexed-db/element.js
··· 1 - import { BroadcastableDiffuseElement } from "@common/element.js"; 1 + import { DiffuseElement } from "@common/element.js"; 2 2 import { outputManager } from "../../common.js"; 3 3 4 4 /** ··· 14 14 /** 15 15 * @implements {OutputElement<SupportedDataTypes>} 16 16 */ 17 - class IndexedDBOutput extends BroadcastableDiffuseElement { 17 + class IndexedDBOutput extends DiffuseElement { 18 18 static NAME = "diffuse/output/polymorphic/indexed-db"; 19 19 static WORKER_URL = "components/output/polymorphic/indexed-db/worker.js"; 20 20 ··· 55 55 this.playlistItems = this.#manager.playlistItems; 56 56 this.themes = this.#manager.themes; 57 57 this.tracks = this.#manager.tracks; 58 + 58 59 this.ready = () => true; 59 60 } 60 61 61 - // LIFECYCLE 62 - 63 - /** 64 - * @override 65 - */ 66 - connectedCallback() { 67 - // Broadcast if needed 68 - if (this.hasAttribute("group")) { 69 - const actions = this.broadcast( 70 - `${this.nameWithGroup}${ 71 - this.namespace.length ? "/" + this.namespace.replace(/\/$/, "") : "" 72 - }`, 73 - { 74 - put: { strategy: "replicate", fn: this.#putIncoming }, 75 - }, 76 - ); 77 - 78 - if (actions) { 79 - this.#put = this.#putOutgoing(actions.put); 80 - } 81 - } 82 - 83 - // Super 84 - super.connectedCallback(); 85 - } 86 - 87 62 // GET & PUT 88 63 89 64 /** @param {string} name */ 90 - #getProxy = (name) => this.proxy.get({ name: this.#cat(name) }); 91 - #get = this.#getProxy; 65 + #get = (name) => this.proxy.get({ name: this.#cat(name) }); 92 66 93 67 /** @param {string} name; @param {any} data */ 94 - #putProxy = (name, data) => this.proxy.put({ name: this.#cat(name), data }); 95 - #put = this.#putProxy; 96 - 97 - /** 98 - * @param {(uuidSender: ReturnType<typeof crypto.randomUUID>, name: string, data: any) => Promise<void>} action 99 - * @returns {(name: string, data: any) => Promise<void>} 100 - */ 101 - #putOutgoing = (action) => async (name, data) => { 102 - return await action(this.uuid, name, data); 103 - }; 104 - 105 - /** 106 - * @param {ReturnType<typeof crypto.randomUUID>} uuidSender 107 - * @param {string} name 108 - * @param {any} data 109 - */ 110 - #putIncoming(uuidSender, name, data) { 111 - if (uuidSender === this.uuid) { 112 - // Initiator 113 - this.#putProxy(name, data); 114 - } else { 115 - // Listener 116 - if (name === "facets") this.#manager.signals.facets.value = data; 117 - if (name === "playlistItems") { 118 - this.#manager.signals.playlistItems.value = data; 119 - } 120 - if (name === "themes") this.#manager.signals.themes.value = data; 121 - if (name === "tracks") this.#manager.signals.tracks.value = data; 122 - } 123 - } 68 + #put = (name, data) => this.proxy.put({ name: this.#cat(name), data }); 124 69 125 70 // 🛠️ 126 71
+4 -58
src/components/output/raw/atproto/element.js
··· 1 1 import { Client, ClientResponseError, ok } from "@atcute/client"; 2 - import { BroadcastableDiffuseElement } from "@common/element.js"; 2 + 3 + import { DiffuseElement } from "@common/element.js"; 3 4 import { computed, signal } from "@common/signal.js"; 4 5 import { outputManager } from "../../common.js"; 5 6 import { ··· 24 25 /** 25 26 * @implements {ATProtoOutputElement} 26 27 */ 27 - class ATProtoOutput extends BroadcastableDiffuseElement { 28 + class ATProtoOutput extends DiffuseElement { 28 29 static NAME = "diffuse/output/raw/atproto"; 29 30 30 31 #manager; ··· 88 89 89 90 /** @override */ 90 91 connectedCallback() { 91 - if (this.hasAttribute("group")) { 92 - const actions = this.broadcast(this.nameWithGroup, { 93 - put: { strategy: "replicate", fn: this.#putIncoming }, 94 - }); 95 - 96 - if (actions) { 97 - this.#put = this.#putOutgoing(actions.put); 98 - } 99 - } 100 - 101 92 super.connectedCallback(); 102 93 103 94 this.#tryRestore(); ··· 259 250 * @param {string} collection 260 251 * @param {Array<{ id: string }>} data 261 252 */ 262 - async #putRecordsSync(collection, data) { 253 + async #putRecords(collection, data) { 263 254 if (!this.#rpc || !this.#did.value) return; 264 255 265 256 try { ··· 335 326 } 336 327 337 328 throw err; 338 - } 339 - } 340 - 341 - // GET & PUT (broadcasting layer) 342 - 343 - /** 344 - * @param {string} collection 345 - * @param {Array<{ id: string }>} data 346 - */ 347 - #putProxy = (collection, data) => this.#putRecordsSync(collection, data); 348 - #put = this.#putProxy; 349 - 350 - /** 351 - * @param {string} collection 352 - * @param {Array<{ id: string }>} data 353 - */ 354 - #putRecords = (collection, data) => this.#put(collection, data); 355 - 356 - /** 357 - * @param {(uuidSender: ReturnType<typeof crypto.randomUUID>, collection: string, data: Array<{ id: string }>) => Promise<void>} action 358 - * @returns {(collection: string, data: Array<{ id: string }>) => Promise<void>} 359 - */ 360 - #putOutgoing = (action) => async (collection, data) => { 361 - return await action(this.uuid, collection, data); 362 - }; 363 - 364 - /** 365 - * @param {ReturnType<typeof crypto.randomUUID>} uuidSender 366 - * @param {string} collection 367 - * @param {Array<{ id: string }>} data 368 - */ 369 - #putIncoming(uuidSender, collection, data) { 370 - if (uuidSender === this.uuid) { 371 - this.#putProxy(collection, data); 372 - } else { 373 - /** @type {Record<string, Signal<unknown[]>>} */ 374 - const collectionMap = { 375 - "sh.diffuse.output.facet": this.#manager.signals.facets, 376 - "sh.diffuse.output.playlistItem": this.#manager.signals.playlistItems, 377 - "sh.diffuse.output.theme": this.#manager.signals.themes, 378 - "sh.diffuse.output.track": this.#manager.signals.tracks, 379 - }; 380 - 381 - const sig = collectionMap[collection]; 382 - if (sig) sig.value = data; 383 329 } 384 330 } 385 331 }
+2 -2
src/components/transformer/output/base.js
··· 1 - import { DiffuseElement, query } from "@common/element.js"; 1 + import { BroadcastableDiffuseElement, query } from "@common/element.js"; 2 2 import { computed, signal } from "@common/signal.js"; 3 3 4 4 /** ··· 8 8 /** 9 9 * @template [T=null] 10 10 */ 11 - export class OutputTransformer extends DiffuseElement { 11 + export class OutputTransformer extends BroadcastableDiffuseElement { 12 12 // SIGNALS 13 13 14 14 #output = signal(/** @type {OutputElement<T> | undefined} */ (undefined));
+1 -1
src/components/transformer/output/bytes/automerge/constants.js
··· 13 13 ); 14 14 15 15 /** @type {Automerge.Doc<PlaylistItemsDocument>} */ 16 - export const INITIAL_PLAYLISTS_DOCUMENT = Automerge.load( 16 + export const INITIAL_PLAYLIST_ITEMS_DOCUMENT = Automerge.load( 17 17 base64.decode( 18 18 "hW9Kg5IPZcsAeAEQIyp0LRYp0l9bpZKWJXTPlgGtUD/lrIatFjiIwoUdtJhh/sBQFIcpPppxduoIp1ArXwYBAgMCEwIjBkACVgIHFQwhAiMCNAFCAlYCgAECfwB/AX8Bf8eTqcwGfwB/B38KY29sbGVjdGlvbn8AfwEBfwJ/AH8AAA", 19 19 ),
+156 -123
src/components/transformer/output/bytes/automerge/element.js
··· 1 1 import * as Automerge from "@automerge/automerge"; 2 + import { ifDefined } from "lit-html/directives/if-defined.js"; 2 3 import { isUint8Array } from "iso-base/utils"; 3 4 4 - import { computed } from "@common/signal.js"; 5 + import "@components/output/polymorphic/indexed-db/element.js"; 6 + 7 + import { computed, signal, untracked } from "@common/signal.js"; 5 8 import { 6 9 recursivelyCloneRecords, 7 10 removeUndefinedValuesFromRecord, ··· 9 12 import { OutputTransformer } from "../../base.js"; 10 13 import { 11 14 INITIAL_FACETS_DOCUMENT, 12 - INITIAL_PLAYLISTS_DOCUMENT, 15 + INITIAL_PLAYLIST_ITEMS_DOCUMENT, 13 16 INITIAL_THEMES_DOCUMENT, 14 17 INITIAL_TRACKS_DOCUMENT, 15 18 } from "./constants.js"; 16 19 17 20 /** 21 + * @import { RenderArg } from "@common/element.d.ts" 18 22 * @import { SignalReader } from "@common/signal.d.ts"; 19 - * @import { OutputManagerDeputy } from "@components/output/types.d.ts" 20 - * @import { FacetsDocument, PlaylistItemsDocument, ThemesDocument, TracksDocument } from "./types.d.ts" 23 + * @import { OutputElement } from "@components/output/types.d.ts"; 21 24 */ 22 25 23 26 /** ··· 27 30 constructor() { 28 31 super(); 29 32 30 - const base = this.base(); 33 + const remote = this.base(); 34 + const local = this.#localOutput.get; 31 35 32 - /** @type {SignalReader<Automerge.Doc<FacetsDocument>>} */ 33 - const facetsDocument = computed(() => { 34 - const value = base.facets.collection(); 36 + /** 37 + * @template T 38 + * @param {SignalReader<Uint8Array | undefined>} localCollection 39 + * @param {SignalReader<Uint8Array | undefined>} remoteCollection 40 + * @param {Automerge.Doc<T>} initial 41 + * @returns {SignalReader<Automerge.Doc<T>>} 42 + */ 43 + const mergedDoc = (localCollection, remoteCollection, initial) => 44 + computed(() => { 45 + const l = loadDocument(localCollection); 46 + const r = remote.ready() ? loadDocument(remoteCollection) : undefined; 35 47 36 - if (isUint8Array(value)) { 37 - return Automerge.load(value); 38 - } else if (value == undefined) { 39 - return INITIAL_FACETS_DOCUMENT; 40 - } else { 41 - // TODO: Better error 42 - throw new Error("Invalid data type"); 43 - } 44 - }); 48 + console.log("Local:", l); 49 + console.log("Remote:", r); 45 50 46 - /** @type {SignalReader<Automerge.Doc<PlaylistItemsDocument>>} */ 47 - const playlistsDocument = computed(() => { 48 - const value = base.playlistItems.collection(); 51 + if (!r) return l ?? initial; 52 + if (!l) return r; 49 53 50 - if (isUint8Array(value)) { 51 - return Automerge.load(value); 52 - } else if (value == undefined) { 53 - return INITIAL_PLAYLISTS_DOCUMENT; 54 - } else { 55 - // TODO: Better error 56 - throw new Error("Invalid data type"); 57 - } 58 - }); 54 + console.log("Merging"); 55 + return Automerge.merge(Automerge.clone(l), Automerge.clone(r)); 56 + }); 59 57 60 - /** @type {SignalReader<Automerge.Doc<ThemesDocument>>} */ 61 - const themesDocument = computed(() => { 62 - const value = base.themes.collection(); 58 + const facetsDoc = mergedDoc( 59 + computed(() => local()?.facets?.collection()), 60 + remote.facets.collection, 61 + INITIAL_FACETS_DOCUMENT, 62 + ); 63 63 64 - if (isUint8Array(value)) { 65 - return Automerge.load(value); 66 - } else if (value == undefined) { 67 - return INITIAL_THEMES_DOCUMENT; 68 - } else { 69 - // TODO: Better error 70 - throw new Error("Invalid data type"); 71 - } 72 - }); 64 + const playlistItemsDoc = mergedDoc( 65 + computed(() => local()?.playlistItems?.collection()), 66 + remote.playlistItems.collection, 67 + INITIAL_PLAYLIST_ITEMS_DOCUMENT, 68 + ); 73 69 74 - /** @type {SignalReader<Automerge.Doc<TracksDocument>>} */ 75 - const tracksDocument = computed(() => { 76 - const value = base.tracks.collection(); 70 + const themesDoc = mergedDoc( 71 + computed(() => local()?.themes?.collection()), 72 + remote.themes.collection, 73 + INITIAL_THEMES_DOCUMENT, 74 + ); 77 75 78 - if (isUint8Array(value)) { 79 - return Automerge.load(value); 80 - } else if (value == undefined) { 81 - return INITIAL_TRACKS_DOCUMENT; 82 - } else { 83 - // TODO: Better error 84 - throw new Error("Invalid data type"); 85 - } 86 - }); 76 + const tracksDoc = mergedDoc( 77 + computed(() => local()?.tracks?.collection()), 78 + remote.tracks.collection, 79 + INITIAL_TRACKS_DOCUMENT, 80 + ); 87 81 88 - /** @type {OutputManagerDeputy} */ 89 - const manager = { 90 - facets: { 91 - ...base.facets, 92 - collection: computed(() => facetsDocument().collection), 93 - save: async (newFacets) => { 94 - const doc = Automerge.change(facetsDocument(), (d) => { 95 - const clonedCollection = newFacets.map((facet) => { 96 - return removeUndefinedValuesFromRecord( 97 - recursivelyCloneRecords(facet), 98 - ); 99 - }); 82 + this.facets = automergeEntry( 83 + computed(() => local()?.facets), 84 + remote.facets, 85 + facetsDoc, 86 + { 87 + stripUndefined: true, 88 + }, 89 + ); 100 90 101 - d.collection = clonedCollection; 102 - }); 91 + this.playlistItems = automergeEntry( 92 + computed(() => local()?.playlistItems), 93 + remote.playlistItems, 94 + playlistItemsDoc, 95 + ); 103 96 104 - const bytes = Automerge.save(doc); 105 - await base.facets.save(bytes); 106 - }, 97 + this.themes = automergeEntry( 98 + computed(() => local()?.themes), 99 + remote.themes, 100 + themesDoc, 101 + { 102 + stripUndefined: true, 107 103 }, 108 - playlistItems: { 109 - ...base.playlistItems, 110 - collection: computed(() => playlistsDocument().collection), 111 - save: async (newPlaylistItems) => { 112 - const doc = Automerge.change(playlistsDocument(), (d) => { 113 - const clonedCollection = newPlaylistItems.map((item) => { 114 - return recursivelyCloneRecords(item); 115 - }); 104 + ); 105 + 106 + this.tracks = automergeEntry( 107 + computed(() => local()?.tracks), 108 + remote.tracks, 109 + tracksDoc, 110 + ); 116 111 117 - d.collection = clonedCollection; 118 - }); 112 + this.ready = () => true; 113 + } 119 114 120 - const bytes = Automerge.save(doc); 121 - await base.playlistItems.save(bytes); 122 - }, 123 - }, 124 - themes: { 125 - ...base.themes, 126 - collection: computed(() => themesDocument().collection), 127 - save: async (newThemes) => { 128 - const doc = Automerge.change(themesDocument(), (d) => { 129 - const clonedCollection = newThemes.map((theme) => { 130 - return removeUndefinedValuesFromRecord( 131 - recursivelyCloneRecords(theme), 132 - ); 133 - }); 115 + // SIGNALS 134 116 135 - d.collection = clonedCollection; 136 - }); 117 + #localOutput = signal( 118 + /** @type {OutputElement<Uint8Array | undefined> | undefined} */ (undefined), 119 + ); 137 120 138 - const bytes = Automerge.save(doc); 139 - await base.themes.save(bytes); 140 - }, 141 - }, 142 - tracks: { 143 - ...base.tracks, 144 - collection: computed(() => tracksDocument().collection), 145 - save: async (newTracks) => { 146 - const doc = Automerge.change(tracksDocument(), (d) => { 147 - const clonedCollection = newTracks.map((track) => { 148 - return recursivelyCloneRecords(track); 149 - }); 121 + // LIFECYCLE 122 + 123 + /** 124 + * @override 125 + */ 126 + connectedCallback() { 127 + super.connectedCallback(); 150 128 151 - d.collection = clonedCollection; 152 - }); 129 + /** @type {OutputElement<Uint8Array | undefined> | null} */ 130 + const local = this.root().querySelector("dop-indexed-db"); 131 + if (!local) throw new Error("Can't find local output"); 153 132 154 - const bytes = Automerge.save(doc); 155 - await base.tracks.save(bytes); 156 - }, 157 - }, 133 + // When defined 134 + customElements.whenDefined(local.localName).then(() => { 135 + this.#localOutput.value = local; 136 + }); 137 + } 158 138 159 - // Other 160 - ready: base.ready, 161 - }; 139 + // RENDER 162 140 163 - // Assign manager properties to class 164 - this.facets = manager.facets; 165 - this.playlistItems = manager.playlistItems; 166 - this.themes = manager.themes; 167 - this.tracks = manager.tracks; 168 - this.ready = manager.ready; 141 + /** 142 + * @param {RenderArg} _ 143 + */ 144 + render({ html }) { 145 + return html` 146 + <dop-indexed-db 147 + namespace="${ifDefined(this.getAttribute(`namespace`))}" 148 + ></dop-indexed-db> 149 + `; 169 150 } 170 151 } 171 152 172 153 export default AutomergeBytesOutputTransformer; 154 + 155 + //////////////////////////////////////////// 156 + // 🛠️ 157 + //////////////////////////////////////////// 158 + 159 + /** 160 + * @template T 161 + * @param {SignalReader<Uint8Array | undefined>} source 162 + * @returns {Automerge.Doc<T> | undefined} 163 + */ 164 + export function loadDocument(source) { 165 + const value = source(); 166 + 167 + if (isUint8Array(value)) { 168 + return Automerge.load(value); 169 + } else if (value == undefined) { 170 + return undefined; 171 + } else { 172 + throw new Error("Invalid data type"); 173 + } 174 + } 175 + 176 + /** 177 + * @template {Record<string, any>} T 178 + * @param {SignalReader<{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> } | undefined>} local 179 + * @param {{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} remote 180 + * @param {SignalReader<Automerge.Doc<{ collection: T[] }>>} document 181 + * @param {{ stripUndefined?: boolean }} [opts] 182 + * @returns {{ collection: SignalReader<T[]>, reload: () => Promise<void>, save: (items: T[]) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} 183 + */ 184 + export function automergeEntry(local, remote, document, opts) { 185 + return { 186 + collection: computed(() => document().collection), 187 + reload: remote.reload, 188 + save: async (/** @type {T[]} */ newItems) => { 189 + const doc = Automerge.change(document(), (d) => { 190 + d.collection = newItems.map((item) => { 191 + const cloned = recursivelyCloneRecords(item); 192 + return opts?.stripUndefined 193 + ? removeUndefinedValuesFromRecord(cloned) 194 + : cloned; 195 + }); 196 + }); 197 + 198 + const bytes = Automerge.save(doc); 199 + 200 + await untracked(local)?.save(bytes); 201 + await remote.save(bytes); 202 + }, 203 + state: computed(() => local()?.state() ?? "sleeping"), 204 + }; 205 + } 173 206 174 207 //////////////////////////////////////////// 175 208 // REGISTER
+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);
-2
src/facets/l/index.js
··· 14 14 document.querySelector("#container") 15 15 ); 16 16 17 - console.log(facet); 18 - 19 17 const range = document.createRange(); 20 18 range.selectNode(container); 21 19 const documentFragment = range.createContextualFragment(facet.html ?? "");
+4 -2
src/themes/webamp/configurators/input/element.js
··· 7 7 import { signal } from "@common/signal.js"; 8 8 9 9 import { buildURI as buildOpenSubsonicURI } from "@components/input/opensubsonic/common.js"; 10 - import { buildURI as buildS3cURI } from "@components/input/s3/common.js"; 10 + import { buildURI as buildS3URI } from "@components/input/s3/common.js"; 11 11 12 12 import { SCHEME as HTTPS_SCHEME } from "@components/input/https/constants.js"; 13 13 import { SCHEME as OPENSUBSONIC_SCHEME } from "@components/input/opensubsonic/constants.js"; ··· 148 148 if (!accessKey) { 149 149 throw new Error("Missing required `accessKey` input value"); 150 150 } 151 + 151 152 if (!bucketName) { 152 153 throw new Error("Missing required `bucketName` input value"); 153 154 } 155 + 154 156 if (!secretKey) { 155 157 throw new Error("Missing required `secretKey` input value"); 156 158 } ··· 165 167 secretKey, 166 168 }; 167 169 168 - const uri = buildS3cURI(bucket); 170 + const uri = buildS3URI(bucket); 169 171 await this.addSource(uri); 170 172 171 173 if (button) button.disabled = false;
+1 -1
src/themes/webamp/configurators/output/element.js
··· 157 157 const bucket = { 158 158 accessKey, 159 159 bucketName, 160 - host: host?.length ? host : "s3.amazonaws.com", 160 + host: host?.length ? host.replace(/^\w+:\/\//, "") : "s3.amazonaws.com", 161 161 path: path?.length ? path : "/", 162 162 region: region?.length ? region : "us-east-1", 163 163 secretKey,