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

Configure Feed

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

chore: remove automerge-repo experiment

-377
-3
deno.jsonc
··· 11 11 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.7", 12 12 "@atcute/oauth-browser-client": "npm:@atcute/oauth-browser-client@^3.0.0", 13 13 "@automerge/automerge": "npm:@automerge/automerge@^3.2.3", 14 - "@automerge/automerge-repo": "npm:@automerge/automerge-repo@^2.5.1", 15 - "@automerge/automerge-repo-network-broadcastchannel": "npm:@automerge/automerge-repo-network-broadcastchannel@^2.5.1", 16 - "@automerge/automerge-repo-storage-indexeddb": "npm:@automerge/automerge-repo-storage-indexeddb@^2.5.1", 17 14 "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.9.5", 18 15 "@char/cbor": "jsr:@char/cbor@^0.1.4", 19 16 "@codemirror/autocomplete": "npm:@codemirror/autocomplete@^6.20.0",
-371
src/components/output/bytes/automerge-repo-server/element.js
··· 1 - import * as Automerge from "@automerge/automerge"; 2 - import { decodeCBOR, encodeCBOR } from "@char/cbor"; 3 - import bs58check from "bs58check"; 4 - 5 - import { DiffuseElement } from "@common/element.js"; 6 - import { outputManager } from "../../common.js"; 7 - 8 - /** 9 - * @import { DocumentId, PeerId } from "@automerge/automerge-repo" 10 - * @import { OutputElement, OutputManager, OutputManagerProperties } from "../../types.d.ts" 11 - */ 12 - 13 - /** 14 - * @typedef {{ collection: Uint8Array }} CollectionDocument 15 - */ 16 - 17 - const DOC_IDS_STORAGE_KEY = "diffuse/output/automerge-repo-server/doc-ids"; 18 - const PROTOCOL_VERSION = "1"; 19 - 20 - const COLLECTIONS = /** @type {const} */ ([ 21 - "facets", 22 - "playlistItems", 23 - "themes", 24 - "tracks", 25 - ]); 26 - 27 - //////////////////////////////////////////// 28 - // ELEMENT 29 - //////////////////////////////////////////// 30 - 31 - /** 32 - * Syncs raw Automerge bytes with an automerge-repo sync server 33 - * over WebSocket, without depending on automerge-repo as a client. 34 - * 35 - * Uses the automerge-repo wire protocol (CBOR-framed messages with 36 - * join handshake) and the standard Automerge sync algorithm. 37 - * 38 - * @implements {OutputElement<Uint8Array>} 39 - */ 40 - class AutomergeRepoServerOutput extends DiffuseElement { 41 - static NAME = "diffuse/output/bytes/automerge-repo-server"; 42 - 43 - /** @type {WebSocket | undefined} */ 44 - #ws; 45 - 46 - /** @type {PeerId} */ 47 - #peerId = /** @type {PeerId} */ (`diffuse-${crypto.randomUUID()}`); 48 - 49 - /** @type {PeerId | undefined} */ 50 - #serverPeerId; 51 - 52 - /** @type {Record<string, Automerge.Doc<CollectionDocument>>} */ 53 - #docs = {}; 54 - 55 - /** @type {Record<string, Automerge.SyncState>} */ 56 - #syncStates = {}; 57 - 58 - /** @type {Record<string, DocumentId>} */ 59 - #docIds = {}; 60 - 61 - #manager; 62 - 63 - constructor() { 64 - super(); 65 - 66 - this.#ensureDocs(); 67 - 68 - /** @type {OutputManagerProperties<Uint8Array>} */ 69 - const properties = { 70 - facets: { 71 - empty: () => this.#getBytes("facets"), 72 - get: async () => this.#getBytes("facets"), 73 - put: async (data) => this.#putBytes("facets", data), 74 - }, 75 - init: () => this.whenConnected(), 76 - playlistItems: { 77 - empty: () => this.#getBytes("playlistItems"), 78 - get: async () => this.#getBytes("playlistItems"), 79 - put: async (data) => this.#putBytes("playlistItems", data), 80 - }, 81 - themes: { 82 - empty: () => this.#getBytes("themes"), 83 - get: async () => this.#getBytes("themes"), 84 - put: async (data) => this.#putBytes("themes", data), 85 - }, 86 - tracks: { 87 - empty: () => this.#getBytes("tracks"), 88 - get: async () => this.#getBytes("tracks"), 89 - put: async (data) => this.#putBytes("tracks", data), 90 - }, 91 - }; 92 - 93 - this.#manager = outputManager(properties); 94 - 95 - this.facets = this.#manager.facets; 96 - this.playlistItems = this.#manager.playlistItems; 97 - this.themes = this.#manager.themes; 98 - this.tracks = this.#manager.tracks; 99 - this.ready = () => true; 100 - } 101 - 102 - // LIFECYCLE 103 - 104 - /** 105 - * @override 106 - */ 107 - connectedCallback() { 108 - super.connectedCallback(); 109 - this.#loadDocIds(); 110 - this.#connect(); 111 - } 112 - 113 - /** 114 - * @override 115 - */ 116 - disconnectedCallback() { 117 - super.disconnectedCallback(); 118 - this.#ws?.close(); 119 - this.#ws = undefined; 120 - } 121 - 122 - // DOCUMENT MANAGEMENT 123 - 124 - #loadDocIds() { 125 - const namespace = this.getAttribute("namespace") ?? "automerge-repo-server"; 126 - const storageKey = `${DOC_IDS_STORAGE_KEY}/${namespace}`; 127 - const stored = localStorage.getItem(storageKey); 128 - 129 - if (stored) { 130 - this.#docIds = JSON.parse(stored); 131 - } 132 - 133 - // Ensure every collection has a document ID 134 - for (const name of COLLECTIONS) { 135 - if (!this.#docIds[name]) { 136 - const bytes = crypto.getRandomValues(new Uint8Array(16)); 137 - 138 - // Set UUID v4 version and variant bits 139 - bytes[6] = (bytes[6] & 0x0f) | 0x40; 140 - bytes[8] = (bytes[8] & 0x3f) | 0x80; 141 - 142 - const docId = /** @type {DocumentId} */ (bs58check.encode(bytes)); 143 - const url = `automerge:${docId}`; 144 - 145 - this.#docIds[name] = docId; 146 - } 147 - } 148 - 149 - localStorage.setItem(storageKey, JSON.stringify(this.#docIds)); 150 - } 151 - 152 - #ensureDocs() { 153 - for (const name of COLLECTIONS) { 154 - if (!this.#docs[name]) { 155 - this.#docs[name] = Automerge.init(); 156 - } 157 - if (!this.#syncStates[name]) { 158 - this.#syncStates[name] = Automerge.initSyncState(); 159 - } 160 - } 161 - } 162 - 163 - /** 164 - * @param {string} name 165 - * @returns {Uint8Array} 166 - */ 167 - #getBytes(name) { 168 - const doc = this.#docs[name]; 169 - if (doc) return Automerge.save(doc); 170 - return new Uint8Array(); 171 - } 172 - 173 - /** 174 - * @param {string} name 175 - * @param {Uint8Array} data 176 - */ 177 - #putBytes(name, data) { 178 - if (data.byteLength > 0) { 179 - this.#docs[name] = Automerge.load(data); 180 - } else { 181 - this.#docs[name] = Automerge.init(); 182 - } 183 - 184 - this.#syncDoc(name); 185 - } 186 - 187 - // WEBSOCKET CONNECTION 188 - 189 - #connect() { 190 - const url = this.getAttribute("url"); 191 - if (!url) return; 192 - 193 - const ws = new WebSocket(url); 194 - ws.binaryType = "arraybuffer"; 195 - this.#ws = ws; 196 - 197 - ws.addEventListener("open", () => { 198 - this.#sendJoin(); 199 - }); 200 - 201 - ws.addEventListener("message", (event) => { 202 - const msg = this.#cborDecode(new Uint8Array(event.data)); 203 - this.#handleMessage(msg); 204 - }); 205 - 206 - ws.addEventListener("close", () => { 207 - this.#serverPeerId = undefined; 208 - this.#scheduleReconnect(); 209 - }); 210 - 211 - ws.addEventListener("error", () => { 212 - ws.close(); 213 - }); 214 - } 215 - 216 - #scheduleReconnect() { 217 - if (!this.isConnected) return; 218 - setTimeout(() => { 219 - if (this.isConnected) this.#connect(); 220 - }, 5000); 221 - } 222 - 223 - // PROTOCOL 224 - 225 - #sendJoin() { 226 - this.#send({ 227 - type: "join", 228 - senderId: this.#peerId, 229 - peerMetadata: { storageId: undefined, isEphemeral: true }, 230 - supportedProtocolVersions: [PROTOCOL_VERSION], 231 - }); 232 - } 233 - 234 - /** 235 - * @param {any} msg 236 - */ 237 - #handleMessage(msg) { 238 - switch (msg.type) { 239 - case "peer": 240 - this.#serverPeerId = msg.senderId; 241 - this.#syncAllDocs(); 242 - break; 243 - 244 - case "sync": 245 - case "request": 246 - this.#handleSyncMessage(msg); 247 - break; 248 - 249 - case "doc-unavailable": 250 - // Server doesn't have this doc; that's fine, we'll push ours 251 - break; 252 - 253 - case "error": 254 - console.error("[automerge-repo-server]", msg.message); 255 - break; 256 - } 257 - } 258 - 259 - /** 260 - * @param {{ documentId: DocumentId, data: Uint8Array }} msg 261 - */ 262 - #handleSyncMessage(msg) { 263 - const name = this.#nameForDocId(msg.documentId); 264 - if (!name) return; 265 - 266 - const doc = this.#docs[name] ?? Automerge.init(); 267 - const syncState = this.#syncStates[name] ?? Automerge.initSyncState(); 268 - 269 - const [newDoc, newSyncState] = Automerge.receiveSyncMessage( 270 - doc, 271 - syncState, 272 - msg.data, 273 - ); 274 - 275 - this.#docs[name] = newDoc; 276 - this.#syncStates[name] = newSyncState; 277 - 278 - // Update the output manager signal with fresh bytes 279 - // @ts-ignore Not sure what type to use here 280 - this.#manager.signals[name].value = Automerge.save(newDoc); 281 - 282 - // Continue the sync round-trip 283 - this.#syncDoc(name); 284 - } 285 - 286 - // SYNC 287 - 288 - #syncAllDocs() { 289 - for (const name of COLLECTIONS) { 290 - this.#syncDoc(name); 291 - } 292 - } 293 - 294 - /** 295 - * @param {string} name 296 - */ 297 - #syncDoc(name) { 298 - if (!this.#serverPeerId) return; 299 - 300 - const doc = this.#docs[name]; 301 - const syncState = this.#syncStates[name] ?? Automerge.initSyncState(); 302 - 303 - if (!doc) return; 304 - 305 - const [newSyncState, syncMessage] = Automerge.generateSyncMessage( 306 - doc, 307 - syncState, 308 - ); 309 - 310 - this.#syncStates[name] = newSyncState; 311 - 312 - if (syncMessage) { 313 - this.#send({ 314 - type: "sync", 315 - senderId: this.#peerId, 316 - targetId: this.#serverPeerId, 317 - documentId: this.#docIds[name], 318 - data: syncMessage, 319 - }); 320 - } 321 - } 322 - 323 - // HELPERS 324 - 325 - /** 326 - * @template [T=unknown] 327 - * @param {Uint8Array} data 328 - * @returns {T} 329 - */ 330 - #cborDecode(data) { 331 - return /** @type {T} */ (decodeCBOR(data)); 332 - } 333 - 334 - /** 335 - * @param {unknown} data 336 - */ 337 - #cborEncode(data) { 338 - return encodeCBOR(data); 339 - } 340 - 341 - /** 342 - * @param {object} msg 343 - */ 344 - #send(msg) { 345 - if (this.#ws?.readyState === WebSocket.OPEN) { 346 - this.#ws.send(this.#cborEncode(msg)); 347 - } 348 - } 349 - 350 - /** 351 - * @param {DocumentId} documentId 352 - * @returns {string | undefined} 353 - */ 354 - #nameForDocId(documentId) { 355 - for (const name of COLLECTIONS) { 356 - if (this.#docIds[name] === documentId) return name; 357 - } 358 - return undefined; 359 - } 360 - } 361 - 362 - export default AutomergeRepoServerOutput; 363 - 364 - //////////////////////////////////////////// 365 - // REGISTER 366 - //////////////////////////////////////////// 367 - 368 - export const CLASS = AutomergeRepoServerOutput; 369 - export const NAME = "dob-automerge-repo-server"; 370 - 371 - customElements.define(NAME, AutomergeRepoServerOutput);
-3
src/index.vto
··· 90 90 desc: "Supplies the tracks from the given output to the given search processor whenever the tracks collection changes. Additionally it can perform a search and other ways to reduce the scope of tracks based on the given scope engine. Provides a `tracks` signal similar to `output.tracks.collection`" 91 91 92 92 output: 93 - - title: "Bytes / Automerge Repo" 94 - desc: "Sync with an Automerge repo [sync server](https://github.com/automerge/automerge-repo-sync-server/tree/main)." 95 - todo: true 96 93 - url: "components/output/polymorphic/indexed-db/element.js" 97 94 title: "Polymorphic / IndexedDB" 98 95 desc: "Stores output into the local indexedDB. Supports any type of data that indexedDB supports."