# Diffuse Elements

## Artwork
### Audio Metadata

Extracts embedded artwork from audio files using the music-metadata library.

```js
import { defineElement, DiffuseElement, query } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {InputElement} from "@specs/components/input/types.d.ts"
 * @import {Actions} from "@specs/components/artwork/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class AudioMetadataArtwork extends DiffuseElement {
  static NAME = "diffuse/artwork/audio-metadata";
  static WORKER_URL = "components/artwork/audio-metadata/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const p = this.workerProxy();

    this.get = p.get;
  }

  // LIFECYCLE

  /** @override */
  async connectedCallback() {
    super.connectedCallback();

    /** @type {InputElement} */
    this.input = query(this, "input-selector");

    await customElements.whenDefined(this.input.localName);
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    if (!this.input) throw new Error("Input element not defined yet");
    return { input: this.input };
  }
}

export default AudioMetadataArtwork;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = AudioMetadataArtwork;
export const NAME = "da-audio-metadata";

defineElement(NAME, AudioMetadataArtwork);
```
### Input

Fetches artwork by delegating to the configured input element's artwork method.

```js
import { defineElement, DiffuseElement, query } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {InputElement} from "@specs/components/input/types.d.ts"
 * @import {Actions} from "@specs/components/artwork/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class InputArtwork extends DiffuseElement {
  static NAME = "diffuse/artwork/input";
  static WORKER_URL = "components/artwork/input/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const p = this.workerProxy();

    this.get = p.get;
  }

  // LIFECYCLE

  /** @override */
  async connectedCallback() {
    super.connectedCallback();

    /** @type {InputElement} */
    this.input = query(this, "input-selector");

    await customElements.whenDefined(this.input.localName);
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    if (!this.input) throw new Error("Input element not defined yet");
    return { input: this.input };
  }
}

export default InputArtwork;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = InputArtwork;
export const NAME = "da-input";

defineElement(NAME, InputArtwork);
```
### Last.fm

Fetches cover art from the Last.fm API using track artist and album tags.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Actions} from "@specs/components/artwork/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class LastFmArtwork extends DiffuseElement {
  static NAME = "diffuse/artwork/last.fm";
  static WORKER_URL = "components/artwork/last.fm/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const p = this.workerProxy();

    this.get = p.get;
  }
}

export default LastFmArtwork;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = LastFmArtwork;
export const NAME = "da-lastfm";

defineElement(NAME, LastFmArtwork);
```
### MusicBrainz

Fetches cover art from MusicBrainz and the Cover Art Archive using track artist and album tags.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Actions} from "@specs/components/artwork/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class MusicBrainzArtwork extends DiffuseElement {
  static NAME = "diffuse/artwork/musicbrainz";
  static WORKER_URL = "components/artwork/musicbrainz/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const p = this.workerProxy();

    this.get = p.get;
  }
}

export default MusicBrainzArtwork;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = MusicBrainzArtwork;
export const NAME = "da-musicbrainz";

defineElement(NAME, MusicBrainzArtwork);
```
## Configurators
### Artwork

Takes artwork components as children and tries each in sequence, returning the first non-null result.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {ArtworkElement} from "@specs/components/artwork/types.d.ts"
 * @import {Actions} from "@specs/components/configurator/artwork/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class ArtworkConfigurator extends DiffuseElement {
  static NAME = "diffuse/configurator/artwork";
  static WORKER_URL = "components/configurator/artwork/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const proxy = this.workerProxy();

    this.get = proxy.get;
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    return Object.fromEntries(
      Array.from(this.children).map((element) => {
        const artwork = /** @type {ArtworkElement} */ (element);
        return [artwork.localName, artwork];
      }),
    );
  }
}

export default ArtworkConfigurator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ArtworkConfigurator;
export const NAME = "dc-artwork";

defineElement(NAME, ArtworkConfigurator);
```
### Input

Allows for multiple inputs to be used at once.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";

/**
 * @import {ProxiedActions, Tunnel} from "~/common/worker.d.ts"
 * @import {InputElement} from "@specs/components/input/types.d.ts"
 * @import {Actions} from "@specs/components/configurator/input/types.d.ts"
 */

/**
 * @typedef {{ element: InputElement, tunnel: Tunnel, worker: Worker | SharedWorker }} Input
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class InputConfigurator extends DiffuseElement {
  static NAME = "diffuse/configurator/input";
  static WORKER_URL = "components/configurator/input/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const proxy = this.workerProxy();

    this.artwork = proxy.artwork;
    this.consult = proxy.consult;
    this.detach = proxy.detach;
    this.groupConsult = proxy.groupConsult;
    this.list = proxy.list;
    this.resolve = proxy.resolve;

    this.cache = proxy.cache;
    this.cacheBlob = proxy.cacheBlob;
    this.listCached = proxy.listCached;
    this.removeFromCache = proxy.removeFromCache;
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    return this.inputs();
  }

  inputs() {
    return Object.fromEntries(
      Array.from(this.children).map((element) => {
        const input = /** @type {InputElement} */ (element);
        return [input.SCHEME, input];
      }),
    );
  }
}

export default InputConfigurator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = InputConfigurator;
export const NAME = "dc-input";

defineElement(NAME, CLASS);
```
### Metadata

Takes metadata components as children and chains their patches in sequence.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {MetadataElement} from "@specs/components/metadata/types.d.ts"
 * @import {Actions} from "@specs/components/configurator/metadata/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class MetadataConfigurator extends DiffuseElement {
  static NAME = "diffuse/configurator/metadata";
  static WORKER_URL = "components/configurator/metadata/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const proxy = this.workerProxy();

    this.patch = proxy.patch;
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    return Object.fromEntries(
      Array.from(this.children).map((element) => {
        const metadata = /** @type {MetadataElement} */ (element);
        return [metadata.localName, metadata];
      }),
    );
  }
}

export default MetadataConfigurator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = MetadataConfigurator;
export const NAME = "dc-metadata";

defineElement(NAME, MetadataConfigurator);
```
### Output

Enables the user to configure a specific output. If no default output is set, it creates a temporary session by storing everything in memory.

```js
import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js";
import { batch, computed, signal } from "~/common/signal.js";

/**
 * @import {DiffuseElement} from "~/common/element.js"
 * @import {Facet, PlaylistItem, Setting, Track} from "~/definitions/types.d.ts"
 * @import {OutputManagerDeputy, OutputElement} from "@specs/components/output/types.d.ts"
 *
 * @import {OutputConfiguratorElement} from "@specs/components/configurator/output/types.d.ts"
 */

/**
 * @typedef {OutputElement} Output
 */

const STORAGE_PREFIX = "diffuse/configurator/output";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {OutputConfiguratorElement}
 */
class OutputConfigurator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/configurator/output";

  constructor() {
    super();

    /** @type {OutputManagerDeputy} */
    const manager = {
      facets: {
        collection: computed(() => {
          const out = this.#selected.value;
          if (out) return out.facets.collection();

          const def = this.#defaultOutput.value;
          if (def) return def.facets.collection();
          if (this.hasDefault()) return { state: "loading" };

          return this.#setupFinished.value
            ? { state: "loaded", data: this.#memory.facets.value }
            : { state: "loading" };
        }),
        reload: () => {
          const def = this.#defaultOutput.value;
          if (def) def.facets.reload();

          const out = this.#selected.value;
          if (out) return out.facets.reload();

          return Promise.resolve();
        },
        save: async (newFacets) => {
          const out = this.#selected.value;
          if (out) return await out.facets.save(newFacets);

          const def = this.#defaultOutput.value;
          if (def) return await def.facets.save(newFacets);

          this.#memory.facets.value = newFacets;
        },
      },
      playlistItems: {
        collection: computed(() => {
          const out = this.#selected.value;
          if (out) return out.playlistItems.collection();

          const def = this.#defaultOutput.value;
          if (def) return def.playlistItems.collection();
          if (this.hasDefault()) return { state: "loading" };

          return this.#setupFinished.value
            ? { state: "loaded", data: this.#memory.playlistItems.value }
            : { state: "loading" };
        }),
        reload: () => {
          const def = this.#defaultOutput.value;
          if (def) def.playlistItems.reload();

          const out = this.#selected.value;
          if (out) return out.playlistItems.reload();

          return Promise.resolve();
        },
        save: async (newPlaylistItems) => {
          const out = this.#selected.value;
          if (out) return await out.playlistItems.save(newPlaylistItems);

          const def = this.#defaultOutput.value;
          if (def) return await def.playlistItems.save(newPlaylistItems);

          this.#memory.playlistItems.value = newPlaylistItems;
        },
      },
      settings: {
        collection: computed(() => {
          const out = this.#selected.value;
          if (out) return out.settings.collection();

          const def = this.#defaultOutput.value;
          if (def) return def.settings.collection();
          if (this.hasDefault()) return { state: "loading" };

          return this.#setupFinished.value
            ? { state: "loaded", data: this.#memory.settings.value }
            : { state: "loading" };
        }),
        reload: () => {
          const def = this.#defaultOutput.value;
          if (def) def.settings.reload();

          const out = this.#selected.value;
          if (out) return out.settings.reload();

          return Promise.resolve();
        },
        save: async (newSettings) => {
          const out = this.#selected.value;
          if (out) return await out.settings.save(newSettings);

          const def = this.#defaultOutput.value;
          if (def) return await def.settings.save(newSettings);

          this.#memory.settings.value = newSettings;
        },
      },
      tracks: {
        collection: computed(() => {
          const out = this.#selected.value;
          if (out) return out.tracks.collection();

          const def = this.#defaultOutput.value;
          if (def) return def.tracks.collection();
          if (this.hasDefault()) return { state: "loading" };

          return this.#setupFinished.value
            ? { state: "loaded", data: this.#memory.tracks.value }
            : { state: "loading" };
        }),
        reload: () => {
          const def = this.#defaultOutput.value;
          if (def) def.tracks.reload();

          const out = this.#selected.value;
          if (out) return out.tracks.reload();

          return Promise.resolve();
        },
        save: async (newTracks) => {
          const out = this.#selected.value;
          if (out) return await out.tracks.save(newTracks);

          const def = this.#defaultOutput.value;
          if (def) return await def.tracks.save(newTracks);

          this.#memory.tracks.value = newTracks;
        },
      },

      // Other
      ready: computed(() => {
        const out = this.#selected.value;
        if (out) return out.ready();

        const def = this.#defaultOutput.value;
        if (def) return def.ready();

        return this.#setupFinished.value;
      }),
    };

    // Assign manager properties to class
    this.facets = manager.facets;
    this.playlistItems = manager.playlistItems;
    this.settings = manager.settings;
    this.tracks = manager.tracks;
    this.ready = manager.ready;

    // Effects

    /**
     * When there's a selected output and its collection changes,
     * save it to the default output or memory.
     */
    this.effect(() => {
      const out = this.#selected.value;
      if (!out) return;

      const col = out.facets.collection();
      if (col.state !== "loaded") return;

      const def = this.#defaultOutput.value;
      if (def) def.facets.save(col.data);
      else this.#memory.facets.set(col.data);
    });

    this.effect(() => {
      const out = this.#selected.value;
      if (!out) return;

      const col = out.playlistItems.collection();
      if (col.state !== "loaded") return;

      const def = this.#defaultOutput.value;
      if (def) def.playlistItems.save(col.data);
      else this.#memory.playlistItems.set(col.data);
    });

    this.effect(() => {
      const out = this.#selected.value;
      if (!out) return;

      const col = out.settings.collection();
      if (col.state !== "loaded") return;

      const def = this.#defaultOutput.value;
      if (def) def.settings.save(col.data);
      else this.#memory.settings.set(col.data);
    });

    this.effect(() => {
      const out = this.#selected.value;
      if (!out) return;

      const col = out.tracks.collection();
      if (col.state !== "loaded") return;

      const def = this.#defaultOutput.value;
      if (def) def.tracks.save(col.data);
      else this.#memory.tracks.set(col.data);
    });
  }

  // SIGNALS

  #activated = signal(/** @type {Set<string>} */ (new Set()));

  #defaultOutput = signal(
    /** @type {Output | null | undefined} */ (undefined),
  );

  #memory = {
    facets: signal(/** @type {Facet[]} */ ([])),
    playlistItems: signal(/** @type {PlaylistItem[]} */ ([])),
    settings: signal(/** @type {Setting[]} */ ([])),
    tracks: signal(/** @type {Track[]} */ ([])),
  };

  #selected = signal(
    /** @type {Output | null | undefined} */ (undefined),
  );

  #setupFinished = signal(false);

  // STATE

  activated = this.#activated.get;
  selected = computed(() => this.#selected.value ?? null);

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        selectOutput: {
          strategy: "replicate",
          fn: this.#selectOutput,
        },
      });

      if (actions) {
        this.#selectOutput = actions.selectOutput;
      }
    }

    // Super
    super.connectedCallback();

    // Outputs
    const def_ault = this.getAttribute("default");
    const selectedOutputId = this.#selectedOutputId();

    batch(() => {
      /** @type {Set<string>} */
      const activated = new Set();

      if (def_ault) {
        activated.add(def_ault);
      }

      if (selectedOutputId) {
        activated.add(selectedOutputId);
      }

      this.#activated.value = activated;
    });

    /** @type {Output | null} */
    const defaultOutput = def_ault ? await this.#findOutput(def_ault) : null;
    const selectedOutput = await this.#findOutput(selectedOutputId);

    batch(() => {
      this.#selected.value = selectedOutput;
      this.#defaultOutput.value = defaultOutput;
      this.#setupFinished.value = true;
    });
  }

  // MISC

  /**
   * @param {string | null} id
   */
  async #findOutput(id) {
    const el = id ? this.root().querySelector(`#${id}`) : null;
    if (!el) return null;

    await customElements.whenDefined(el.localName);

    if (
      "identifier" in el === false ||
      "tracks" in el === false
    ) {
      return null;
    }

    return /** @type {Output} */ (/** @type {unknown} */ (el));
  }

  /**
   * @param {string | null} id
   */
  #selectOutput = async (id) => {
    if (id) {
      this.#activated.value = new Set([...this.#activated.value.values(), id]);
    }

    this.#selected.value = await this.#findOutput(id);
  };

  #selectedOutputId() {
    return localStorage.getItem(`${STORAGE_PREFIX}/selected/id`);
  }

  /**
   * @override
   */
  dependencies = () => {
    return Object.fromEntries(
      Array.from(this.root().children).flatMap((element) => {
        if (element.hasAttribute("id") === false) {
          console.warn(
            "Missing `id` for output configurator child element with `localName` '" +
              element.localName + "'",
          );
          return [];
        }

        const d = /** @type {DiffuseElement} */ (element);
        return [[d.id, d]];
      }),
    );
  };

  // ADDITIONAL ACTIONS

  deselect = async () => {
    localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`);
    await this.#selectOutput(null);
  };

  hasDefault() {
    return this.hasAttribute("default");
  }

  hasSelected() {
    return this.#selectedOutputId() !== null;
  }

  loadSelected = async () => {
    const selectedOutput = await this.#findOutput(this.#selectedOutputId());
    this.#selected.value = selectedOutput;
  };

  options = async () => {
    const deps = this.dependencies();
    const entries = Object.entries(deps);

    return entries.map(([k, v]) => {
      return {
        id: k,
        label: v.label ?? v.getAttribute("label"),
        element: /** @type {OutputElement} */ (v),
      };
    });
  };

  /**
   * @param {string} id
   */
  select = async (id) => {
    localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id);
    await this.#selectOutput(id);
  };

  /**
   * @param {string} label
   * @returns {Promise<{ id: string, label: string, element: OutputElement }>}
   */
  waitForOption = (label) => {
    return new Promise((resolve) => {
      const check = async () => {
        const opt = (await this.options()).find((o) => o.label === label);
        if (opt) {
          observer.disconnect();
          resolve(opt);
        }
      };

      const observer = new MutationObserver(check);
      observer.observe(this, { childList: true });
      check();
    });
  };
}

export default OutputConfigurator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = OutputConfigurator;
export const NAME = "dc-output";

defineElement(NAME, CLASS);
```
### Scrobbles

Configure multiple scrobblers (music trackers).

```js
import { defineElement, DiffuseElement } from "~/common/element.js";

/**
 * @import {Track} from "~/definitions/types.d.ts"
 * @import {ScrobbleActions, ScrobbleElement} from "@specs/components/supplement/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ScrobbleActions}
 */
class ScrobblesConfigurator extends DiffuseElement {
  static NAME = "diffuse/configurator/scrobbles";

  // SCROBBLE ACTIONS

  /**
   * @param {Track} track
   */
  async nowPlaying(track) {
    return await Promise.all(
      this.#activeScrobblers().map((s) => s.nowPlaying(track)),
    );
  }

  /**
   * @param {Track} track
   * @param {number} startedAt Unix timestamp in milliseconds
   * @param {{ duration?: number }} [options] duration in milliseconds
   */
  async scrobble(track, startedAt, options) {
    return await Promise.all(
      this.#activeScrobblers().map((s) => s.scrobble(track, startedAt, options)),
    );
  }

  // MISC

  /**
   * All child scrobble elements, regardless of authentication state.
   *
   * @returns {ScrobbleElement[]}
   */
  scrobblers() {
    return Array.from(this.root().children).flatMap((el) => {
      if (!("isAuthenticated" in el && "nowPlaying" in el)) return [];
      return [/** @type {ScrobbleElement} */ (/** @type {unknown} */ (el))];
    });
  }

  /**
   * Child scrobble elements that are currently authenticated.
   *
   * @returns {ScrobbleElement[]}
   */
  #activeScrobblers() {
    return this.scrobblers().filter((s) => s.isAuthenticated());
  }
}

export default ScrobblesConfigurator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ScrobblesConfigurator;
export const NAME = "dc-scrobbles";

defineElement(NAME, CLASS);
```
## Engines
### Audio

Plays audio through audio elements.

```js
import { keyed } from "lit-html/directives/keyed.js";

import {
  BroadcastableDiffuseElement,
  defineElement,
  nothing,
} from "~/common/element.js";
import { computed, signal, untracked } from "~/common/signal.js";

/**
 * @import {Actions, AudioUrl, AudioState, AudioStateReadOnly, LoadingState} from "@specs/components/engine/audio/types.d.ts"
 * @import {RenderArg} from "~/common/element.d.ts"
 * @import {SignalReader} from "~/common/signal.d.ts"
 */

////////////////////////////////////////////
// CONSTANTS
////////////////////////////////////////////
const SILENT_MP3 =
  "data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {Actions}
 */
class AudioEngine extends BroadcastableDiffuseElement {
  static NAME = "diffuse/engine/audio";

  constructor() {
    super();

    this.state = this.state.bind(this);
  }

  // SIGNALS

  #items = signal(/** @type {AudioUrl[]} */ ([]));
  #volume = signal(0.75);

  /** @type {Map<string, ReadableStream>} Streams pending MediaSource setup */
  #streams = new Map();

  /** @type {Map<string, string>} MediaSource object URLs created from streams, keyed by item ID */
  #mediaSourceUrls = new Map();

  // STATE

  items = this.#items.get;
  volume = this.#volume.get;

  isPlaying = computed(() => {
    const item = this.items()?.[0];
    if (!item) return false;

    const state = this.state(item.id);
    if (!state) return false;

    return state.isPlaying() || state.hasEnded() ||
      (state.duration() > 0 && state.currentTime() === state.duration());
  });

  // LIFECYCLE

  /**
   * @override
   */
  connectedCallback() {
    // Setup broadcasting if part of group
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(
        this.identifier,
        {
          adjustVolume: { strategy: "replicate", fn: this.adjustVolume },
          pause: { strategy: "leaderOnly", fn: this.pause },
          play: { strategy: "leaderOnly", fn: this.play },
          seek: { strategy: "leaderOnly", fn: this.seek },
          supply: { strategy: "replicate", fn: this.supply },

          // State
          items: { strategy: "leaderOnly", fn: this.items },
        },
      );

      if (!actions) return;

      this.adjustVolume = actions.adjustVolume;
      this.pause = actions.pause;
      this.play = actions.play;
      this.seek = actions.seek;
      this.supply = actions.supply;

      // Sync items with leader if needed
      this.broadcastingStatus().then(async (status) => {
        if (status.leader) return;
        this.#items.value = await actions.items();
      });
    }

    // Super
    super.connectedCallback();

    // Get volume from previous session if possible
    const VOLUME_KEY =
      `${this.constructor.prototype.constructor.NAME}/${this.group}/volume`;
    const volume = localStorage.getItem(VOLUME_KEY);

    if (volume != undefined) {
      this.#volume.set(parseFloat(volume));
    }

    // Monitor volume signal
    this.effect(() => {
      Array.from(this.querySelectorAll("de-audio-item")).forEach(
        (node) => {
          const item = /** @type {AudioEngineItem} */ (node);
          if (item.hasAttribute("preload")) return;
          const audio = item.querySelector("audio");
          if (audio) audio.volume = this.#volume.value;
        },
      );

      localStorage.setItem(VOLUME_KEY, this.#volume.value.toString());
    });

    // Only broadcasting stuff from here on out
    if (!this.broadcasted) return;

    // Manage playback across tabs if needed
    this.effect(async () => {
      const status = await this.broadcastingStatus();
      untracked(() => {
        if (!(status.leader && status.initialLeader === false)) return;

        console.log("🧙 Leadership acquired");
        this.items().forEach((item) => {
          const el = this.#itemElement(item.id);
          if (!el) return;

          el.removeAttribute("initial-progress");

          if (!el.audio) return;

          const currentTime = el.$state.currentTime.value;
          const canPlay = () => {
            this.seek({
              audioId: item.id,
              currentTime: currentTime,
            });

            if (el.$state.isPlaying.value) this.play({ audioId: item.id });
          };

          el.audio.addEventListener("canplay", canPlay, { once: true });

          if (el.audio.readyState === 0) el.audio.load();
          else canPlay();
        });
      });
    });
  }

  // ACTIONS

  /**
   * @type {Actions["adjustVolume"]}
   */
  adjustVolume(args) {
    if (args.audioId) {
      this.#withAudioNode(args.audioId, (audio) => {
        audio.volume = args.volume;
      });
    } else {
      this.#volume.value = args.volume;
    }
  }

  /**
   * @type {Actions["pause"]}
   */
  pause({ audioId }) {
    this.#withAudioNode(audioId, (audio) => audio.pause());
  }

  /**
   * @type {Actions["play"]}
   */
  play({ audioId, volume }) {
    this.#withAudioNode(audioId, (audio, item) => {
      audio.volume = volume ?? this.volume();
      audio.muted = false;

      // TODO: Might need this for `data-initial-progress`
      //       Does seem to cause trouble when broadcasting
      //       (open multiple sessions and play the next audio)
      // if (audio.readyState === 0) audio.load();
      if (!audio.isConnected) return;

      const promise = audio.play() || Promise.resolve();
      item.$state.isPlaying.set(true);

      promise.catch((e) => {
        if (!audio.isConnected) {
          return; /* The node was removed from the DOM, we can ignore this error */
        }
        const err =
          "Couldn't play audio automatically. Please resume playback manually.";
        console.error(err, e);
        item.$state.isPlaying.set(false);
      });
    });
  }

  /**
   * Use this function to reload the audio after an error occurred.
   *
   * @type {Actions["reload"]}
   */
  reload(args) {
    this.#withAudioNode(args.audioId, (audio, item) => {
      if (audio.readyState === 0 || audio.error?.code === 2) {
        audio.load();

        if (args.progress !== undefined) {
          item.setAttribute(
            "initial-progress",
            JSON.stringify(args.progress),
          );
        }

        if (args.play) {
          this.play({ audioId: args.audioId, volume: audio.volume });
        }
      }
    });
  }

  /**
   * @type {Actions["seek"]}
   */
  seek({ audioId, currentTime, percentage }) {
    this.#withAudioNode(audioId, (audio) => {
      if (currentTime != undefined) {
        audio.currentTime = currentTime;
      } else if (
        percentage != undefined && !isNaN(audio.duration) &&
        audio.duration !== Infinity
      ) {
        audio.currentTime = percentage * audio.duration;
      }
    });
  }

  /**
   * @type {Actions["supply"]}
   */
  supply(args) {
    const existingMap = new Map(this.#items.value.map((a) => [a.id, a]));

    // Start loading new streams
    for (const item of args.audio) {
      if (
        "stream" in item &&
        !existingMap.has(item.id) &&
        !this.#streams.has(item.id)
      ) {
        this.#streams.set(item.id, item.stream);
        this.#resolveStream(
          item.id,
          item.stream,
          item.mimeType ?? "",
          item.seek,
          item.duration,
        );
      }
    }

    // Stop streams that are no longer needed
    const newIds = new Set(args.audio.map((a) => a.id));

    for (const [id, objectUrl] of this.#mediaSourceUrls) {
      if (!newIds.has(id)) {
        URL.revokeObjectURL(objectUrl);
        this.#mediaSourceUrls.delete(id);
      }
    }

    for (const id of this.#streams.keys()) {
      if (!newIds.has(id)) this.#streams.delete(id);
    }

    /** @type {AudioUrl[]} Remove `stream` field, replace it with `url` */
    const resolvedAudio = args.audio.map((a) => {
      const url = "stream" in a ? this.#mediaSourceUrls.get(a.id) : a.url;

      if (!url) {
        throw new Error("Stream did not produce a media source url");
      }

      return {
        id: a.id,
        isPreload: a.isPreload,
        mimeType: a.mimeType,
        progress: a.progress,
        track: a.track,
        url,
      };
    });

    const hasNewIds = resolvedAudio.some((a) => !existingMap.has(a.id));
    const hasPreloadChanges = resolvedAudio.some(
      (a) => existingMap.get(a.id)?.isPreload !== a.isPreload,
    );

    const hasUrlChanges = resolvedAudio.some(
      (a) => existingMap.get(a.id)?.url !== a.url,
    );

    if (hasNewIds || hasPreloadChanges || hasUrlChanges) {
      this.#items.value = resolvedAudio;
    }

    // When only the URL changed for an existing item (e.g. tab leadership handoff invalidated
    // a blob URL), the same <de-audio-item> element is reused via `keyed`. lit-html will
    // update <source src> but the browser won't reload on its own — call audio.load() if the
    // element hasn't successfully loaded yet so it picks up the fresh URL.
    if (hasUrlChanges && !hasNewIds) {
      for (const a of resolvedAudio) {
        if (existingMap.has(a.id) && existingMap.get(a.id)?.url !== a.url) {
          this.#withAudioNode(a.id, (audio) => {
            if (audio.readyState === 0 || audio.error) audio.load();
          });
        }
      }
    }

    if (args.play) this.play(args.play);
  }

  // STREAMS

  /**
   * @param {string} id
   * @param {ReadableStream} stream
   * @param {string} mimeType
   * @param {((timeSeconds: number) => Promise<ReadableStream>) | undefined} seekFn
   * @param {number | undefined} duration
   */
  async #resolveStream(id, stream, mimeType, seekFn, duration) {
    const mediaSource = new MediaSource();
    const objectUrl = URL.createObjectURL(mediaSource);

    this.#mediaSourceUrls.set(id, objectUrl);
    this.#streams.delete(id);

    // Yield so the render triggered by supply() can complete, ensuring the
    // audio element is in the DOM before we set its src.
    await Promise.resolve();

    if (!this.#mediaSourceUrls.has(id)) {
      // Item was removed while waiting
      URL.revokeObjectURL(objectUrl);
      return;
    }

    const itemEl = this.#itemElement(id);
    if (!itemEl) {
      URL.revokeObjectURL(objectUrl);
      this.#mediaSourceUrls.delete(id);
      return;
    }

    // MediaSource must be attached via audio.src directly;
    // <source> elements do not trigger sourceopen.
    itemEl.audio.src = objectUrl;

    await new Promise((resolve) => {
      mediaSource.addEventListener("sourceopen", resolve, { once: true });
    });

    if (duration !== undefined) mediaSource.duration = duration;

    const sourceBuffer = mediaSource.addSourceBuffer(mimeType);

    // 'reader' is always the current active reader; the seeking handler
    // closes over this variable so it always cancels the right one.
    let reader = stream.getReader();
    let seekPending = false;
    let seekTarget = 0;

    const onSeeking = () => {
      if (!seekFn) return;
      const audio = itemEl.audio;
      const target = audio.currentTime;

      // Only intervene if the target is outside what's already buffered.
      for (let i = 0; i < audio.buffered.length; i++) {
        if (
          audio.buffered.start(i) <= target && target <= audio.buffered.end(i)
        ) {
          return; // Browser can handle it with buffered data.
        }
      }

      seekPending = true;
      seekTarget = target;
      reader.cancel().catch(() => {});
    };

    itemEl.audio.addEventListener("seeking", onSeeking);

    try {
      while (true) {
        if (!this.#mediaSourceUrls.has(id)) {
          await reader.cancel();
          break;
        }

        let done, value;

        try {
          ({ done, value } = await reader.read());
        } catch {
          done = true;
        }

        if (!this.#mediaSourceUrls.has(id)) break;

        if (seekPending) {
          seekPending = false;

          // Clear all buffered data before feeding from the new position.
          if (sourceBuffer.updating) {
            await new Promise((r) =>
              sourceBuffer.addEventListener("updateend", r, { once: true })
            );
          }
          await new Promise((r) => {
            sourceBuffer.addEventListener("updateend", r, { once: true });
            sourceBuffer.remove(0, Infinity);
          });

          if (!seekFn) throw new Error("seekFn is undefined");
          reader = (await seekFn(seekTarget)).getReader();

          continue;
        }

        if (done) {
          if (mediaSource.readyState === "open") mediaSource.endOfStream();
          break;
        }

        if (sourceBuffer.updating) {
          await new Promise((r) =>
            sourceBuffer.addEventListener("updateend", r, { once: true })
          );
        }

        sourceBuffer.appendBuffer(value);
        await new Promise((r) =>
          sourceBuffer.addEventListener("updateend", r, { once: true })
        );
      }
    } catch (err) {
      console.error("[audio engine] Stream error:", err);
      if (mediaSource.readyState === "open") mediaSource.endOfStream("decode");
    } finally {
      itemEl.audio.removeEventListener("seeking", onSeeking);
    }
  }

  // RENDER

  /**
   * @param {RenderArg} _
   */
  render({ html }) {
    const ids = this.items().map((i) => i.id);

    this.querySelectorAll("de-audio-item").forEach((element) => {
      if (ids.includes(element.id)) return;

      const source = element.querySelector("source");
      if (source) source.src = SILENT_MP3;
    });

    const group = this.group;
    const nodes = this.items().map((audio) => {
      const ip = audio.progress === undefined
        ? "0"
        : JSON.stringify(audio.progress);

      return keyed(
        audio.id,
        html`
          <de-audio-item
            group="${this.broadcasted ? `${group}/${audio.id}` : nothing}"
            id="${audio.id}"
            initial-progress="${ip}"
            mime-type="${audio.mimeType ? audio.mimeType : nothing}"
            preload="${audio.isPreload ? `preload` : nothing}"
            url="${audio.url ?? nothing}"
          >
            <audio
              crossorigin="anonymous"
              muted="true"
              preload="auto"
            >
              ${audio.url
                ? html`
                  <source
                    src="${audio.url}"
                    ${audio.mimeType ? 'type="' + audio.mimeType + '"' : ""}
                  />
                `
                : nothing}
            </audio>
          </de-audio-item>
        `,
      );
    });

    return html`
      <section id="audio-nodes">
        ${nodes}
      </section>
    `;
  }

  // 🛠️

  /**
   * Get the state of a single audio item.
   *
   * @param {string} audioId
   * @returns {SignalReader<AudioStateReadOnly | undefined>}
   */
  _state(audioId) {
    return computed(() => {
      const _trigger = this.#items.value;

      const s = this.#itemElement(audioId)?.state;
      return s ? { ...s } : undefined;
    });
  }

  /**
   * Get the state of a single audio item.
   *
   * @param {string} audioId
   * @returns {AudioStateReadOnly | undefined}
   */
  state(audioId) {
    return this._state(audioId)();
  }

  /**
   * @param {string} audioId
   */
  #itemElement(audioId) {
    const node = this.querySelector(
      `de-audio-item[id="${audioId}"]:not([preload])`,
    ) ?? this.querySelector(
      `de-audio-item[id="${audioId}"]`,
    );

    if (node) {
      const item = /** @type {AudioEngineItem} */ (node);
      return item;
    }
  }

  /**
   * @param {string} audioId
   * @param {(audio: HTMLAudioElement, item: AudioEngineItem) => void} fn
   */
  #withAudioNode(audioId, fn) {
    const item = this.#itemElement(audioId);
    if (item) fn(item.audio, item);
  }
}

export default AudioEngine;

////////////////////////////////////////////
// ITEM ELEMENT
////////////////////////////////////////////

class AudioEngineItem extends BroadcastableDiffuseElement {
  static NAME = "diffuse/engine/audio/item";
  static observedAttributes = ["preload"];

  constructor() {
    super();

    // TODO:
    // const ip = this.getAttribute("initial-progress");

    /**
     * @type {AudioState}
     */
    this.$state = {
      currentTime: signal(0),
      duration: signal(0),
      hasEnded: signal(false),
      isPlaying: signal(false),
      isPreload: signal(this.hasAttribute("preload")),
      loadingState: signal(/** @type {LoadingState} */ ("initialisation")),

      progress: computed(() => {
        const currentTime = this.$state.currentTime.value;
        const duration = this.$state.duration.value;

        if (!duration || isNaN(duration) || duration === Infinity) return 0;

        return currentTime / duration;
      }),
    };
  }

  /**
   * @override
   * @param {string} name
   * @param {string} oldValue
   * @param {string} newValue
   */
  attributeChangedCallback(name, oldValue, newValue) {
    super.attributeChangedCallback(name, oldValue, newValue);
    if (name === "preload") {
      this.$state.isPreload.set(newValue !== null);
    }
  }

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    const audio = this.audio;

    audio.addEventListener("canplay", this.canplayEvent);
    audio.addEventListener("durationchange", this.durationchangeEvent);
    audio.addEventListener("ended", this.endedEvent);
    audio.addEventListener("error", this.errorEvent);
    audio.addEventListener("pause", this.pauseEvent);
    audio.addEventListener("play", this.playEvent);
    audio.addEventListener("playing", this.playingEvent);
    audio.addEventListener("suspend", this.suspendEvent);
    audio.addEventListener("timeupdate", this.timeupdateEvent);
    audio.addEventListener("waiting", this.waitingEvent);

    // Transition from initialisation to loading for non-preload items
    if (!this.hasAttribute("preload")) {
      this.$state.loadingState.set("loading");
    }

    // Setup broadcasting if part of group
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(
        this.identifier,
        {
          getCurrentTime: {
            strategy: "leaderOnly",
            fn: this.$state.currentTime.get,
          },
          getDuration: { strategy: "leaderOnly", fn: this.$state.duration.get },
          getHasEnded: { strategy: "leaderOnly", fn: this.$state.hasEnded.get },
          getIsPlaying: {
            strategy: "leaderOnly",
            fn: this.$state.isPlaying.get,
          },
          getIsPreload: {
            strategy: "leaderOnly",
            fn: this.$state.isPreload.get,
          },
          getLoadingState: {
            strategy: "leaderOnly",
            fn: this.$state.loadingState.get,
          },

          // SET
          setCurrentTime: {
            strategy: "replicate",
            fn: this.$state.currentTime.set,
          },
          setDuration: { strategy: "replicate", fn: this.$state.duration.set },
          setHasEnded: { strategy: "replicate", fn: this.$state.hasEnded.set },
          setIsPlaying: {
            strategy: "replicate",
            fn: this.$state.isPlaying.set,
          },
          setIsPreload: {
            strategy: "replicate",
            fn: this.$state.isPreload.set,
          },
          setLoadingState: {
            strategy: "replicate",
            fn: this.$state.loadingState.set,
          },
        },
        {
          // Sync leadership with engine's broadcasting channel
          assumeLeadership: (await this.engine?.broadcastingStatus())?.leader,
        },
      );

      if (actions) {
        this.$state.currentTime.set = actions.setCurrentTime;
        this.$state.duration.set = actions.setDuration;
        this.$state.hasEnded.set = actions.setHasEnded;
        this.$state.isPlaying.set = actions.setIsPlaying;
        this.$state.isPreload.set = actions.setIsPreload;
        this.$state.loadingState.set = actions.setLoadingState;

        untracked(async () => {
          this.$state.currentTime.value = await actions.getCurrentTime();
          this.$state.duration.value = await actions.getDuration();
          this.$state.hasEnded.value = await actions.getHasEnded();
          this.$state.isPlaying.value = await actions.getIsPlaying();
          this.$state.isPreload.value = await actions.getIsPreload();
          this.$state.loadingState.value = await actions.getLoadingState();
        });
      }
    }

    // Super
    super.connectedCallback();
  }

  // STATE

  /**
   * @type {AudioStateReadOnly}
   */
  get state() {
    return {
      id: this.id,
      mimeType: this.getAttribute("mime-type") ?? undefined,
      url: this.getAttribute("url") ?? "",

      currentTime: this.$state.currentTime.get,
      duration: this.$state.duration.get,
      hasEnded: this.$state.hasEnded.get,
      isPlaying: this.$state.isPlaying.get,
      isPreload: this.$state.isPreload.get,
      loadingState: this.$state.loadingState.get,

      progress: this.$state.progress,
    };
  }

  // RELATED ELEMENTS

  get audio() {
    const el = this.querySelector("audio");
    if (el) return /** @type {HTMLAudioElement} */ (el);
    else throw new Error("Cannot find child audio element");
  }

  get engine() {
    const el = this.closest("de-audio");
    if (el) return /** @type {AudioEngine} */ (el);
    else return null;
  }

  // EVENTS

  /**
   * @param {Event} event
   */
  canplayEvent(event) {
    const audio = /** @type {HTMLAudioElement} */ (event.target);
    const item = engineItem(audio);

    if (
      item?.hasAttribute("initial-progress") &&
      audio.duration &&
      !isNaN(audio.duration)
    ) {
      const progress = JSON.parse(
        item.getAttribute("initial-progress") ?? "0",
      );
      if (
        progress !== 0 && !isNaN(audio.duration) && audio.duration !== Infinity
      ) {
        audio.currentTime = audio.duration * progress;
      }

      item.removeAttribute("initial-progress");
    }

    finishedLoading(event);
  }

  /**
   * @param {Event} event
   */
  durationchangeEvent(event) {
    const audio = /** @type {HTMLAudioElement} */ (event.target);

    if (!isNaN(audio.duration)) {
      engineItem(audio)?.$state.duration.set(audio.duration);
    }
  }

  /**
   * @param {Event} event
   */
  endedEvent(event) {
    const audio = /** @type {HTMLAudioElement} */ (event.target);
    audio.currentTime = 0;

    engineItem(audio)?.$state.hasEnded.set(true);
  }

  /**
   * @param {Event} event
   */
  errorEvent(event) {
    const audio = /** @type {HTMLAudioElement} */ (event.target);
    const code = audio.error?.code || 0;

    engineItem(audio)?.$state.loadingState.set({ error: { code } });
  }

  /**
   * @param {Event} event
   */
  pauseEvent(event) {
    const audio = /** @type {HTMLAudioElement} */ (event.target);
    const item = engineItem(audio);

    item?.$state.isPlaying.set(false);
  }

  /**
   * @param {Event} event
   */
  playEvent(event) {
    const audio = /** @type {HTMLAudioElement} */ (event.target);

    const item = engineItem(audio);
    item?.$state.hasEnded.set(false);
    item?.$state.isPlaying.set(true);

    // In case audio was preloaded:
    if (audio.readyState >= 2) finishedLoading(event);
  }

  /**
   * @param {Event} event
   */
  playingEvent(event) {
    finishedLoading(event);
  }

  /**
   * @param {Event} event
   */
  suspendEvent(event) {
    finishedLoading(event);
  }

  /**
   * @param {Event} event
   */
  timeupdateEvent(event) {
    const audio = /** @type {HTMLAudioElement} */ (event.target);
    if (isNaN(audio.duration) || audio.duration === 0) return;

    engineItem(audio)?.$state.currentTime.set(audio.currentTime);
  }

  /**
   * @param {Event} event
   */
  waitingEvent(event) {
    initiateLoading(event);

    const audio = /** @type {HTMLAudioElement} */ (event.target);
    if (audio.seeking) return;
    if (audio.networkState !== HTMLMediaElement.NETWORK_IDLE) return;

    const item = engineItem(audio);
    if (!item || item.hasAttribute("preload")) return;

    const progress = !isNaN(audio.duration) && audio.duration > 0 &&
        audio.duration !== Infinity
      ? audio.currentTime / audio.duration
      : 0;

    if (progress > 0) {
      item.setAttribute("initial-progress", JSON.stringify(progress));
    }

    // Don't force a full reload if the browser already has buffered data —
    // it should be able to continue buffering on its own. This prevents
    // discarding the preloaded buffer when playback briefly catches up to
    // the end of the downloaded portion.
    if (audio.buffered.length > 0) return;

    audio.load();

    audio.addEventListener("canplay", () => {
      if (item.$state.isPlaying.get()) {
        item.engine?.play({ audioId: item.id });
      }
    }, { once: true });
  }
}

export { AudioEngineItem };

////////////////////////////////////////////
// 🛠️
////////////////////////////////////////////

/**
 * @param {HTMLAudioElement} audio
 */
function engineItem(audio) {
  const c = audio.closest("de-audio-item");
  if (c) return /** @type {AudioEngineItem} */ (c);
  else return null;
}

/**
 * @param {Event} event
 */
function finishedLoading(event) {
  const audio = /** @type {HTMLAudioElement} */ (event.target);
  engineItem(audio)?.$state.loadingState.set("loaded");
}

/**
 * @param {Event} event
 */
function initiateLoading(event) {
  const audio = /** @type {HTMLAudioElement} */ (event.target);
  if (audio.readyState < 4) {
    const item = engineItem(audio);
    if (item?.hasAttribute("preload")) return;
    item?.$state.loadingState.set("loading");
  }
}

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = AudioEngine;
export const NAME = "de-audio";
export const NAME_ITEM = "de-audio-item";

defineElement(NAME, AudioEngine);
defineElement(NAME_ITEM, AudioEngineItem);
```
### Queue

A queue for tracks.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { signal } from "~/common/signal.js";
import { listen } from "~/common/worker.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts";
 * @import {Actions, Item, State} from "@specs/components/engine/queue/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class QueueEngine extends DiffuseElement {
  static NAME = "diffuse/engine/queue";
  static WORKER_URL = "components/engine/queue/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions & State>} */
    this.proxy = this.workerProxy();

    this.add = this.proxy.add;
    this.clear = this.proxy.clear;
    this.expel = this.proxy.expel;
    this.fill = this.proxy.fill;
    this.move = this.proxy.move;
    this.shift = this.proxy.shift;
    this.supply = this.proxy.supply;
    this.unshift = this.proxy.unshift;
  }

  // SIGNALS

  #future = signal(/** @type {Array<Item>} */ ([]));
  #now = signal(/** @type {Item | null} */ (null));
  #past = signal(/** @type {Array<Item>} */ ([]));
  #supplyFingerprint = signal(/** @type {string | undefined} */ (undefined));

  // STATE

  future = this.#future.get;
  now = this.#now.get;
  past = this.#past.get;
  supplyFingerprint = this.#supplyFingerprint.get;

  // LIFECYCLE

  /**
   * @override
   */
  connectedCallback() {
    super.connectedCallback();

    // Sync data with worker
    const link = this.workerLink();

    // Listen for remote data changes
    listen("future", this.#future.set, link);
    listen("now", this.#now.set, link);
    listen("past", this.#past.set, link);
    listen("supplyFingerprint", this.#supplyFingerprint.set, link);

    // Fetch current data state
    this.proxy.future().then(this.#future.set);
    this.proxy.now().then(this.#now.set);
    this.proxy.past().then(this.#past.set);
    this.proxy.supplyFingerprint().then(this.#supplyFingerprint.set);
  }
}

export default QueueEngine;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = QueueEngine;
export const NAME = "de-queue";

defineElement(NAME, QueueEngine);
```
### Repeat & Shuffle

Signals synced with local storage (classified by group) that decide if audio should be repeated and if the queue should be shuffled when filling it.

```js
import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js";
import { signal } from "~/common/signal.js";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class RepeatShuffleEngine extends BroadcastableDiffuseElement {
  static NAME = "diffuse/engine/repeat-shuffle";

  // SIGNALS

  #repeat = signal(false);
  #shuffle = signal(false);

  repeat = this.#repeat.get;
  shuffle = this.#shuffle.get;

  // LIFECYCLE

  /**
   * @override
   */
  connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        setRepeat: { strategy: "replicate", fn: this.setRepeat },
        setShuffle: { strategy: "replicate", fn: this.setShuffle },
      });

      if (actions) {
        this.setRepeat = actions.setRepeat;
        this.setShuffle = actions.setShuffle;
      }
    }

    // Super
    super.connectedCallback();

    // Signals
    const storagePrefix =
      `${this.constructor.prototype.constructor.NAME}/${this.group}`;

    this.#repeat.value =
      localStorage.getItem(`${storagePrefix}/repeat`) === "true" ? true : false;
    this.#shuffle.value =
      localStorage.getItem(`${storagePrefix}/shuffle`) === "true"
        ? true
        : false;

    // Effects
    this.effect(() =>
      localStorage.setItem(
        `${storagePrefix}/repeat`,
        this.#repeat.value ? "true" : "false",
      )
    );

    this.effect(() =>
      localStorage.setItem(
        `${storagePrefix}/shuffle`,
        this.#shuffle.value ? "true" : "false",
      )
    );
  }

  // ACTIONS

  /** @param {boolean} bool */
  setRepeat = async (bool) => this.#repeat.value = bool;

  /** @param {boolean} bool */
  setShuffle = async (bool) => this.#shuffle.value = bool;
}

export default RepeatShuffleEngine;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = RepeatShuffleEngine;
export const NAME = "de-repeat-shuffle";

defineElement(NAME, CLASS);
```
### Scope

Signals that could influence the scope of a set of tracks.

```js
import {
  BroadcastableDiffuseElement,
  defineElement,
} from "~/common/element.js";
import { batch, signal } from "~/common/signal.js";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class ScopeEngine extends BroadcastableDiffuseElement {
  static NAME = "diffuse/engine/scope";

  // SIGNALS

  #groupBy = signal(/** @type {string | undefined} */ (undefined));
  #playlist = signal(/** @type {string | undefined} */ (undefined));
  #searchTerm = signal(/** @type {string | undefined} */ (undefined));
  #sortBy = signal(
    /** @type {string[]} */ ([
      "tags.artist",
      "tags.album",
      "tags.disc.no",
      "tags.track.no",
    ]),
  );
  #sortDirection = signal(/** @type {"asc" | "desc" | undefined} */ ("asc"));

  groupBy = this.#groupBy.get;
  playlist = this.#playlist.get;
  searchTerm = this.#searchTerm.get;
  sortBy = this.#sortBy.get;
  sortDirection = this.#sortDirection.get;

  // LIFECYCLE

  /**
   * @override
   */
  connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        revertToDefaultSort: {
          strategy: "replicate",
          fn: this.revertToDefaultSort,
        },
        setGroupBy: { strategy: "replicate", fn: this.setGroupBy },
        setPlaylist: { strategy: "replicate", fn: this.setPlaylist },
        setSearchTerm: { strategy: "replicate", fn: this.setSearchTerm },
        setSortBy: { strategy: "replicate", fn: this.setSortBy },
        setSortDirection: { strategy: "replicate", fn: this.setSortDirection },
      });

      if (actions) {
        this.revertToDefaultSort = actions.revertToDefaultSort;
        this.setGroupBy = actions.setGroupBy;
        this.setPlaylist = actions.setPlaylist;
        this.setSearchTerm = actions.setSearchTerm;
        this.setSortBy = actions.setSortBy;
        this.setSortDirection = actions.setSortDirection;
      }
    }

    // Super
    super.connectedCallback();

    // Signals
    const storagePrefix =
      `${this.constructor.prototype.constructor.NAME}/${this.group}`;

    batch(() => {
      this.#groupBy.value = localStorage.getItem(`${storagePrefix}/groupBy`) ??
        undefined;
      this.#playlist.value =
        localStorage.getItem(`${storagePrefix}/playlistId`) ?? undefined;
      this.#searchTerm.value =
        localStorage.getItem(`${storagePrefix}/searchTerm`) ?? undefined;
      this.#sortBy.value = JSON.parse(
        localStorage.getItem(`${storagePrefix}/sortBy`) ??
          `["tags.artist", "tags.album", "tags.disc.no", "tags.track.no"]`,
      );
      this.#sortDirection.value =
        /** @type {"desc" | "asc"} */ (localStorage.getItem(
          `${storagePrefix}/sortDirection`,
        ) ?? "asc");
    });

    // Effects
    this.effect(() => {
      const key = `${storagePrefix}/groupBy`;
      const val = this.#groupBy.value;

      if (val) localStorage.setItem(key, val);
      else localStorage.removeItem(key);
    });

    this.effect(() => {
      const key = `${storagePrefix}/playlistId`;
      const val = this.#playlist.value;

      if (val) localStorage.setItem(key, val);
      else localStorage.removeItem(key);
    });

    this.effect(() => {
      const key = `${storagePrefix}/searchTerm`;
      const val = this.#searchTerm.value;

      if (val) localStorage.setItem(key, val);
      else localStorage.removeItem(key);
    });

    this.effect(() => {
      const key = `${storagePrefix}/sortBy`;
      const val = this.#sortBy.value;

      if (val.length) localStorage.setItem(key, JSON.stringify(val));
      else localStorage.removeItem(key);
    });

    this.effect(() => {
      const key = `${storagePrefix}/sortDirection`;
      const val = this.#sortDirection.value;

      if (val) localStorage.setItem(key, val);
      else localStorage.removeItem(key);
    });
  }

  // ACTIONS

  revertToDefaultSort = async () => {
    this.#sortBy.value = [
      "tags.artist",
      "tags.album",
      "tags.disc.no",
      "tags.track.no",
    ];
    this.#sortDirection.value = "asc";
  };

  /** @param {string | undefined} val */
  setGroupBy = async (val) => this.#groupBy.value = val;

  /** @param {string | undefined} val */
  setPlaylist = async (val) => this.#playlist.value = val;

  /** @param {string | undefined} val */
  setSearchTerm = async (val) => this.#searchTerm.value = val;

  /** @param {string[]} val */
  setSortBy = async (val) => this.#sortBy.value = val;

  /** @param {"asc" | "desc" | undefined} val */
  setSortDirection = async (val) => this.#sortDirection.value = val;
}

export default ScopeEngine;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ScopeEngine;
export const NAME = "de-scope";

defineElement(NAME, CLASS);
```
## Input
### Dropbox

Dropbox, using the Dropbox v2 HTTP API.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { DEFAULT_APP_KEY, SCHEME } from "./constants.js";
import { accountsFromTracks, buildURI } from "./common.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class DropboxInput extends DiffuseElement {
  static NAME = "diffuse/input/dropbox";
  static WORKER_URL = "components/input/dropbox/worker.js";

  SCHEME = SCHEME;

  /** @type {string} */
  appKey = DEFAULT_APP_KEY;

  static observedAttributes = ["app-key"];

  /**
   * @override
   * @param {string} name
   * @param {string} old
   * @param {string} next
   */
  attributeChangedCallback(name, old, next) {
    super.attributeChangedCallback(name, old, next);
    if (name === "app-key" && next !== null) this.appKey = next;
  }

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;
  }

  // 🛠️

  authorize() {
    localStorage.setItem("oauth/callback/redirect_path", location.pathname + location.search);

    const params = new URLSearchParams({
      response_type: "token",
      client_id: this.appKey,
      redirect_uri: location.origin + "/oauth/callback/",
    });

    location.assign(`https://www.dropbox.com/oauth2/authorize?${params}`);
  }

  /** @param {Track[]} tracks */
  sources(tracks) {
    return Object.values(accountsFromTracks(tracks)).map((account) => ({
      label: `Dropbox (${account.directoryPath})`,
      uri: buildURI(account),
    }));
  }
}

export default DropboxInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = DropboxInput;
export const NAME = "di-dropbox";

defineElement(NAME, CLASS);
```
### Ephemeral Cache

Ephemeral blobs stored in indexedDB, resolving creates temporary Blob URLs. Not responsible for storing blobs.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { SCHEME } from "./constants.js";

/**
 * @import { InputActions, InputSchemeProvider } from "@specs/components/input/types.d.ts"
 * @import { ProxiedActions } from "~/common/worker.d.ts"
 * @import { Track } from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class EphemeralCacheInput extends DiffuseElement {
  static NAME = "diffuse/input/ephemeral-cache";
  static WORKER_URL = "components/input/ephemeral-cache/worker.js";

  SCHEME = SCHEME;

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    const proxy = this.workerProxy();

    this.artwork = proxy.artwork;
    this.consult = proxy.consult;
    this.detach = proxy.detach;
    this.groupConsult = proxy.groupConsult;
    this.list = proxy.list;
    this.resolve = proxy.resolve;
  }

  // 🛠️

  /** @param {Track[]} tracks */
  sources(tracks) {
    return tracks.map((t) => ({
      label: t.uri,
      uri: t.uri,
    }));
  }
}

export default EphemeralCacheInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = EphemeralCacheInput;
export const NAME = "di-ephemeral-cache";

defineElement(NAME, CLASS);
```
### HTTPS

HTTPS URLs to audio files or streams.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { SCHEME } from "./constants.js";
import { hostsFromTracks } from "./common.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class HttpsInput extends DiffuseElement {
  static NAME = "diffuse/input/https";
  static WORKER_URL = "components/input/https/worker.js";

  SCHEME = SCHEME;

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;
  }

  // 🛠️

  /** @param {Track[]} tracks */
  sources(tracks) {
    const hosts = Object.values(hostsFromTracks(tracks));

    return hosts.map((host) => ({
      label: host,
      uri: `https://${host}`,
    }));
  }
}

export default HttpsInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = HttpsInput;
export const NAME = "di-https";

defineElement(NAME, CLASS);
```
### HTTPS + JSON

HTTPS servers with JSON directory listings.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { SCHEME } from "./constants.js";
import { buildURI, serversFromTracks } from "./common.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class HttpsJsonInput extends DiffuseElement {
  static NAME = "diffuse/input/https-json";
  static WORKER_URL = "components/input/https-json/worker.js";

  SCHEME = SCHEME;

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;
  }

  // 🛠️

  /** @param {Track[]} tracks */
  sources(tracks) {
    return Object.values(serversFromTracks(tracks)).map((server) => ({
      label: `${server.host}${server.dir}`,
      uri: buildURI(server),
    }));
  }
}

export default HttpsJsonInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = HttpsJsonInput;
export const NAME = "di-https-json";

defineElement(NAME, CLASS);
```
### Icecast

Icecast internet radio streams. Fetches ICY metadata to populate track information.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { hostsFromTracks } from "./common.js";
import { SCHEME } from "./constants.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class IcecastInput extends DiffuseElement {
  static NAME = "diffuse/input/icecast";
  static WORKER_URL = "components/input/icecast/worker.js";

  SCHEME = SCHEME;

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;
  }

  // 🛠️

  /** @param {Track[]} tracks */
  sources(tracks) {
    const hosts = Object.values(hostsFromTracks(tracks));

    return hosts.map((host) => ({
      label: host,
      uri: `${SCHEME}://${host}`,
    }));
  }
}

export default IcecastInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = IcecastInput;
export const NAME = "di-icecast";

defineElement(NAME, CLASS);
```
### Local

Audio files or directories from your local device, using the browser's File System Access API.

```js
import * as TID from "@atcute/tid";
import { defineElement, DiffuseElement } from "~/common/element.js";
import { SCHEME } from "./constants.js";
import {
  buildURI,
  loadHandles,
  saveHandles,
  tidsFromTracks,
} from "./common.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class LocalInput extends DiffuseElement {
  static NAME = "diffuse/input/local";
  static WORKER_URL = "components/input/local/worker.js";

  SCHEME = SCHEME;

  /** @type {Map<string, string>} tid → handle name */
  #names = new Map();

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;
  }

  // LIFECYCLE

  /** @override */
  async connectedCallback() {
    super.connectedCallback();
    const handles = await loadHandles();
    for (const [tid, handle] of Object.entries(handles)) {
      this.#names.set(tid, handle.name);
    }
  }

  // 🛠️

  /**
   * Prompts the user to pick a directory.
   * Stores handle in IDB.
   * Returns the URI for the track placeholder.
   */
  async addDirectory() {
    const dirHandle = await /** @type {any} */ (globalThis).showDirectoryPicker(
      { mode: "read" },
    );

    const tid = TID.now();
    const handles = await loadHandles();

    handles[tid] = dirHandle;
    await saveHandles(handles);
    this.#names.set(tid, dirHandle.name);

    return buildURI(tid);
  }

  /**
   * Prompts the user to pick one or more files.
   * Stores handles in IDB.
   * Returns the URIs for the track placeholders.
   */
  async addFiles() {
    const fileHandles = await /** @type {any} */ (globalThis)
      .showOpenFilePicker({ multiple: true });
    const handles = await loadHandles();
    const uris = [];

    for (const fileHandle of fileHandles) {
      const tid = TID.now();
      handles[tid] = fileHandle;
      this.#names.set(tid, fileHandle.name);
      uris.push(buildURI(tid));
    }

    await saveHandles(handles);
    return uris;
  }

  /** @param {Track[]} tracks */
  sources(tracks) {
    return Object.values(tidsFromTracks(tracks)).map((tid) => ({
      label: this.#names.get(tid) ?? tid,
      uri: buildURI(tid),
    }));
  }
}

export default LocalInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = LocalInput;
export const NAME = "di-local";

defineElement(NAME, CLASS);
```
### Opensubsonic

(Open)subsonic audio servers.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { SCHEME } from "./constants.js";
import { buildURI, serversFromTracks } from "./common.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class OpensubsonicInput extends DiffuseElement {
  static NAME = "diffuse/input/opensubsonic";
  static WORKER_URL = "components/input/opensubsonic/worker.js";

  SCHEME = SCHEME;

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;
  }

  // 🛠️

  /** @param {Track[]} tracks */
  sources(tracks) {
    return Object.values(serversFromTracks(tracks)).map((server) => {
      return {
        label: server.host,
        uri: buildURI(server),
      };
    });
  }
}

export default OpensubsonicInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = OpensubsonicInput;
export const NAME = "di-opensubsonic";

defineElement(NAME, CLASS);
```
### S3

AWS S3 and services that provide the same surface API such as Cloudflare R2.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { SCHEME } from "./constants.js";
import { bucketsFromTracks, buildURI } from "./common.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 * @import {Demo} from "@specs/components/input/s3/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class S3Input extends DiffuseElement {
  static NAME = "diffuse/input/s3";
  static WORKER_URL = "components/input/s3/worker.js";

  SCHEME = SCHEME;

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions & { demo: () => Demo }>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;

    this.demo = this.proxy.demo;
  }

  // 🛠️

  /** @param {Track[]} tracks */
  sources(tracks) {
    return Object.values(bucketsFromTracks(tracks)).map((server) => {
      return {
        label: `${server.bucketName} (${server.host})`,
        uri: buildURI(server),
      };
    });
  }
}

export default S3Input;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = S3Input;
export const NAME = "di-s3";

defineElement(NAME, CLASS);
```
### WebDAV

WebDAV servers. Depends on a service worker handling the `diffuse:basic-auth` query parameter and converting it to a `Authorization` header.

```js
import { defineElement, DiffuseElement } from "~/common/element.js";
import { SCHEME } from "./constants.js";
import { buildURI, serversFromTracks } from "./common.js";

/**
 * @import {InputActions, InputSchemeProvider} from "@specs/components/input/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<InputActions>}
 * @implements {InputSchemeProvider}
 */
class WebdavInput extends DiffuseElement {
  static NAME = "diffuse/input/webdav";
  static WORKER_URL = "components/input/webdav/worker.js";

  SCHEME = SCHEME;

  constructor() {
    super();

    /** @type {ProxiedActions<InputActions>} */
    this.proxy = this.workerProxy();

    this.artwork = this.proxy.artwork;
    this.consult = this.proxy.consult;
    this.detach = this.proxy.detach;
    this.groupConsult = this.proxy.groupConsult;
    this.list = this.proxy.list;
    this.resolve = this.proxy.resolve;
  }

  // 🛠️

  /** @param {Track[]} tracks */
  sources(tracks) {
    return Object.values(serversFromTracks(tracks)).map((server) => ({
      label: `${server.host}${server.dir}`,
      uri: buildURI(server),
    }));
  }
}

export default WebdavInput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = WebdavInput;
export const NAME = "di-webdav";

defineElement(NAME, CLASS);
```
## Metadata
### Audio File

Extracts tags and audio stats from audio files using the music-metadata library.

```js
import { defineElement, DiffuseElement, query } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {InputElement} from "@specs/components/input/types.d.ts"
 * @import {Actions} from "@specs/components/metadata/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ProxiedActions<Actions>}
 */
class AudioFileMetadata extends DiffuseElement {
  static NAME = "diffuse/metadata/audio-file";
  static WORKER_URL = "components/metadata/audio-file/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const p = this.workerProxy();

    this.patch = p.patch;
  }

  // LIFECYCLE

  /** @override */
  async connectedCallback() {
    super.connectedCallback();

    /** @type {InputElement} */
    this.input = query(this, "input-selector");

    await customElements.whenDefined(this.input.localName);
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    if (!this.input) throw new Error("Input element not defined yet");
    return { input: this.input };
  }
}

export default AudioFileMetadata;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = AudioFileMetadata;
export const NAME = "dm-audio-file";

defineElement(NAME, AudioFileMetadata);
```
## Orchestrators
### Artwork

Fetches cover art for a given set of tracks, stored locally in indexedDB. Uses the artwork configurator to try each configured source in sequence.

```js
import { defineElement, DiffuseElement, query } from "~/common/element.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Actions} from "@specs/components/artwork/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class ArtworkOrchestrator extends DiffuseElement {
  static NAME = "diffuse/orchestrator/artwork";
  static WORKER_URL = "components/orchestrator/artwork/worker.js";

  constructor() {
    super();

    /** @type {ProxiedActions<Actions>} */
    const p = this.workerProxy();

    this.get = p.get;
  }

  // LIFECYCLE

  /** @override */
  async connectedCallback() {
    super.connectedCallback();

    /** @type {import("~/components/configurator/artwork/element.js").CLASS} */
    this.artworkConfigurator = query(this, "artwork-selector");

    await customElements.whenDefined(this.artworkConfigurator.localName);
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    if (!this.artworkConfigurator) {
      throw new Error("Artwork configurator element not defined yet");
    }

    return {
      artwork: this.artworkConfigurator,
    };
  }
}

export default ArtworkOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ArtworkOrchestrator;
export const NAME = "do-artwork";

defineElement(NAME, ArtworkOrchestrator);
```
### Automatic Queue

Fill the queue automatically with non-manual items (shuffled or regular, based on repeat-shuffle engine).

```js
import { BroadcastableDiffuseElement, defineElement, query } from "~/common/element.js";

/**
 * @import {DiffuseElement} from "~/common/element.js";
 * @import {SignalReader} from "~/common/signal.d.ts";
 * @import {Track} from "~/definitions/types.d.ts"
 * @import RepeatShuffleEngine from "~/components/engine/repeat-shuffle/element.js"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * Update the queue pool whenever tracks have been loaded,
 * or the tracks collection changes.
 */
class AutoTracksOrchestrator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/auto-queue";

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      this.broadcast(this.identifier, {});
    }

    // Super
    super.connectedCallback();

    /** @type {import("~/components/engine/queue/element.js").CLASS} */
    const queue = query(this, "queue-engine-selector");

    /** @type {RepeatShuffleEngine} */
    const repeatShuffle = query(this, "repeat-shuffle-engine-selector");

    /** @type {DiffuseElement & { tracks: SignalReader<Track[]> }} */
    const tracksElement = query(this, "tracks-selector");

    // When defined
    await customElements.whenDefined(queue.localName);
    await customElements.whenDefined(repeatShuffle.localName);
    await customElements.whenDefined(tracksElement.localName);

    // Watch tracks
    this.effect(() => {
      const tracks = tracksElement.tracks();

      this.isLeader().then(async (isLeader) => {
        if (!isLeader) return;
        queue.supply({ trackIds: tracks.map((t) => t.id) });
      });
    });

    // Automatically fill queue
    let lastShuffle = repeatShuffle.shuffle();
    let lastFingerprint = queue.supplyFingerprint();

    this.effect(() => {
      const trigger = queue.now();
      const fingerprint = queue.supplyFingerprint();
      const shuffled = repeatShuffle.shuffle();

      this.isLeader().then((isLeader) => {
        if (!isLeader) return;

        // Clear non-manual items from the queue
        // when 'shuffle' gets turned off or on;
        // or when queue supply changes.
        if (shuffled !== lastShuffle || fingerprint !== lastFingerprint) {
          lastShuffle = shuffled;
          lastFingerprint = fingerprint;
          queue.clear({ keepManual: true });
        }

        queue.fill({ amount: 10, shuffled: repeatShuffle.shuffle() });

        // Insert now-playing track if there's none
        if (!trigger) queue.shift();
      });
    });
  }
}

export default AutoTracksOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = AutoTracksOrchestrator;
export const NAME = "do-auto-queue";

defineElement(NAME, CLASS);
```
### Controller

Provides commonly used computed signals derived from the audio engine, queue engine, and output. Exposes currentTrack(), isPlaying(), and references to the underlying engines.

```js
import { defineElement, DiffuseElement, query, whenElementsDefined } from "~/common/element.js";
import { computed, signal } from "~/common/signal.js";

/**
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 * @import AudioEngine from "~/components/engine/audio/element.js"
 * @import QueueEngine from "~/components/engine/queue/element.js"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * Provides commonly used computed signals derived from the audio engine,
 * queue engine, and output — so theme elements don't need to re-implement them.
 */
class ControllerOrchestrator extends DiffuseElement {
  static NAME = "diffuse/orchestrator/controller";

  // SIGNALS - DEPENDENCIES

  $audio = signal(/** @type {AudioEngine | undefined} */ (undefined));
  $output = signal(/** @type {OutputElement | undefined} */ (undefined));
  $queue = signal(/** @type {QueueEngine | undefined} */ (undefined));

  // SIGNALS - COMPUTED

  audio = computed(() => {
    const curr = this.$queue.value?.now();
    return curr ? this.$audio.value?.state(curr.id) : undefined;
  });

  currentTrack = computed(() => {
    const item = this.$queue.value?.now();
    if (!item) return undefined;
    const col = this.$output.value?.tracks.collection();
    if (!col || col.state !== "loaded") return undefined;
    return col.data.find((t) => t.id === item.id);
  });

  isPlaying = computed(() => {
    return this.$audio.value?.isPlaying();
  });

  // LIFECYCLE

  /**
   * @override
   */
  connectedCallback() {
    super.connectedCallback();

    /** @type {AudioEngine} */
    const audio = query(this, "audio-engine-selector");

    /** @type {OutputElement} */
    const output = query(this, "output-selector");

    /** @type {QueueEngine} */
    const queue = query(this, "queue-engine-selector");

    whenElementsDefined({ audio, output, queue }).then(() => {
      this.$audio.value = audio;
      this.$output.value = output;
      this.$queue.value = queue;
    });
  }
}

export default ControllerOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ControllerOrchestrator;
export const NAME = "do-controller";

defineElement(NAME, CLASS);
```
### Cover Groups

Groups tracks by cover art to form collections.

```js
import { defineElement, DiffuseElement, query } from "~/common/element.js";
import { computed, signal } from "~/common/signal.js";

/**
 * @import {SignalReader} from "~/common/signal.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class CoverGroupsOrchestrator extends DiffuseElement {
  static NAME = "diffuse/orchestrator/cover-groups";

  // SIGNALS

  #provider = signal(
    /** @type {DiffuseElement & { tracks: SignalReader<Track[]> } | null} */ (null),
  );

  // STATE

  artistGroups = computed(() => {
    const provider = this.#provider.value;
    const groups = /** @type {any} */ (provider)?.groups?.();

    /** @type {{ label: string; groups: ArtistGroup[] }[]} */
    const result = [];

    if (groups?.length) {
      const allTracks = provider?.tracks() ?? [];

      // Total track counts per artist across all groups
      /** @type {Map<string, number>} */
      const totalCounts = new Map();
      for (const track of allTracks) {
        const key = String(track.tags?.artist ?? "").toLowerCase();
        totalCounts.set(key, (totalCounts.get(key) ?? 0) + 1);
      }

      for (
        const group
          of /** @type {{ label: string; tracks: Track[] }[]} */ (groups)
      ) {
        const artists = deduplicateArtists(group.tracks).map((a) => ({
          ...a,
          trackCount: totalCounts.get(a.artistKey) ?? a.trackCount,
        }));
        if (artists.length) result.push({ label: group.label, groups: artists });
      }
    } else {
      const allTracks = provider?.tracks() ?? [];
      const artists = deduplicateArtists(allTracks);
      if (artists.length) result.push({ label: "", groups: artists });
    }

    return result;
  });

  coverGroups = computed(() => {
    const provider = this.#provider.value;
    const groups = /** @type {any} */ (provider)?.groups?.();

    /** @type {{ label: string; groups: CoverGroup[] }[]} */
    const result = [];

    if (groups?.length) {
      for (
        const group
          of /** @type {{ label: string; tracks: Track[] }[]} */ (groups)
      ) {
        const albums = deduplicateAlbums(group.tracks);
        if (albums.length) result.push({ label: group.label, groups: albums });
      }
    } else {
      const tracks = provider?.tracks() ?? [];
      const albums = deduplicateAlbums(tracks);
      if (albums.length) result.push({ label: "", groups: albums });
    }

    return result;
  });

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    super.connectedCallback();

    /** @type {DiffuseElement & { tracks: SignalReader<Track[]> }} */
    const provider = query(this, "tracks-selector");

    await customElements.whenDefined(provider.localName);
    this.#provider.value = provider;
  }
}

export default CoverGroupsOrchestrator;

////////////////////////////////////////////
// HELPERS
////////////////////////////////////////////

/**
 * @typedef {{ albumKey: string; albumName: string; artist: string; track: Track }} CoverGroup
 */

/**
 * @typedef {{ artistKey: string; artistName: string; trackCount: number; track: Track }} ArtistGroup
 */

/**
 * @param {Track[]} tracks
 * @returns {CoverGroup[]}
 */
function deduplicateAlbums(tracks) {
  /** @type {Map<string, { track: Track; artists: Set<string> }>} */
  const albumMap = new Map();

  for (const track of tracks) {
    const albumKey = String(track.tags?.album ?? "").toLowerCase();
    const existing = albumMap.get(albumKey);
    if (existing) {
      existing.artists.add(track.tags?.artist ?? "Unknown artist");
    } else {
      albumMap.set(albumKey, {
        track,
        artists: new Set([track.tags?.artist ?? "Unknown artist"]),
      });
    }
  }

  return [...albumMap.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([albumKey, { track, artists }]) => ({
      albumKey,
      albumName: track.tags?.album ?? "Unknown album",
      artist: artists.size > 1 ? "Various Artists" : /** @type {string} */ (artists.values().next().value),
      track,
    }));
}

/**
 * @param {Track[]} tracks
 * @returns {ArtistGroup[]}
 */
function deduplicateArtists(tracks) {
  /** @type {Map<string, { artistName: string; count: number; track: Track }>} */
  const map = new Map();

  for (const track of tracks) {
    const artistKey = String(track.tags?.artist ?? "").toLowerCase();
    const existing = map.get(artistKey);
    if (existing) {
      existing.count++;
    } else {
      map.set(artistKey, {
        artistName: track.tags?.artist ?? "Unknown artist",
        count: 1,
        track,
      });
    }
  }

  return [...map.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([artistKey, { artistName, count, track }]) => ({
      artistKey,
      artistName,
      trackCount: count,
      track,
    }));
}

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = CoverGroupsOrchestrator;
export const NAME = "do-cover-groups";

defineElement(NAME, CLASS);
```
### Favourites

Mark tracks as favourites. Automatically creates an unordered 'Favourites' playlist.

```js
import {
  BroadcastableDiffuseElement,
  defineElement,
  query,
} from "~/common/element.js";
import { match as matchPlaylistItem } from "~/common/playlist.js";
import { computed, signal } from "~/common/signal.js";
import { filterFavourites } from "./common.js";
import * as Output from "~/common/output.js";

/**
 * @import {Track} from "~/definitions/types.d.ts"
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 * @import {Actions} from "@specs/components/orchestrator/favourites/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class FavouritesOrchestrator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/favourites";
  static WORKER_URL = "components/orchestrator/favourites/worker.js";

  /** @type {ProxiedActions<Actions>} */
  #proxy;

  #toggleInFlight = false;

  constructor() {
    super();

    this.#proxy = this.workerProxy();

    // Bind methods for broadcasting
    this.include = this.include.bind(this);
    this.expel = this.expel.bind(this);
    this.toggle = this.toggle.bind(this);
  }

  // SIGNALS

  #output = signal(/** @type {OutputElement | null} */ (null));

  // STATE

  /**
   * Returns the favourites playlist items.
   */
  playlistItems = computed(() => {
    const output = this.#output.value;
    if (!output) return [];

    const col = output.playlistItems.collection();
    if (col.state !== "loaded") return [];
    return filterFavourites(col.data);
  });

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        include: { strategy: "leaderOnly", fn: this.include },
        expel: { strategy: "leaderOnly", fn: this.expel },
        toggle: { strategy: "leaderOnly", fn: this.toggle },
      });

      if (actions) {
        this.include = actions.include;
        this.expel = actions.expel;
        this.toggle = actions.toggle;
      }
    }

    // Super
    super.connectedCallback();

    /** @type {OutputElement} */
    const output = query(this, "output-selector");

    // Wait until defined
    await customElements.whenDefined(output.localName);

    this.#output.value = output;
  }

  // ACTIONS

  /**
   * Add one or more tracks to favourites.
   * @param {Track | Track[]} tracks
   */
  async include(tracks) {
    const tracksArray = Array.isArray(tracks) ? tracks : [tracks];
    if (tracksArray.length === 0) return;

    const output = this.#output.value;
    if (!output) {
      console.warn("Favourites orchestrator: output element not ready");
      return;
    }

    const playlistItems = await Output.data(output.playlistItems);
    const result = await this.#proxy.include({
      playlistItems,
      tracks: tracksArray,
    });

    if (result) await output.playlistItems.save(result);
  }

  /**
   * Remove one or more tracks from favourites.
   * @param {Track | Track[]} tracks
   */
  async expel(tracks) {
    const tracksArray = Array.isArray(tracks) ? tracks : [tracks];
    if (tracksArray.length === 0) return;

    const output = this.#output.value;
    if (!output) {
      console.warn("Favourites orchestrator: output element not ready");
      return;
    }

    const playlistItems = await Output.data(output.playlistItems);
    const result = await this.#proxy.expel({
      playlistItems,
      tracks: tracksArray,
    });

    if (result) await output.playlistItems.save(result);
  }

  /**
   * Toggle favourite status for one or more tracks.
   * Adds tracks if not in favourites, removes if already favourited.
   * @param {Track | Track[]} tracks
   */
  async toggle(tracks) {
    if (this.#toggleInFlight) return;
    this.#toggleInFlight = true;

    try {
      const tracksArray = Array.isArray(tracks) ? tracks : [tracks];
      if (tracksArray.length === 0) return;

      const output = this.#output.value;
      if (!output) {
        console.warn("Favourites orchestrator: output element not ready");
        return;
      }

      const playlistItems = await Output.data(output.playlistItems);
      const result = await this.#proxy.toggle({
        playlistItems,
        tracks: tracksArray,
      });

      if (result) await output.playlistItems.save(result);
    } finally {
      this.#toggleInFlight = false;
    }
  }

  // 🛠️

  /**
   * Check if a track is a favourite.
   *
   * @param {Track} track
   */
  isFavourite(track) {
    return this.playlistItems().some((item) => {
      return matchPlaylistItem(track, item);
    });
  }
}

export default FavouritesOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = FavouritesOrchestrator;
export const NAME = "do-favourites";

defineElement(NAME, CLASS);
```
### Media Session

Keeps the browser/os media session in sync with queue and audio state. Adds handlers for previous, next, seek to, etc.

```js
import {
  BroadcastableDiffuseElement,
  defineElement,
  query,
  queryOptional,
} from "~/common/element.js";
import { signal } from "~/common/signal.js";

/**
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 * @import ArtworkOrchestrator from "~/components/orchestrator/artwork/element.js"
 * @import {default as AudioEngine} from "~/components/engine/audio/element.js"
 * @import {default as QueueEngine} from "~/components/engine/queue/element.js"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * Keeps the browser/OS Media Session in sync with queue and audio engine state.
 *
 * Forwards play, pause, seek and track-skip actions from the OS back to the engines.
 */
class MediaSessionOrchestrator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/media-session";

  // SIGNALS

  /** @type {string | null} */
  #artworkUrl = null;

  #isLeader = signal(true);

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    if (this.hasAttribute("group")) {
      this.broadcast(this.identifier, {});
    }

    super.connectedCallback();

    if (!("mediaSession" in navigator)) return;

    /** @type {AudioEngine} */
    this.audio = query(this, "audio-engine-selector");
    /** @type {QueueEngine} */
    this.queue = query(this, "queue-engine-selector");
    /** @type {OutputElement | null} */
    this.output = queryOptional(this, "output-selector");
    /** @type {ArtworkOrchestrator | null} */
    this.artwork = queryOptional(this, "artwork-selector");

    await Promise.all([
      customElements.whenDefined(this.audio.localName),
      customElements.whenDefined(this.queue.localName),
      this.output && customElements.whenDefined(this.output.localName),
      this.artwork && customElements.whenDefined(this.artwork.localName),
    ].filter(Boolean));

    this.effect(() => {
      const promise = this.isLeader();
      promise?.then((b) => this.#isLeader.set(b));
    });

    this.#registerActionHandlers();

    this.effect(() => this.#syncMetadata());
    this.effect(() => this.#syncPlaybackState());
    this.effect(() => this.#syncPositionState());
  }

  // 🛠️

  async #syncMetadata() {
    if (!this.queue) return;

    const now = this.queue.now();
    const tracksCol = this.output?.tracks.collection();
    const track = now && tracksCol?.state === "loaded"
      ? tracksCol.data.find((t) => t.id === now.id)
      : undefined;

    if (!track) {
      if (this.#artworkUrl) {
        URL.revokeObjectURL(this.#artworkUrl);
        this.#artworkUrl = null;
      }
      navigator.mediaSession.metadata = null;
      return;
    }

    const tags = track.tags ?? {};

    navigator.mediaSession.metadata = new MediaMetadata({
      title: tags.title ?? "",
      artist: tags.artist ?? tags.albumartist ?? "",
      album: tags.album ?? "",
      artwork: [],
    });

    // Optionally fetch and attach artwork
    if (this.artwork) {
      const artworkOrchestrator = this.artwork;

      /** @type {Uint8Array | null} */
      let bytes = null;

      try {
        bytes = await artworkOrchestrator.get(track);
      } catch {
        bytes = null;
      }

      if (bytes && navigator.mediaSession.metadata) {
        const mime = detectMime(bytes);
        const blob = new Blob([/** @type {ArrayBuffer} */ (bytes.buffer)], {
          type: mime,
        });

        const url = URL.createObjectURL(blob);
        const nowLater = this.queue.now();

        // If in the meantime the now-playing track has changed,
        // don't set the artwork.
        if (nowLater?.id !== now?.id) {
          URL.revokeObjectURL(url);
          return;
        }

        if (this.#artworkUrl) URL.revokeObjectURL(this.#artworkUrl);
        this.#artworkUrl = url;

        navigator.mediaSession.metadata = new MediaMetadata({
          title: tags.title ?? "",
          artist: tags.artist ?? tags.albumartist ?? "",
          album: tags.album ?? "",
          artwork: [{ src: url, type: mime }],
        });
      }
    }
  }

  #syncPlaybackState() {
    if (!this.audio) return;
    navigator.mediaSession.playbackState = this.audio.isPlaying()
      ? "playing"
      : "paused";
  }

  #syncPositionState() {
    if (!this.audio || !this.queue) return;

    const now = this.queue.now();
    if (!now) return;

    const state = this.audio.state(now.id);
    if (!state) return;

    const duration = state.duration();
    const progress = state.progress();

    if (!duration || isNaN(duration) || duration === 0) return;
    if (navigator.mediaSession.playbackState === "none") return;

    try {
      navigator.mediaSession.setPositionState({
        duration,
        position: Math.min(duration * progress, duration),
        playbackRate: 1,
      });
    } catch {
      // setPositionState may throw if duration is not finite
    }
  }

  #registerActionHandlers() {
    navigator.mediaSession.setActionHandler("play", () => {
      if (!this.audio || !this.queue) return;
      if (!this.#isLeader) return;
      const now = this.queue.now();
      if (now) this.audio.play({ audioId: now.id });
    });

    navigator.mediaSession.setActionHandler("pause", () => {
      if (!this.audio || !this.queue) return;
      if (!this.#isLeader) return;
      const now = this.queue.now();
      if (now) this.audio.pause({ audioId: now.id });
    });

    navigator.mediaSession.setActionHandler("previoustrack", () => {
      if (!this.queue) return;
      if (!this.#isLeader) return;
      this.queue.unshift();
    });

    navigator.mediaSession.setActionHandler("nexttrack", () => {
      if (!this.queue) return;
      if (!this.#isLeader) return;
      this.queue.shift();
    });

    navigator.mediaSession.setActionHandler("seekto", (details) => {
      if (!this.audio || !this.queue) return;
      if (!this.#isLeader) return;
      const now = this.queue.now();
      if (!now || details.seekTime == null) return;
      const state = this.audio.state(now.id);
      const duration = state?.duration();
      if (!duration || duration === 0) return;
      this.audio.seek({
        audioId: now.id,
        percentage: details.seekTime / duration,
      });
    });
  }
}

export default MediaSessionOrchestrator;

////////////////////////////////////////////
// 🛠️
////////////////////////////////////////////

/**
 * @param {Uint8Array} bytes
 * @returns {string}
 */
function detectMime(bytes) {
  if (bytes[0] === 0xFF && bytes[1] === 0xD8) return "image/jpeg";
  if (bytes[0] === 0x89 && bytes[1] === 0x50) return "image/png";
  if (bytes[0] === 0x47 && bytes[1] === 0x49) return "image/gif";
  if (bytes[0] === 0x52 && bytes[1] === 0x49) return "image/webp";
  return "image/jpeg";
}

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = MediaSessionOrchestrator;
export const NAME = "do-media-session";

defineElement(NAME, MediaSessionOrchestrator);
```
### Output

A default output configuration. Contains all the outputs provided here along with the relevant transformers.

```js
import { ifDefined } from "lit-html/directives/if-defined.js";
import {
  DEFAULT_GROUP,
  defineElement,
  DiffuseElement,
} from "~/common/element.js";

import "~/components/configurator/output/element.js";
import "~/components/transformer/output/refiner/default/element.js";
import "~/components/transformer/output/refiner/initial-contents/element.js";

import "~/components/output/polymorphic/indexed-db/element.js";
import "~/components/transformer/output/string/json/element.js";

/**
 * @import {RenderArg} from "~/common/element.d.ts"
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 * @import {OutputConfiguratorElement} from "@specs/components/configurator/output/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * A default setup for managing output.
 *
 * @implements {OutputConfiguratorElement}
 */
class OutputOrchestrator extends DiffuseElement {
  static NAME = "diffuse/orchestrator/output";

  // ELEMENT GETTERS

  /**
   * @returns {OutputElement}
   */
  get output() {
    /** @type {OutputElement | null} */
    const output = this.root().querySelector("#do-output__output");

    if (!output) throw new Error("Output orchestrator did not render yet.");
    return output;
  }

  /**
   * @returns {OutputConfiguratorElement}
   */
  get outputConfigurator() {
    /** @type {OutputConfiguratorElement | null} */
    const outputConfigurator = this.root().querySelector(
      "#do-output__dc-output",
    );

    if (!outputConfigurator) {
      throw new Error("Output orchestrator did not render yet.");
    }

    return outputConfigurator;
  }

  // PROXY OUTPUT ACTIONS

  get facets() {
    return this.output.facets;
  }

  get playlistItems() {
    return this.output.playlistItems;
  }

  get settings() {
    return this.output.settings;
  }

  get tracks() {
    return this.output.tracks;
  }

  get ready() {
    return this.output.ready;
  }

  // PROXY ADDITIONAL OUTPUT CONFIGURATOR ACTIONS

  get activated() {
    return this.outputConfigurator.activated.bind(this.outputConfigurator);
  }

  get deselect() {
    return this.outputConfigurator.deselect.bind(this.outputConfigurator);
  }

  get hasDefault() {
    return this.outputConfigurator.hasDefault.bind(this.outputConfigurator);
  }

  get hasSelected() {
    return this.outputConfigurator.hasSelected.bind(this.outputConfigurator);
  }

  get loadSelected() {
    return this.outputConfigurator.loadSelected.bind(this.outputConfigurator);
  }

  get options() {
    return this.outputConfigurator.options.bind(this.outputConfigurator);
  }

  get select() {
    return this.outputConfigurator.select.bind(this.outputConfigurator);
  }

  get selected() {
    return this.outputConfigurator.selected.bind(this.outputConfigurator);
  }

  get waitForOption() {
    return this.outputConfigurator.waitForOption.bind(this.outputConfigurator);
  }

  // RENDER

  /**
   * @param {RenderArg} _
   */
  render({ html }) {
    const group = this.group === DEFAULT_GROUP ? undefined : this.group;

    return html`
      <dop-indexed-db
        id="do-output__dop-indexed-db__json"
        namespace="${this.namespace ? this.namespace + "/" : ""}json"
        group="${ifDefined(group)}"
      ></dop-indexed-db>

      <!-- OUTPUT CONFIGURATOR -->
      <dc-output
        id="do-output__dc-output"
        default="do-output__dc-output__local"
        group="${ifDefined(group)}"
      >
        <dtos-json
          id="do-output__dc-output__local"
          output-selector="#do-output__dop-indexed-db__json"
          label="Local"
        ></dtos-json>
      </dc-output>

      <!-- ENTRY ⬆️ -->
      <dtor-initial-contents
        id="do-output__dtor-initial-contents"
        output-selector="#do-output__dc-output"
      ></dtor-initial-contents>

      <dtor-default
        id="do-output__output"
        output-selector="#do-output__dtor-initial-contents"
        group="${ifDefined(group)}"
      ></dtor-default>
    `;
  }
}

export default OutputOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = OutputOrchestrator;
export const NAME = "do-output";

defineElement(NAME, CLASS);
```
### Path Collections

Wraps an output element to generate ephemeral playlists based on the first path segment of each track's URI. Ephemeral items are excluded from storage.

```js
import { computed } from "~/common/signal.js";
import { OutputTransformer } from "~/components/transformer/output/base.js";
import { defineElement } from "~/common/element.js";

/**
 * @import {PlaylistItem, Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class PathCollectionsOrchestrator extends OutputTransformer {
  static NAME = "diffuse/orchestrator/path-collections";

  constructor() {
    super();

    const base = this.base();

    const ephemeralItems = computed(() => {
      const col = base.tracks.collection();
      return createEphemeralItems(col.state === "loaded" ? col.data : []);
    });

    this.facets = base.facets;
    this.settings = base.settings;
    this.playlistItems = {
      ...base.playlistItems,
      collection: computed(() => {
        const col = base.playlistItems.collection();
        if (col.state !== "loaded") return col;
        return { state: "loaded", data: [...col.data, ...ephemeralItems()] };
      }),
      /** @type {(typeof base.playlistItems.save)} */
      save: async (items) => {
        await base.playlistItems.save(
          (items || []).filter((p) => !p.ephemeral),
        );
      },
    };
    this.tracks = base.tracks;

    this.ready = base.ready;
  }
}

/**
 * @param {Track[]} tracks
 * @returns {PlaylistItem[]}
 */
function createEphemeralItems(tracks) {
  /** @type {PlaylistItem[]} */
  const items = [];

  /** @type {Set<string>} */
  const segmentsAdded = new Set();

  tracks.forEach((track) => {
    const segment = pathSegment(track.uri);
    if (!segment) return;

    if (segmentsAdded.has(segment)) return;

    /** @type {PlaylistItem} */
    const item = {
      $type: "sh.diffuse.output.playlistItem",
      id: `path-collections/${track.id}`,
      playlist: segment,
      criteria: [{ field: "uri", value: /** @type {any} */ (track.uri) }],
      ephemeral: true,
    };

    items.push(item);
    segmentsAdded.add(segment);
  });

  return items;
}

/**
 * @param {string} uri
 * @returns {string | undefined}
 */
function pathSegment(uri) {
  try {
    const url = new URL(uri);
    const segment = url.pathname.split("/").filter(Boolean)[0];
    return segment || undefined;
  } catch {
    return undefined;
  }
}

export default PathCollectionsOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = PathCollectionsOrchestrator;
export const NAME = "do-path-collections";

defineElement(NAME, CLASS);
```
### Process Inputs Into Tracks

Whenever the cached tracks are initially loaded through the passed output element it will list tracks by using the passed input element. Afterwards it loops over all tracks and checks if metadata needs to be fetched. If anything has changed, it'll pass the results to the output element.

```js
import {
  BroadcastableDiffuseElement,
  defineElement,
  query,
} from "~/common/element.js";
import { data, mergeTracks } from "~/common/output.js";
import { signal, untracked } from "~/common/signal.js";
import { listen } from "~/common/worker.js";

import { parseDisabledUris } from "~/components/orchestrator/sources/common.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {InputElement} from "@specs/components/input/types.d.ts"
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 * @import MetadataConfigurator from "~/components/configurator/metadata/element.js"
 *
 * @import {Actions, Progress} from "@specs/components/orchestrator/process-tracks/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * Processes inputs into tracks whenever
 * the already existing tracks are loaded
 * from the assigned output element.
 */
class ProcessTracksOrchestrator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/process-tracks";
  static WORKER_URL = "components/orchestrator/process-tracks/worker.js";

  /** @type {ProxiedActions<Actions>} */
  #proxy;

  constructor() {
    super();
    this.#proxy = this.workerProxy({
      forceNew: {
        dependencies: { input: true },
      },
    });
  }

  // SIGNALS

  #isProcessing = signal(false);
  #performedInitialProcess = signal(false);
  #progress = signal(/** @type {Progress} */ ({ processed: 0, total: 0 }));

  // STATE

  isProcessing = this.#isProcessing.get;
  progress = this.#progress.get;

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        getPerfInit: {
          strategy: "leaderOnly",
          fn: this.#performedInitialProcess.get,
        },
        setPerfInit: {
          strategy: "replicate",
          fn: this.#performedInitialProcess.set,
        },
        getIsProcessing: {
          strategy: "leaderOnly",
          fn: this.#isProcessing.get,
        },
        setIsProcessing: {
          strategy: "replicate",
          fn: this.#isProcessing.set,
        },
        process: { strategy: "leaderOnly", fn: this.process },
      });

      if (!actions) return;

      this.process = actions.process;
      this.#isProcessing.set = actions.setIsProcessing;

      // Sync #performedInitialProcess and #isProcessing with leader
      actions.getPerfInit().then((val) => {
        this.#performedInitialProcess.value = val;
      });

      actions.getIsProcessing().then((val) => {
        this.#isProcessing.value = val;
      });
    }

    // Super
    super.connectedCallback();

    /** @type {InputElement} */
    const input = query(this, "input-selector");

    /** @type {OutputElement} */
    const output = query(this, "output-selector");

    /** @type {MetadataConfigurator} */
    const metadataConfigurator = query(this, "metadata-selector");

    // Assign to self
    this.input = input;
    this.output = output;
    this.metadataConfigurator = metadataConfigurator;

    // Worker link
    const link = this.workerLink();

    // Wait until defined
    await customElements.whenDefined(input.localName);
    await customElements.whenDefined(output.localName);
    await customElements.whenDefined(metadataConfigurator.localName);

    // Sync progress with worker
    listen("progress", this.#progress.set, link);
    this.#proxy.progress().then(this.#progress.set);

    //
    listen("list", /** @param {Track[]} tracks */ async (tracks) => {
      if (!this.output) return;
      this.output.tracks.save(tracks);
    }, link);

    // Save patched tracks as they arrive so progress isn't lost.
    // Merge with existing tracks to avoid overwriting ones not yet patched.
    listen("patch", /** @param {Track[]} tracks */ async (tracks) => {
      if (!this.output) return;
      const existing = await data(this.output.tracks);
      const merged = mergeTracks(existing, tracks);
      this.output.tracks.save(merged);
    }, link);

    // Process whenever tracks are initially loaded;
    // unless already done so (possibly through another instance of this element)
    if (this.hasAttribute("process-when-ready")) {
      let unregistered = false;

      const unregister = this.effect(() => {
        if (unregistered) {
          unregister();
          return;
        }

        const col = output.tracks.collection();
        if (col.state !== "loaded") return;

        if (this.#performedInitialProcess.value) {
          unregistered = true;
          return;
        }

        this.#performedInitialProcess.set(true);

        const skip = /** @type {any} */ (import.meta).env
          ?.DISABLE_AUTOMATIC_TRACKS_PROCESSING ?? false;
        if (skip) return;

        unregistered = true;
        untracked(() => this.process());
      });
    }
  }

  // WORKERS

  /**
   * @override
   */
  dependencies() {
    if (!this.input) throw new Error("Input element not defined yet");
    if (!this.metadataConfigurator) {
      throw new Error("Metadata configurator element not defined yet");
    }

    return {
      input: this.input,
      metadata: this.metadataConfigurator,
    };
  }

  // ACTIONS

  async process() {
    if (!this.output) return;
    if (this.#isProcessing.value) return;

    // Start
    this.#isProcessing.set(true);
    console.log("🪵 Processing initiated");

    const cachedTracks = await data(this.output.tracks);

    const settings = await data(this.output.settings);
    const disabledUris = parseDisabledUris(settings);

    const result = await this.#proxy.process({ tracks: cachedTracks, disabledUris });

    if (result) {
      await this.output.tracks.save(mergeTracks(cachedTracks, result));
    }

    // Fin
    console.log("🪵 Processing completed");
    this.#isProcessing.set(false);
  }
}

export default ProcessTracksOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ProcessTracksOrchestrator;
export const NAME = "do-process-tracks";

defineElement(NAME, ProcessTracksOrchestrator);
```
### Queue ⭤ Audio

Connects the given queue engine to the given audio engine.

```js
import { BroadcastableDiffuseElement, defineElement, query } from "~/common/element.js";
import { untracked } from "~/common/signal.js";

/**
 * @import {InputElement} from "@specs/components/input/types.d.ts"
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 * @import RepeatShuffleEngine from "~/components/engine/repeat-shuffle/element.js"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * When the active queue item changes,
 * coordinate the audio engine accordingly.
 *
 * Vice versa, when the audio ends,
 * shift the queue if needed.
 */
class QueueAudioOrchestrator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/queue-audio";

  // LIFE CYCLE

  /**
   * @override
   */
  async connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      this.broadcast(this.identifier, {});
    }

    // Super
    super.connectedCallback();

    /** @type {import("~/components/engine/audio/element.js").CLASS} */
    this.audio = query(this, "audio-engine-selector");

    /** @type {InputElement} */
    this.input = query(this, "input-selector");

    /** @type {OutputElement} */
    this.output = query(this, "output-selector");

    /** @type {import("~/components/engine/queue/element.js").CLASS} */
    this.queue = query(this, "queue-engine-selector");

    /** @type {RepeatShuffleEngine} */
    this.repeatShuffle = query(this, "repeat-shuffle-engine-selector");

    // Wait until defined
    await customElements.whenDefined(this.audio.localName);
    await customElements.whenDefined(this.input.localName);
    await customElements.whenDefined(this.queue.localName);
    await customElements.whenDefined(this.repeatShuffle.localName);

    // Effects
    this.effect(() => this.monitorActiveQueueItem());
    this.effect(() => this.monitorAudioEnd());
  }

  // 🛠️

  async monitorActiveQueueItem() {
    const audio = this.audio;
    const input = this.input;
    const queue = this.queue;

    if (!audio) return;
    if (!input) return;
    if (!queue) return;

    const activeItem = queue.now();
    const tracksCol = this.output?.tracks.collection();
    const tracks = tracksCol?.state === "loaded" ? tracksCol.data : undefined;

    // Read synchronously so leadership changes (e.g. tab takeover) re-trigger this effect.
    const statusPromise = this.broadcasted ? this.broadcastingStatus() : undefined;

    const activeTrack = activeItem
      ? tracks?.find((t) => t.id === activeItem.id)
      : undefined;

    const status = statusPromise ? await statusPromise : undefined;
    if (status && !status.leader) return;

    const isPlaying = untracked(audio.isPlaying);

    // Resolve active URI
    const resolvedUri = activeTrack
      ? await input.resolve({ method: "GET", uri: activeTrack.uri })
      : undefined;

    // Check if we still need to render
    if (queue.now?.()?.id !== activeItem?.id) return;

    // Supply active track immediately
    // TODO: Take URL expiration timestamp into account
    // TODO: Add support for seeking streams
    //       (requires a lot of code, decoding audio frames, etc.)
    const activeAudio = activeTrack && resolvedUri
      ? [{
        id: activeTrack.id,
        isPreload: false,
        track: activeTrack,
        ...resolvedUri,
      }]
      : [];

    audio.supply({
      audio: activeAudio,
      play: activeItem && isPlaying ? { audioId: activeItem.id } : undefined,
    });
  }

  async monitorAudioEnd() {
    if (!this.audio) return;
    if (!this.queue) return;

    const now = this.queue.now();
    const aud = now ? this.audio.state(now.id) : undefined;

    if (aud?.hasEnded() && (await this.isLeader())) {
      if (this.repeatShuffle?.repeat() && now) {
        this.audio.seek({ audioId: now.id, currentTime: 0 });
        this.audio.play({ audioId: now.id });
        return;
      }

      await this.queue.shift();
    }
  }
}

export default QueueAudioOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = QueueAudioOrchestrator;
export const NAME = "do-queue-audio";

defineElement(NAME, QueueAudioOrchestrator);
```
### Scoped Tracks

Watches the given output's tracks collection and runs them through a built-in search index. 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.

```js
import {
  BroadcastableDiffuseElement,
  defineElement,
  query,
  queryOptional,
} from "~/common/element.js";
import { batch, computed, signal } from "~/common/signal.js";
import { filterByPlaylist } from "~/common/playlist.js";
import { safeDecodeURIComponent } from "~/common/utils.js";
import { listen } from "~/common/worker.js";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 * @import {InputElement} from "@specs/components/input/types.d.ts"
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 * @import {Actions, State} from "@specs/components/orchestrator/scoped-tracks/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class ScopedTracksOrchestrator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/scoped-tracks";
  static WORKER_URL = "components/orchestrator/scoped-tracks/worker.js";

  /** @type {ProxiedActions<Actions & State>} */
  #proxy;

  constructor() {
    super();
    this.#proxy = this.workerProxy();
  }

  // SIGNALS

  #input = signal(/** @type {InputElement | null} */ (null));
  #output = signal(/** @type {OutputElement | null} */ (null));

  #scope = signal(
    /** @type {import("~/components/engine/scope/element.js").CLASS | null} */ (null),
  );

  #supplyFingerprint = signal(/** @type {string | undefined} */ (undefined));

  #selectedPlaylistItems = computed(() => {
    const playlist = this.#scope.value?.playlist();
    if (!playlist) return undefined;

    const col = this.#output.value?.playlistItems.collection();
    if (!col || col.state !== "loaded") return undefined;
    return col.data.filter((p) => p.playlist === playlist);
  });

  #disabledSources = computed(() => {
    const col = this.#output.value?.settings.collection();
    if (!col || col.state !== "loaded") return [];

    const setting = col.data.find((s) =>
      s.key === "sh.diffuse.input.disabled.uris"
    );

    if (!setting) return [];

    try {
      const parsed = JSON.parse(setting.value);
      return Array.isArray(parsed) ? /** @type {string[]} */ (parsed) : [];
    } catch {
      return [];
    }
  });

  #tracksAvailable = signal(/** @type {Track[]} */ ([]));
  #tracksSearch = signal(/** @type {Track[]} */ ([]));
  #tracksFinal = signal(/** @type {Track[]} */ ([]));

  #tracksGrouped = computed(() => {
    const tracks = this.#tracksFinal.value;
    const groupBy = this.#scope.value?.groupBy();
    if (!groupBy) return undefined;
    return buildGroups(tracks, groupBy);
  });

  // STATE

  supplyFingerprint = this.#supplyFingerprint.get;
  tracks = this.#tracksFinal.get;
  groups = this.#tracksGrouped;

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        getTracksAvailable: {
          strategy: "leaderOnly",
          fn: this.#tracksAvailable.get,
        },
        getTracksSearch: {
          strategy: "leaderOnly",
          fn: this.#tracksSearch.get,
        },
        getTracksFinal: {
          strategy: "leaderOnly",
          fn: this.#tracksFinal.get,
        },
        setTracksAvailable: {
          strategy: "replicate",
          fn: this.#tracksAvailable.set,
        },
        setTracksSearch: {
          strategy: "replicate",
          fn: this.#tracksSearch.set,
        },
        setTracksFinal: {
          strategy: "replicate",
          fn: this.#tracksFinal.set,
        },
      });

      if (!actions) return;

      this.#tracksAvailable.set = actions.setTracksAvailable;
      this.#tracksSearch.set = actions.setTracksSearch;
      this.#tracksFinal.set = actions.setTracksFinal;

      // Sync signal state with leader
      Promise.all([
        actions.getTracksAvailable(),
        actions.getTracksSearch(),
        actions.getTracksFinal(),
      ]).then(([available, search, final]) =>
        batch(() => {
          this.#tracksAvailable.value = available;
          this.#tracksSearch.value = search;
          this.#tracksFinal.value = final;
        })
      );
    }

    // Super
    super.connectedCallback();

    /** @type {InputElement} */
    const input = query(this, "input-selector");

    /** @type {OutputElement} */
    const output = query(this, "output-selector");

    /** @type {import("~/components/engine/scope/element.js").CLASS | null} */
    const scope = queryOptional(this, "scope-engine-selector");

    // Assign to self
    this.#input.value = input;
    this.#output.value = output;
    if (scope) this.#scope.value = scope;

    // Sync supply fingerprint with worker
    const link = this.workerLink();
    listen("supplyFingerprint", this.#supplyFingerprint.set, link);
    this.#proxy.supplyFingerprint().then(this.#supplyFingerprint.set);

    // When defined
    await customElements.whenDefined(input.localName);
    await customElements.whenDefined(output.localName);
    if (scope) await customElements.whenDefined(scope.localName);

    // Watch tracks collection
    this.effect(async () => {
      const tracksCol = output.tracks.collection();

      if ((await this.isLeader()) === false) return;
      if (tracksCol.state !== "loaded") return;

      /** @type {string[]} */
      const uris = [];
      const tracks = tracksCol.data.filter((t) => {
        uris.push(t.uri);
        return t.kind !== "placeholder";
      });

      // Consult inputs
      const groups = tracksCol.data.length
        ? await input.groupConsult(uris)
        : {};

      /** @type {Set<string>} */
      const availableUris = new Set();

      Object.values(groups).forEach((value) => {
        if (value.available === false) return;
        for (const uri of value.uris) {
          availableUris.add(uri);
        }
      });

      const availableTracks = tracks.filter((t) => {
        return availableUris.has(t.uri) && !!t.tags;
      });

      // Set pool
      this.#proxy.supply({ tracks: availableTracks });
      this.#tracksAvailable.set(availableTracks);
    });

    // Watch search supply
    this.effect(async () => {
      const _trigger = this.#supplyFingerprint.value;
      const availableTracks = this.#tracksAvailable.value;
      const searchTerm = this.#scope.value?.searchTerm();

      if ((await this.isLeader()) === false) return;

      if (searchTerm?.length) {
        const searchResults = await this.#proxy.search({
          term: searchTerm,
        });
        this.#tracksSearch.set(searchResults);
      } else {
        this.#tracksSearch.set(availableTracks);
      }
    });

    // Watch `#tracksSearch` + Playlist + Sort
    let x = 0;

    this.effect(async () => {
      const tracks = this.#tracksSearch.value;
      const playlistItems = this.#selectedPlaylistItems();
      const disabledSources = this.#disabledSources();
      const sortBy = this.#scope.value?.sortBy();
      const sortDirection = this.#scope.value?.sortDirection();
      const groupBy = this.#scope.value?.groupBy();

      const y = ++x;

      if ((await this.isLeader()) === false) return;
      if (y !== x) return;

      let final = playlistItems?.length
        ? filterByPlaylist(tracks, playlistItems)
        : tracks;

      if (disabledSources.length) {
        final = final.filter((t) =>
          !disabledSources.some((source) => t.uri.startsWith(source))
        );
      }

      // When groupBy is active, sort by group key first using the group's
      // canonical direction (from GROUP_BY_SORT_OVERRIDES, or user's direction
      // for firstLetter). Within each group, sort by the user's sortBy and
      // sortDirection as normal.
      //
      // Schwartzian transform: precompute all keys once (O(N)) so the
      // comparator never re-parses URLs or re-splits dot-paths (O(N log N)).
      const groupOverride = groupBy
        ? GROUP_BY_SORT_OVERRIDES[groupBy]
        : undefined;
      const groupDir =
        (groupOverride?.sortDirection ?? sortDirection) === "desc" ? -1 : 1;
      const userFields = sortBy ?? [];
      const userDir = sortDirection === "desc" ? -1 : 1;
      const splitPaths = userFields.map((f) => f.split("."));

      if (groupBy || userFields.length) {
        const decorated = final.map((track) => ({
          track,
          groupKey: groupBy ? groupKeyLabel(track, groupBy).key : "",
          fieldVals: splitPaths.map((parts) => {
            let v = /** @type {any} */ (track);
            for (const p of parts) v = v?.[p];
            return v;
          }),
        }));

        decorated.sort((a, b) => {
          if (groupBy && a.groupKey !== b.groupKey) {
            if (!a.groupKey) return 1;
            if (!b.groupKey) return -1;
            return collator.compare(a.groupKey, b.groupKey) * groupDir;
          }
          for (let i = 0; i < a.fieldVals.length; i++) {
            const av = a.fieldVals[i];
            const bv = b.fieldVals[i];
            // Null/undefined always sorts last regardless of direction
            if (av == null && bv == null) continue;
            if (av == null) return 1;
            if (bv == null) return -1;
            const cmp = compareValues(av, bv);
            if (cmp !== 0) return cmp * userDir;
          }
          return 0;
        });

        final = decorated.map((d) => d.track);
      }

      this.#tracksFinal.set(final);
    });
  }
}

export default ScopedTracksOrchestrator;

////////////////////////////////////////////
// HELPERS
////////////////////////////////////////////

const collator = new Intl.Collator();

const MONTHS = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];

/** @type {Record<string, { sortDirection: "asc" | "desc" }>} */
const GROUP_BY_SORT_OVERRIDES = {
  createdAt: { sortDirection: "desc" },
  directory: { sortDirection: "asc" },
  firstLetter: { sortDirection: "asc" },
  "tags.year": { sortDirection: "desc" },
};

/**
 * @param {Track[]} tracks
 * @param {string} groupBy  dot-path field, e.g. "createdAt" or "tags.artist"
 * @returns {{ label: string; tracks: Track[] }[]}
 */
function buildGroups(tracks, groupBy) {
  /** @type {{ label: string; tracks: Track[] }[]} */
  const groups = [];
  let lastKey = /** @type {string | undefined} */ (undefined);
  let current =
    /** @type {{ label: string; tracks: Track[] } | undefined} */ (undefined);

  for (const track of tracks) {
    const { key, label } = groupKeyLabel(track, groupBy);

    if (key !== lastKey) {
      current = { label, tracks: [] };
      groups.push(current);
      lastKey = key;
    }

    current?.tracks.push(track);
  }

  return groups;
}

/**
 * @param {Track} track
 * @param {string} fieldPath
 * @returns {{ key: string; label: string }}
 */
function groupKeyLabel(track, fieldPath) {
  if (fieldPath === "createdAt") {
    const iso = track.createdAt;
    if (!iso) return { key: "", label: "Unknown" };
    const year = iso.slice(0, 4);
    const month = iso.slice(5, 7);
    return {
      key: `${year}-${month}`,
      label: `${MONTHS[parseInt(month, 10) - 1]} ${year}`,
    };
  }

  if (fieldPath === "directory") {
    const uri = track.uri ?? "";
    let path = uri;
    try {
      path = new URL(uri).pathname;
    } catch {
      // not a valid URL, use as-is
    }
    const slash = path.lastIndexOf("/");
    const dir = slash > 0 ? path.slice(0, slash) : path;
    const key = uri.slice(0, uri.lastIndexOf("/"));
    return { key, label: safeDecodeURIComponent(dir) || "Unknown" };
  }

  if (fieldPath.startsWith("firstLetter:")) {
    const dotPath = fieldPath.slice("firstLetter:".length);
    let val = /** @type {any} */ (track);
    for (const key of dotPath.split(".")) val = val?.[key];
    const str = val != null ? String(val) : "";
    const letter = str.charAt(0).toUpperCase();
    const key = /[A-Z]/.test(letter) ? letter : "#";
    return { key, label: key };
  }

  // Generic dot-path extraction
  let val = /** @type {any} */ (track);
  for (const key of fieldPath.split(".")) val = val?.[key];
  const str = val != null ? String(val) : "";
  return { key: str, label: str || "Unknown" };
}

/**
 * @param {any} aVal
 * @param {any} bVal
 * @returns {number}
 */
function compareValues(aVal, bVal) {
  if (aVal == null && bVal == null) return 0;
  if (aVal == null) return 1;
  if (bVal == null) return -1;
  return typeof aVal === "string" && typeof bVal === "string"
    ? aVal.localeCompare(bVal)
    : aVal < bVal
    ? -1
    : aVal > bVal
    ? 1
    : 0;
}

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ScopedTracksOrchestrator;
export const NAME = "do-scoped-tracks";

defineElement(NAME, CLASS);
```
### Scrobble ⭤ Audio

Connects the audio engine with a scrobbler element. Calls nowPlaying when a track starts playing and scrobble once the user has listened long enough.

```js
import { BroadcastableDiffuseElement, defineElement, query } from "~/common/element.js";

/**
 * @import {ScrobbleElement} from "@specs/components/supplement/types.d.ts"
 * @import {Track} from "~/definitions/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * Connects the audio engine with the scrobble configurator.
 *
 * Calls `nowPlaying` when a track starts and `scrobble` once the user
 * has listened long enough per the last.fm rules:
 *   - Track must be at least 30 seconds long.
 *   - User must have listened to at least min(duration / 2, 4 minutes).
 */
class ScrobbleAudioOrchestrator extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/scrobble-audio";

  // LIFECYCLE

  /** @override */
  async connectedCallback() {
    if (this.hasAttribute("group")) {
      this.broadcast(this.identifier, {});
    }

    super.connectedCallback();

    /** @type {import("~/components/engine/audio/element.js").CLASS} */
    this.audio = query(this, "audio-engine-selector");

    /** @type {ScrobbleElement} */
    this.scrobble = query(this, "scrobble-selector");

    await customElements.whenDefined(this.audio.localName);
    await customElements.whenDefined(this.scrobble.localName);

    this.effect(() => this.#monitorAudio());
  }

  /** @override */
  disconnectedCallback() {
    super.disconnectedCallback();
    this.#stopTimer();
  }

  // TRACK STATE
  // Resets whenever the active (non-preload) audio item changes.

  /** @type {string | null} */
  #trackId = null;

  /** @type {Track | null} */
  #activeTrack = null;

  /** @type {number | null} Date.now() when track first started, used as the scrobble timestamp. */
  #startedAt = null;

  /** Whether `nowPlaying` has been called for the current track. */
  #nowPlayingSent = false;

  /** Whether `scrobble` has been called for the current track. */
  #scrobbled = false;

  /** Whether the current track has ended (used to detect restarts, e.g. repeat). */
  #hadEnded = false;

  // TIMER STATE
  // Accumulates actual listening time (pauses don't count).

  /** Accumulated listening time in ms before the last pause. */
  #listenedMs = 0;

  /** Date.now() when the timer was last resumed; null when paused. */
  #timerResumedAt = /** @type {number | null} */ (null);

  /** @type {ReturnType<typeof setInterval> | null} */
  #intervalId = null;

  // EFFECT

  /**
   * Reacts to audio item changes and playback state.
   * Detects track changes, resets state, and starts/stops the listening timer.
   */
  #monitorAudio() {
    if (!this.audio) return;

    const active = this.audio.items().find((item) => !item.isPreload);
    const id = active?.id ?? null;

    // Detect track change
    if (id !== this.#trackId) {
      this.#stopTimer();

      this.#trackId = id;
      this.#activeTrack = active?.track ?? null;
      this.#startedAt = id ? Date.now() : null;
      this.#nowPlayingSent = false;
      this.#scrobbled = false;
      this.#listenedMs = 0;
      this.#hadEnded = false;
    }

    if (!id) return;

    const state = this.audio.state(id);
    const loadingState = state?.loadingState() ?? "loading";
    const hasError = typeof loadingState === "object" && "error" in loadingState;
    const isPlaying = !hasError && (state?.isPlaying() ?? false);
    const hasEnded = state?.hasEnded() ?? false;

    // Detect same-track restart (e.g. repeat): the track ended and now plays again.
    if (this.#hadEnded && !hasEnded && isPlaying) {
      this.#stopTimer();
      this.#startedAt = Date.now();
      this.#nowPlayingSent = false;
      this.#scrobbled = false;
      this.#listenedMs = 0;
      this.#hadEnded = false;
    }

    if (hasEnded) this.#hadEnded = true;

    if (isPlaying) {
      this.#startTimer();

      if (!this.#nowPlayingSent) {
        this.#nowPlayingSent = true;
        this.#sendNowPlaying(id);
      }
    } else {
      this.#stopTimer();
    }
  }

  // TIMER

  #startTimer() {
    if (this.#timerResumedAt !== null) return;

    this.#timerResumedAt = Date.now();
    this.#intervalId = setInterval(() => this.#checkScrobble(), 1_000);
  }

  #stopTimer() {
    if (this.#timerResumedAt !== null) {
      this.#listenedMs += Date.now() - this.#timerResumedAt;
      this.#timerResumedAt = null;
    }

    if (this.#intervalId !== null) {
      clearInterval(this.#intervalId);
      this.#intervalId = null;
    }
  }

  #totalListenedMs() {
    return this.#listenedMs +
      (this.#timerResumedAt !== null ? Date.now() - this.#timerResumedAt : 0);
  }

  // SCROBBLING

  /**
   * @param {string} id
   */
  async #sendNowPlaying(id) {
    if (!(await this.isLeader())) return;
    if (this.#trackId !== id || !this.#activeTrack) return;

    try {
      await this.scrobble?.nowPlaying(this.#activeTrack);
    } catch (err) {
      console.warn("scrobble: nowPlaying failed", err);
    }
  }

  async #checkScrobble() {
    if (this.#scrobbled) return;

    const id = this.#trackId;
    if (!id || !this.#startedAt || !this.#activeTrack) return;

    const durationSec = this.audio?.state(id)?.duration() ?? 0;

    // Track must be at least 30 seconds
    if (durationSec < 30) return;

    // Must have listened to at least half the track or 4 minutes
    const listenedSec = this.#totalListenedMs() / 1000;
    if (listenedSec < Math.min(durationSec / 2, 240)) return;

    this.#scrobbled = true;

    if (!(await this.isLeader())) return;
    if (this.#trackId !== id) return;

    const track = this.#activeTrack;
    const startedAt = this.#startedAt;

    try {
      await this.scrobble?.scrobble(track, startedAt, { duration: Math.round(durationSec * 1000) });
    } catch (err) {
      console.warn("Scrobble failed", err);
      this.#scrobbled = false;
    }
  }
}

export default ScrobbleAudioOrchestrator;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ScrobbleAudioOrchestrator;
export const NAME = "do-scrobble-audio";

defineElement(NAME, ScrobbleAudioOrchestrator);
```
### Sources

Monitor tracks from the given output to form a list of sources based on the input's sources return value.

```js
import deepDiff from "@fry69/deep-diff";

import * as Output from "~/common/output.js";
import { BroadcastableDiffuseElement, defineElement, query } from "~/common/element.js";
import { groupTracksPerScheme } from "~/common/utils.js";
import { signal } from "~/common/signal.js";

import { DISABLED_KEY } from "./constants.js";
import { parseDisabledUris, uriKey } from "./common.js";

/**
 * @import {InputElement, Source} from "@specs/components/input/types.d.ts"
 * @import {OutputElement} from "@specs/components/output/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

class Sources extends BroadcastableDiffuseElement {
  static NAME = "diffuse/orchestrator/sources";

  // SIGNALS

  #sources = signal(/** @type {{ [scheme: string]: Source[] }} */ ({}));
  #disabled = signal(/** @type {string[]} */ ([]));

  // STATE

  sources = this.#sources.get;
  disabled = this.#disabled.get;

  #output = signal(/** @type {OutputElement | null} */ (null));

  // METHODS

  /**
   * Returns whether the given source URI is disabled.
   * Strips query params before comparing, matching how {@link toggle} stores keys.
   *
   * @param {string} uri
   * @returns {boolean}
   */
  isDisabled(uri) {
    return this.#disabled.get().includes(uriKey(uri));
  }

  /**
   * @param {string} uri
   */
  async toggle(uri) {
    const key = uriKey(uri);

    const output = this.#output.value;
    if (!output) {
      console.warn("Output element is not available yet.");
      return;
    }

    const settings = await Output.data(output.settings);
    const existing = settings.find((s) => s.key === DISABLED_KEY);

    let disabled = parseDisabledUris(settings);

    if (disabled.includes(key)) {
      disabled = disabled.filter((u) => u !== key);
    } else {
      disabled = [...disabled, key];
    }

    const value = JSON.stringify(disabled);
    const updated = existing
      ? settings.map((s) =>
        s.key === DISABLED_KEY ? { ...s, value } : s
      )
      : [
        ...settings,
        {
          $type: /** @type {"sh.diffuse.output.setting"} */ (
            "sh.diffuse.output.setting"
          ),
          id: crypto.randomUUID(),
          key: DISABLED_KEY,
          value,
        },
      ];

    await output.settings.save(updated);
  }

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    super.connectedCallback();

    /** @type {InputElement} */
    const input = query(this, "input-selector");

    /** @type {OutputElement} */
    const output = query(this, "output-selector");

    // Wait until defined
    await customElements.whenDefined(input.localName);
    await customElements.whenDefined(output.localName);

    // Signals
    this.#output.value = output;

    // Effects
    this.effect(() => {
      const col = output.settings.collection();
      if (col.state !== "loaded") { this.#disabled.value = []; return; }
      this.#disabled.value = parseDisabledUris(col.data);
    });

    // Single input mode + dependencies
    const singleInputMode = !!input.SCHEME;
    const deps =
      /** @type {{ [k: string]: InputElement }} */ (singleInputMode
        ? {}
        : input.dependencies());

    // Effects
    this.effect(() => {
      const col = output.tracks.collection();
      const tracks = col.state === "loaded" ? col.data : [];
      const groups = groupTracksPerScheme(tracks);

      /** @type {{ [scheme: string]: Source[] }} */
      const record = {};

      Object.entries(groups).map(([scheme, tracks]) => {
        /** @type {Source[]} */
        let sources;

        if (singleInputMode) {
          if (input.SCHEME === scheme) {
            sources = input.sources(tracks);
          } else {
            sources = [];
          }
        } else {
          const dep = deps[scheme];
          if (!dep) sources = tracks.map((t) => ({ label: t.uri, uri: t.uri }));
          else sources = dep.sources(tracks);
        }

        record[scheme] = sources;
      });

      if (deepDiff(this.#sources.value, record)) {
        this.#sources.value = record;
      }
    });
  }
}

export default Sources;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = Sources;
export const NAME = "do-sources";

defineElement(NAME, CLASS);
```
## Output
### Polymorphic / IndexedDB

Stores output into the local indexedDB. Supports any type of data that indexedDB supports.

```js
import * as IDB from "idb-keyval";

import { IDB_PREFIX } from "./constants.js";
import { BroadcastedOutputElement, outputManager } from "../../common.js";
import { defineElement } from "~/common/element.js";

/**
 * @import {OutputElement, OutputManager, OutputWorkerActions} from "@specs/components/output/types.d.ts"
 * @import {SupportedDataTypes} from "@specs/components/output/polymorphic/indexed-db/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {OutputElement<SupportedDataTypes>}
 */
class IndexedDBOutput extends BroadcastedOutputElement {
  static NAME = "diffuse/output/polymorphic/indexed-db";
  static WORKER_URL = "components/output/polymorphic/indexed-db/worker.js";

  #manager;

  constructor() {
    super();

    /** @type {OutputManager<SupportedDataTypes>} */
    this.#manager = outputManager({
      facets: {
        empty: () => undefined,
        get: () => this.#get("facets"),
        put: (data) => this.#put("facets", data),
      },
      init: () => this.whenConnected(),
      playlistItems: {
        empty: () => undefined,
        get: () => this.#get("playlistItems"),
        put: (data) => this.#put("playlistItems", data),
      },
      settings: {
        empty: () => undefined,
        get: () => this.#get("settings"),
        put: (data) => this.#put("settings", data),
      },
      tracks: {
        empty: () => undefined,
        get: () => this.#get("tracks"),
        put: (data) => this.#put("tracks", data),
      },
    });

    this.facets = this.#manager.facets;
    this.playlistItems = this.#manager.playlistItems;
    this.settings = this.#manager.settings;
    this.tracks = this.#manager.tracks;

    this.ready = () => true;
  }

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    this.replicateSavedData(this.#manager);
    super.connectedCallback();
  }

  // GET & PUT

  /** @param {string} name */
  #get = (name) => IDB.get(`${IDB_PREFIX}/${this.#cat(name)}`);

  /** @param {string} name; @param {any} data */
  #put = (name, data) => IDB.set(`${IDB_PREFIX}/${this.#cat(name)}`, data);

  // 🛠️

  /** @param {string} name */
  #cat(name) {
    return `${this.namespace?.length ? this.namespace + "/" : ""}${name}`;
  }
}

export default IndexedDBOutput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = IndexedDBOutput;
export const NAME = "dop-indexed-db";

defineElement(NAME, IndexedDBOutput);
```
### Bytes / S3

Store output data on AWS S3 or compatible services such as Cloudflare R2.

```js
import * as IDB from "idb-keyval";

import { computed, signal } from "~/common/signal.js";
import { BroadcastedOutputElement, outputManager } from "../../common.js";
import { defineElement } from "~/common/element.js";

const STORAGE_PREFIX = "diffuse/output/bytes/s3";

/**
 * @import {ProxiedActions} from "~/common/worker.d.ts"
 * @import {OutputElement, OutputManager} from "@specs/components/output/types.d.ts"
 * @import {Bucket} from "@specs/components/input/s3/types.d.ts"
 * @import {S3OutputElement, S3OutputWorkerActions} from "@specs/components/output/bytes/s3/types.d.ts"
 */

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {OutputElement<Uint8Array | undefined>}
 * @implements {S3OutputElement}
 */
class S3Output extends BroadcastedOutputElement {
  static NAME = "diffuse/output/bytes/s3";
  static WORKER_URL = "components/output/bytes/s3/worker.js";

  #manager;

  constructor() {
    super();

    /** @type {ProxiedActions<S3OutputWorkerActions>} */
    this.proxy = this.workerProxy();

    /** @type {OutputManager<Uint8Array | undefined>} */
    this.#manager = outputManager({
      facets: {
        empty: () => undefined,
        get: () => this.#get("facets"),
        put: (data) => this.#put("facets", data),
      },
      init: () => this.whenConnected(),
      playlistItems: {
        empty: () => undefined,
        get: () => this.#get("playlistItems"),
        put: (data) => this.#put("playlistItems", data),
      },
      settings: {
        empty: () => undefined,
        get: () => this.#get("settings"),
        put: (data) => this.#put("settings", data),
      },
      tracks: {
        empty: () => undefined,
        get: () => this.#get("tracks"),
        put: (data) => this.#put("tracks", data),
      },
    });

    this.facets = this.#manager.facets;
    this.playlistItems = this.#manager.playlistItems;
    this.settings = this.#manager.settings;
    this.tracks = this.#manager.tracks;
  }

  // SIGNALS

  #isOnline = signal(navigator.onLine);

  // STATE

  ready = computed(() => {
    return this.#bucket.value !== undefined && this.#isOnline.value;
  });

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    this.replicateSavedData(this.#manager);

    super.connectedCallback();

    /** @type {Bucket | undefined} */
    const stored = await IDB.get(`${STORAGE_PREFIX}/bucket`);
    if (stored) this.#bucket.value = stored;

    globalThis.addEventListener("online", this.#online);
    globalThis.addEventListener("offline", this.#offline);
  }

  /** @override */
  disconnectedCallback() {
    globalThis.removeEventListener("online", this.#online);
    globalThis.removeEventListener("offline", this.#offline);
  }

  #offline = () => this.#isOnline.set(false);
  #online = () => this.#isOnline.set(true);

  // BUCKET

  #bucket = signal(/** @type {Bucket | undefined} */ (undefined));

  bucket = this.#bucket.get;

  /** @returns {Promise<Bucket | undefined>} */
  async getBucket() {
    if (!this.#bucket.value) {
      /** @type {Bucket | undefined} */
      const stored = await IDB.get(`${STORAGE_PREFIX}/bucket`);
      if (stored) this.#bucket.value = stored;
      return stored;
    }

    return this.#bucket.value;
  }

  /**
   * @param {Bucket} bucket
   */
  async setBucket(bucket) {
    this.#bucket.value = bucket;
    await IDB.set(`${STORAGE_PREFIX}/bucket`, bucket);
  }

  async unsetBucket() {
    this.#bucket.value = undefined;
    await IDB.del(`${STORAGE_PREFIX}/bucket`);
  }

  // GET & PUT

  /** @param {string} name */
  #get = async (name) => {
    const bucket = await this.getBucket();
    if (!bucket) return undefined;
    return this.proxy.get({ bucket, name: this.#cat(name) });
  };

  /** @param {string} name; @param {any} data */
  #put = async (name, data) => {
    const bucket = await this.getBucket();
    if (!bucket) return undefined;
    return this.proxy.put({ bucket, data, name: this.#cat(name) });
  };

  // 🛠️

  /** @param {string} name */
  #cat(name) {
    const ns = this.namespace;
    return `${ns?.length ? ns + "/" : ""}${name}`;
  }
}

export default S3Output;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = S3Output;
export const NAME = "dob-s3";

defineElement(NAME, S3Output);
```
### Raw / AT Protocol

Store your user data on the storage associated with your ATProtocol identity. Data is lexicon shaped by default so this element takes in that data directly without any transformations.

```js
import { Client, ClientResponseError, ok } from "@atcute/client";
import { ComAtprotoSyncSubscribeRepos } from "@atcute/atproto";
import { decode, encode } from "@atcute/cbor";
import { xxh32r } from "xxh32/dist/raw.js";

import { computed, signal } from "~/common/signal.js";
import { BroadcastedOutputElement, outputManager } from "../../common.js";
import { defineElement } from "~/common/element.js";

import {
  clearStoredSession,
  login,
  logout,
  OAuthUserAgent,
  restoreOrFinalize,
  TokenRefreshError,
} from "./oauth.js";

/**
 * @import {OutputManager} from "@specs/components/output/types.d.ts"
 * @import {ATProtoOutputElement} from "@specs/components/output/raw/atproto/types.d.ts"
 * @import {MessageOf} from "@atcute/firehose"
 * @typedef {import("@atcute/atproto").ComAtprotoRepoApplyWrites.$input['writes'][number]} WriteOp
 */

/** @type {Set<string>} */
const WATCHED_COLLECTIONS = new Set([
  "sh.diffuse.output.facet",
  "sh.diffuse.output.playlistItemBundle",
  "sh.diffuse.output.setting",
  "sh.diffuse.output.trackBundle",
]);

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ATProtoOutputElement}
 */
class ATProtoOutput extends BroadcastedOutputElement {
  static NAME = "diffuse/output/raw/atproto";

  #manager;

  /** @type {PromiseWithResolvers<void>} */
  #authenticated = Promise.withResolvers();

  /** @type {PromiseWithResolvers<void>} */
  #restoreSettled = Promise.withResolvers();

  /** @type {Client | null} */
  #rpc = null;

  /** @type {OAuthUserAgent | null} */
  #agent = null;

  /** @type {string | null} */
  #pdsUrl = null;

  constructor() {
    super();

    /** @type {OutputManager} */
    this.#manager = outputManager({
      init: async () => {
        await this.#restoreSettled.promise;
        return true;
      },
      facets: this.#recordCollection("sh.diffuse.output.facet"),
      playlistItems: this.#blobCollection(
        "sh.diffuse.output.playlistItemBundle",
        { groupBy: "playlist" },
      ),
      settings: this.#recordCollection("sh.diffuse.output.setting"),
      tracks: this.#blobCollection("sh.diffuse.output.trackBundle", {
        groupBy: "scheme",
        keyOf: (item) => {
          const uri = String(
            /** @type {Record<string, unknown>} */ (item)["uri"] ?? "",
          );
          const colon = uri.indexOf(":");
          return colon > 0 ? uri.substring(0, colon) : undefined;
        },
      }),
    });

    this.facets = this.#manager.facets;
    this.playlistItems = this.#manager.playlistItems;
    this.settings = this.#manager.settings;
    this.tracks = this.#manager.tracks;
  }

  // SIGNALS

  #did = signal(/** @type {`did:${string}:${string}` | null} */ (null));
  #handle = signal(/** @type {string | null} */ (null));
  #isOnline = signal(navigator.onLine);
  #rev = signal(/** @type {string | null} */ (null));
  #firehoseRev = signal(/** @type {{ rev: string, collections: ReadonlySet<string> } | null} */ (null));

  /** @type {AsyncIterator<unknown> | null} */
  #firehoseIterator = null;
  #firehoseGen = 0;
  #writing = 0;

  /** @type {Array<{ fn: () => Promise<void>, resolve: () => void, reject: (err: unknown) => void }>} */
  #writeQueue = [];
  #writeDraining = false;
  /** @type {Map<string, { cancelled: boolean }>} */
  #writeCancels = new Map();

  did = this.#did.get;
  handle = this.#handle.get;
  rev = this.#rev.get;
  firehoseRev = this.#firehoseRev.get;

  ready = computed(() => {
    return this.#did.value !== null && !!this.#rpc && this.#isOnline.value;
  });

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    this.replicateSavedData(this.#manager);

    super.connectedCallback();

    this.#tryRestore();

    globalThis.addEventListener("online", this.#online);
    globalThis.addEventListener("offline", this.#offline);
  }

  /** @override */
  disconnectedCallback() {
    this.#stopFirehose();
    globalThis.removeEventListener("online", this.#online);
    globalThis.removeEventListener("offline", this.#offline);
  }

  #offline = () => this.#isOnline.set(false);
  #online = () => this.#isOnline.set(true);

  // AUTH

  /**
   * Initiate the OAuth flow.
   * Navigates the browser to the authorization server.
   *
   * @param {string} handle
   */
  async login(handle) {
    await login(handle);
  }

  /**
   * @returns {Promise<void>}
   */
  whenRestored() {
    return this.#restoreSettled.promise;
  }

  /**
   * Sign out and revoke the current session.
   */
  async logout() {
    if (this.#agent) {
      this.#stopFirehose();
      await logout(this.#agent);
      this.#agent = null;
      this.#authenticated = Promise.withResolvers();
      this.#did.value = null;
      this.#handle.value = null;
      this.#pdsUrl = null;
      this.#rpc = null;
    }
  }

  /**
   * Clear session state without contacting the server.
   * Used when the session has already been revoked.
   */
  #clearSession() {
    this.#stopFirehose();
    this.#agent = null;
    this.#authenticated = Promise.withResolvers();
    this.#did.value = null;
    this.#handle.value = null;
    this.#pdsUrl = null;
    this.#rpc = null;

    clearStoredSession();
  }

  /**
   * @param {unknown} err
   * @returns {boolean}
   */
  #isSessionError(err) {
    if (err instanceof TokenRefreshError) return true;
    // OAuthUserAgent.handle() swallows TokenRefreshError and returns the
    // original 401 response, which ok() wraps as a ClientResponseError.
    if (err instanceof ClientResponseError && err.status === 401) return true;
    if (err && typeof err === "object" && "cause" in err) {
      return this.#isSessionError(
        (/** @type {{ cause: unknown }} */ (err)).cause,
      );
    }
    return false;
  }

  async #tryRestore() {
    await this.whenConnected();

    try {
      const session = await restoreOrFinalize();

      if (session) {
        this.#setSession(session);
      }
    } catch (err) {
      if (this.#isSessionError(err)) {
        this.#clearSession();
      } else {
        throw err;
      }
    } finally {
      this.#restoreSettled.resolve();
    }
  }

  /**
   * @param {import("@atcute/oauth-browser-client").Session} session
   */
  #setSession(session) {
    const agent = new OAuthUserAgent(session);

    // Intercept token refresh to detect session revocation proactively.
    // OAuthUserAgent.handle() swallows TokenRefreshError silently,
    // so we hook into getSession to clear state as soon as refresh fails.
    const originalGetSession = agent.getSession.bind(agent);
    agent.getSession = /** @type {typeof originalGetSession} */ ((...args) => {
      const promise = originalGetSession(...args);

      promise.catch((err) => {
        if (err instanceof TokenRefreshError) {
          this.#clearSession();
        }
      });

      return promise;
    });

    this.#agent = agent;
    this.#rpc = new Client({ handler: agent });
    this.#did.value = session.info.sub;
    this.#pdsUrl = session.info.aud;
    this.#authenticated.resolve();
    this.#fetchHandle(session.info.sub);
    this.#startFirehose();
  }

  // FIREHOSE

  #stopFirehose() {
    const iter = this.#firehoseIterator;
    this.#firehoseIterator = null;
    iter?.return?.();
  }

  async #startFirehose() {
    this.#stopFirehose();

    const gen = ++this.#firehoseGen;
    const pdsUrl = this.#pdsUrl;
    if (!pdsUrl) return;

    const wssUrl = pdsUrl.replace(
      /^https?:\/\//,
      (m) => m === "https://" ? "wss://" : "ws://",
    );

    const { FirehoseSubscription } = await import("@atcute/firehose");

    if (this.#firehoseGen !== gen) return;

    const subscription = new FirehoseSubscription({
      service: wssUrl,
      nsid: ComAtprotoSyncSubscribeRepos.mainSchema,
      validateMessages: false,
    });

    const iter = subscription[Symbol.asyncIterator]();
    this.#firehoseIterator = iter;

    try {
      for await (const message of iter) {
        this.#handleFirehoseCommit(message);
      }
    } catch {
      // Non-fatal; partysocket handles reconnection automatically
    }
  }

  /**
   * @param {MessageOf<typeof ComAtprotoSyncSubscribeRepos.mainSchema>} message
   */
  #handleFirehoseCommit(message) {
    if (message.$type !== "com.atproto.sync.subscribeRepos#commit") return;

    const commit =
      /** @type {{ repo: string, rev: string, ops?: Array<{ path: string }> }} */ (message);

    if (commit.repo !== this.#did.value) return;
    if (commit.rev === this.#rev.value) return;
    if (this.#writing > 0) return;

    const touched = new Set(
      (commit.ops ?? [])
        .map((op) => op.path?.split("/")[0])
        .filter((c) => WATCHED_COLLECTIONS.has(c)),
    );

    if (touched.size === 0) return;

    this.#firehoseRev.value = { rev: commit.rev, collections: touched };
  }

  /**
   * @param {string} did
   */
  async #fetchHandle(did) {
    const rpc = this.#rpc;
    if (!rpc) return;
    try {
      const result = await ok(rpc.get("com.atproto.repo.describeRepo", {
        params: {
          repo: /** @type {import("@atcute/lexicons").ActorIdentifier} */ (did),
        },
      }));
      if (this.#did.value === did) {
        this.#handle.value = result?.handle ?? null;
      }
    } catch {
      // Non-fatal; handle stays null
    }
  }

  // RECORDS

  /**
   * Returns `{ empty, get, put }` for a small record collection (facets, settings).
   *
   * Tracks last-known remote state in closure so `put()` skips the `listRecords`
   * round-trip on every write. Uses `putRecord` (upsert) to avoid create/update
   * ambiguity; batches deletes via `applyWrites`.
   *
   * @param {string} nsid
   */
  #recordCollection(nsid) {
    /** @type {Map<string, Record<string, unknown>> | null} */
    let lastKnown = null;

    return {
      empty: () => [],
      get: async () => {
        const records = await this.listRecords(nsid);
        lastKnown = new Map(
          /** @type {Array<Record<string, unknown>>} */ (records).map((r) => [
            String(r["id"]),
            r,
          ]),
        );
        return records;
      },
      put: async (/** @type {unknown[]} */ data) => {
        const nsidTyped = /** @type {`${string}.${string}.${string}`} */ (nsid);

        /** @type {Map<string, Record<string, unknown>>} */
        const desired = new Map(
          /** @type {Array<{ id: string }>} */ (data).map((r) => [
            r.id,
            /** @type {Record<string, unknown>} */ ({ $type: nsidTyped, ...r }),
          ]),
        );

        const known = lastKnown ?? new Map();

        /** @type {Array<[string, Record<string, unknown>]>} */
        const upserts = [];
        for (const [id, record] of desired) {
          const existing = known.get(id);
          if (existing && JSON.stringify(existing) === JSON.stringify(record)) {
            continue;
          }
          upserts.push([id, record]);
        }

        /** @type {WriteOp[]} */
        const deletes = [];
        for (const id of known.keys()) {
          if (!desired.has(id)) {
            deletes.push({
              $type: "com.atproto.repo.applyWrites#delete",
              collection: nsidTyped,
              rkey: id,
            });
          }
        }

        if (upserts.length === 0 && deletes.length === 0) return;

        const newKnown = new Map(known);
        for (const [id, record] of upserts) newKnown.set(id, record);
        for (const { rkey } of deletes) newKnown.delete(rkey);

        const prior = this.#writeCancels.get(nsid);
        if (prior) prior.cancelled = true;
        const token = { cancelled: false };
        this.#writeCancels.set(nsid, token);

        await this.#enqueueWrite(async () => {
          if (token.cancelled) return;
          const rpc = this.#rpc;
          const did = this.#did.value;
          if (!rpc || !did) return;
          this.#writing++;
          try {
            for (const [rkey, record] of upserts) {
              const result = await ok(rpc.post("com.atproto.repo.putRecord", {
                input: { repo: did, collection: nsidTyped, rkey, record },
              }));
              if (result?.commit?.rev) this.#rev.value = result.commit.rev;
            }
            for (let i = 0; i < deletes.length; i += 100) {
              const result = await ok(rpc.post("com.atproto.repo.applyWrites", {
                input: { repo: did, writes: deletes.slice(i, i + 100) },
              }));
              if (result?.commit?.rev) this.#rev.value = result.commit.rev;
            }
            lastKnown = newKnown;
          } catch (err) {
            if (this.#isSessionError(err)) { this.#clearSession(); return; }
            throw err;
          } finally {
            this.#writing--;
            if (this.#writeCancels.get(nsid) === token) {
              this.#writeCancels.delete(nsid);
            }
          }
        });
      },
    };
  }

  /**
   * Returns `{ empty, get, put }` for a collection stored as CBOR blobs.
   * Each distinct group key gets its own bundle record.
   *
   * `groupBy` is the field name stored on the bundle record (used by `get()`
   * to reconstruct the key). `keyOf` extracts the group key from each item;
   * defaults to `item[groupBy]` when omitted.
   *
   * @param {string} nsid
   * @param {{ groupBy: string, keyOf?: (item: unknown) => string | undefined }} options
   */
  #blobCollection(nsid, { groupBy, keyOf } = /** @type {any} */ ({})) {
    /** @type {Map<string, string>} groupKey → content hash */
    let lastHashes = new Map();
    /** @type {Map<string, unknown>} groupKey → blob ref */
    let lastBlobs = new Map();

    return {
        empty: () => /** @type {unknown[]} */ ([]),
        get: async () => {
          const bundles = await this.listRecords(nsid);
          /** @type {unknown[]} */
          const items = [];
          /** @type {Map<string, string>} */
          const newHashes = new Map();
          /** @type {Map<string, unknown>} */
          const newBlobs = new Map();

          for (const bundle of bundles) {
            if (!bundle.data?.ref?.$link) continue;

            const key =
              /** @type {Record<string, unknown>} */ (bundle)[groupBy];
            if (typeof key !== "string") continue;

            const bytes = await this.#fetchBlob(bundle.data.ref.$link);
            const groupItems = /** @type {unknown[]} */ (decode(bytes));
            if (!Array.isArray(groupItems)) continue;

            items.push(...groupItems);
            // Hash the re-encoded form so put() compares apples to apples.
            // Raw PDS bytes may not equal encode(decode(bytes)) if field order
            // or numeric encoding differs, causing spurious re-uploads.
            newHashes.set(key, xxh32r(encode(groupItems)).toString(16));
            newBlobs.set(key, bundle.data);
          }

          // Assign atomically after all fetches complete so a concurrent put()
          // never sees a partially-populated lastHashes and generates wrong #create ops.
          lastHashes = newHashes;
          lastBlobs = newBlobs;
          return items;
        },
        put: async (/** @type {unknown[]} */ data) => {
          const nsidTyped =
            /** @type {`${string}.${string}.${string}`} */ (nsid);

          const extractKey = keyOf ??
            ((/** @type {unknown} */ item) =>
              /** @type {string | undefined} */ (
                /** @type {Record<string, unknown>} */ (item)[groupBy]
              ));

          /** @type {Map<string, unknown[]>} */
          const groups = new Map();
          for (const item of data) {
            const key = extractKey(item);
            if (typeof key !== "string") continue;
            const group = groups.get(key) ?? [];
            if (!groups.has(key)) groups.set(key, group);
            group.push(item);
          }

          // Snapshot state so we only commit on success
          const newHashes = new Map(lastHashes);
          const newBlobs = new Map(lastBlobs);

          // Upload blobs for changed groups (outside write queue — not a mutation)
          /** @type {Array<{ rkey: string, value: unknown }>} */
          const upserts = [];

          for (const [key, groupItems] of groups) {
            const bytes = encode(groupItems);
            const hash = xxh32r(bytes).toString(16);

            if (lastHashes.get(key) === hash && lastBlobs.has(key)) continue;

            const blob = await this.#uploadBlob(bytes);
            if (!blob) continue;

            const rkey = xxh32r(encode(key)).toString(16);
            const value = {
              $type: nsidTyped,
              id: rkey,
              [groupBy]: key,
              data: blob,
            };
            upserts.push({ rkey, value });
            newHashes.set(key, hash);
            newBlobs.set(key, blob);
          }

          /** @type {WriteOp[]} */
          const deletes = [];
          for (const key of lastHashes.keys()) {
            if (!groups.has(key)) {
              const rkey = xxh32r(encode(key)).toString(16);
              deletes.push({
                $type: "com.atproto.repo.applyWrites#delete",
                collection: nsidTyped,
                rkey,
              });
              newHashes.delete(key);
              newBlobs.delete(key);
            }
          }

          if (upserts.length === 0 && deletes.length === 0) return;

          await this.#enqueueWrite(async () => {
            const rpc = this.#rpc;
            const did = this.#did.value;
            if (!rpc || !did) return;
            this.#writing++;
            try {
              // putRecord is a true upsert — no need to track create vs update
              for (const { rkey, value } of upserts) {
                const result = await ok(rpc.post("com.atproto.repo.putRecord", {
                  input: {
                    repo: did,
                    collection: nsidTyped,
                    rkey,
                    record: /** @type {Record<string, unknown>} */ (value),
                  },
                }));
                if (result?.commit?.rev) this.#rev.value = result.commit.rev;
              }
              for (let i = 0; i < deletes.length; i += 100) {
                const result = await ok(
                  rpc.post("com.atproto.repo.applyWrites", {
                    input: { repo: did, writes: deletes.slice(i, i + 100) },
                  }),
                );
                if (result?.commit?.rev) this.#rev.value = result.commit.rev;
              }
            } catch (err) {
              if (this.#isSessionError(err)) {
                this.#clearSession();
                return;
              }
              throw err;
            } finally {
              this.#writing--;
            }
          });

          lastHashes = newHashes;
          lastBlobs = newBlobs;
        },
    };
  }

  /**
   * Fetch the latest commit rev for this repo.
   * Returns `null` if not authenticated or on error.
   *
   * @returns {Promise<string | null>}
   */
  async getLatestCommit() {
    const did = this.#did.value;

    const rpc = this.#rpc;
    if (!rpc || !did) return null;

    try {
      const result = await ok(rpc.get(
        "com.atproto.sync.getLatestCommit",
        { params: { did } },
      ));

      this.#rev.value = result?.rev;
      return result?.rev;
    } catch (err) {
      if (this.#isSessionError(err)) {
        this.#clearSession();
        return null;
      }

      throw err;
    }
  }

  /** @param {Uint8Array} bytes */
  async #uploadBlob(bytes) {
    const rpc = this.#rpc;
    if (!rpc) return;
    const result = await ok(rpc.post("com.atproto.repo.uploadBlob", {
      input: bytes,
      headers: { "content-type": "application/octet-stream" },
    }));
    return result?.blob;
  }

  /**
   * @param {string} cid
   * @returns {Promise<Uint8Array>}
   */
  async #fetchBlob(cid) {
    const rpc = this.#rpc;
    const did = this.#did.value;
    if (!rpc || !did) return new Uint8Array();
    return await ok(rpc.get("com.atproto.sync.getBlob", {
      params: { did, cid },
      as: "bytes",
    }));
  }

  /**
   * @template T
   * @param {string} collection
   * @param {string} [did]
   * @returns {Promise<T[]>}
   */
  async listRecords(collection, did) {
    did ??= this.#did.value ?? undefined;

    if (!this.#rpc || !did) return [];

    try {
      const records = [];
      /** @type {string | undefined} */
      let cursor;
      do {
        const page =
          /** @type {{ records: { value: unknown }[], cursor?: string }} */ (
            await ok(this.#rpc.get("com.atproto.repo.listRecords", {
              params: {
                repo:
                  /** @type {import("@atcute/lexicons").ActorIdentifier} */ (did),
                collection:
                  /** @type {`${string}.${string}.${string}`} */ (collection),
                limit: 100,
                cursor,
              },
            }))
          );
        records.push(...page.records.map((r) => /** @type {T} */ (r.value)));
        cursor = page.cursor;
      } while (cursor);
      return records;
    } catch (err) {
      if (this.#isSessionError(err)) {
        this.#clearSession();
        return [];
      }

      throw err;
    }
  }

  // WRITE QUEUE

  /**
   * @param {() => Promise<void>} fn
   * @returns {Promise<void>}
   */
  #enqueueWrite(fn) {
    return new Promise((resolve, reject) => {
      this.#writeQueue.push({ fn, resolve, reject });
      this.#drainWrites();
    });
  }

  async #drainWrites() {
    if (this.#writeDraining) return;
    this.#writeDraining = true;

    while (this.#writeQueue.length > 0) {
      const { fn, resolve, reject } =
        /** @type {{ fn: () => Promise<void>, resolve: () => void, reject: (err: unknown) => void }} */ (
          this.#writeQueue.shift()
        );
      try {
        await fn();
        resolve();
      } catch (err) {
        reject(err);
      }
    }

    this.#writeDraining = false;
  }

}

export default ATProtoOutput;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ATProtoOutput;
export const NAME = "dor-atproto";

defineElement(NAME, ATProtoOutput);
```
## Supplements
### Last.fm Scrobbler

Scrobbles track plays to Last.fm.

```js
import { md5 } from "@noble/hashes/legacy.js";
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";

import {
  BroadcastableDiffuseElement,
  defineElement,
} from "~/common/element.js";
import { computed, signal } from "~/common/signal.js";
import { clearSession, readSession, saveSession } from "../session.js";

/**
 * @import {Track} from "~/definitions/types.d.ts"
 * @import {ScrobbleElement} from "@specs/components/supplement/types.d.ts"
 */

////////////////////////////////////////////
// CONSTANTS
////////////////////////////////////////////

const LASTFM_API_URL = "https://ws.audioscrobbler.com/2.0/";
const LASTFM_AUTH_URL = "https://www.last.fm/api/auth/";
const STORAGE_KEY = "diffuse/supplement/last.fm/session";

const DEFAULT_API_KEY = "4f0fe85b67baef8bb7d008a8754a95e5";
const DEFAULT_API_SECRET = "0cec3ca0f58e04a5082f1131aba1e0d3";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ScrobbleElement}
 */
class LastFmScrobbler extends BroadcastableDiffuseElement {
  static NAME = "diffuse/supplement/last.fm";

  get #apiKey() {
    return this.getAttribute("api-key") ?? DEFAULT_API_KEY;
  }

  get #apiSecret() {
    return this.getAttribute("api-secret") ?? DEFAULT_API_SECRET;
  }

  // SIGNALS

  #handle = signal(/** @type {string | null} */ (null));
  #sessionKey = signal(/** @type {string | null} */ (null));
  #isAuthenticating = signal(false);

  // STATE

  handle = this.#handle.get;
  isAuthenticated = computed(() => this.#sessionKey.value !== null);
  isAuthenticating = this.#isAuthenticating.get;

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        nowPlaying: { strategy: "leaderOnly", fn: this.nowPlaying },
        scrobble: { strategy: "leaderOnly", fn: this.scrobble },

        setHandle: { strategy: "replicate", fn: this.#handle.set },
        setSession: { strategy: "replicate", fn: this.#sessionKey.set },
      });

      if (actions) {
        this.nowPlaying = actions.nowPlaying;
        this.scrobble = actions.scrobble;

        this.#handle.set = actions.setHandle;
        this.#sessionKey.set = actions.setSession;
      }
    }

    super.connectedCallback();

    this.#tryRestore();
  }

  async #tryRestore() {
    await this.whenConnected();

    // last.fm appends ?token=TOKEN to the callback URL after authorization.
    const urlParams = new URLSearchParams(location.search);
    const urlToken = urlParams.get("token");

    if (urlToken) {
      urlParams.delete("token");
      const newSearch = urlParams.toString();
      history.replaceState(
        null,
        "",
        location.pathname + (newSearch ? "?" + newSearch : "") + location.hash,
      );

      this.#isAuthenticating.set(true);

      try {
        const session = await this.#getSession(urlToken);
        await this.#setSession(session);
      } catch (err) {
        console.warn("last.fm: failed to exchange token for session", err);
      } finally {
        this.#isAuthenticating.set(false);
      }

      return;
    }

    const stored = await readSession(this, STORAGE_KEY);

    if (stored) {
      try {
        const { key, name: handle } = JSON.parse(stored);
        if (await this.isLeader()) {
          this.#sessionKey.set(key);
          this.#handle.set(handle);
        } else {
          this.#sessionKey.value = key;
          this.#handle.value = handle;
        }
      } catch {
        await clearSession(this, STORAGE_KEY);
      }
    }
  }

  // AUTH

  /**
   * Initiate the last.fm auth flow.
   * Redirects the browser to the authorization page; last.fm appends ?token=TOKEN to the callback.
   */
  signIn() {
    const callbackUrl = location.origin + location.pathname + location.search;
    const authUrl = new URL(LASTFM_AUTH_URL);
    authUrl.searchParams.set("api_key", this.#apiKey);
    authUrl.searchParams.set("cb", callbackUrl);

    // Navigate the top-level frame so last.fm's X-Frame-Options doesn't block loading
    // when this element is used inside an iframe.
    (window.top ?? window).location.assign(authUrl.toString());
  }

  /**
   * Clear the stored session.
   */
  async signOut() {
    this.#sessionKey.set(null);
    this.#handle.set(null);
    await clearSession(this, STORAGE_KEY);
  }

  /** @param {{ key: string, name: string }} session */
  async #setSession({ key, name: handle }) {
    this.#sessionKey.set(key);
    this.#handle.set(handle);
    await saveSession(this, STORAGE_KEY, JSON.stringify({ key, name: handle }));
  }

  // SCROBBLE ACTIONS

  /**
   * @param {Track} track
   */
  async nowPlaying(track) {
    const tags = track.tags ?? {};
    /** @type {Record<string, string>} */
    const params = {};

    if (tags.title) params.track = tags.title;
    if (tags.artist) params.artist = tags.artist;
    if (tags.album) params.album = tags.album;
    if (tags.albumartist) params.albumArtist = tags.albumartist;
    if (tags.track?.no != null) params.trackNumber = String(tags.track.no);
    if (track.stats?.duration != null) {
      params.duration = String(Math.round(track.stats.duration / 1000));
    }

    return this.#authenticatedCall("track.updateNowPlaying", params);
  }

  /**
   * @param {Track} track
   * @param {number} startedAt Unix timestamp in milliseconds
   * @param {{ duration?: number }} [_options] duration in milliseconds
   */
  async scrobble(track, startedAt, _options) {
    const tags = track.tags ?? {};
    /** @type {Record<string, string>} */
    const params = {
      timestamp: String(Math.floor(startedAt / 1000)),
    };

    if (tags.title) params.track = tags.title;
    if (tags.artist) params.artist = tags.artist;
    if (tags.album) params.album = tags.album;
    if (tags.albumartist) params.albumArtist = tags.albumartist;
    if (tags.track?.no != null) params.trackNumber = String(tags.track.no);
    if (track.stats?.duration != null) {
      params.duration = String(Math.round(track.stats.duration / 1000));
    }

    return this.#authenticatedCall("track.scrobble", params);
  }

  // API

  /**
   * Sign a set of API parameters (excluding `format` and `callback`).
   *
   * @param {Record<string, string>} params
   * @returns {string} MD5 hex digest
   */
  #sign(params) {
    const str = Object.keys(params)
      .sort()
      .map((k) => k + params[k])
      .join("");
    return bytesToHex(md5(utf8ToBytes(str + this.#apiSecret)));
  }

  /**
   * @param {string} method
   * @param {Record<string, string>} [params]
   * @returns {Promise<any>}
   */
  async #call(method, params = {}) {
    const allParams = { ...params, api_key: this.#apiKey, method };
    const api_sig = this.#sign(allParams);
    const body = new URLSearchParams({ ...allParams, api_sig, format: "json" });

    const response = await fetch(LASTFM_API_URL, { method: "POST", body });
    const data = await response.json();

    if (data.error) {
      throw new Error(`last.fm error ${data.error}: ${data.message}`);
    }

    return data;
  }

  /**
   * @param {string} method
   * @param {Record<string, string>} [params]
   * @returns {Promise<any>}
   */
  async #authenticatedCall(method, params = {}) {
    const sk = this.#sessionKey.value;
    if (!sk) throw new Error("Not authenticated with last.fm");
    return this.#call(method, { ...params, sk });
  }

  /**
   * @param {string} token
   * @returns {Promise<{ key: string, name: string }>}
   */
  async #getSession(token) {
    const data = await this.#call("auth.getSession", { token });
    return data.session;
  }
}

export default LastFmScrobbler;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = LastFmScrobbler;
export const NAME = "ds-lastfm-scrobbler";

defineElement(NAME, CLASS);
```
### ListenBrainz Scrobbler

Scrobbles track plays to ListenBrainz.

```js
import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js";
import { computed, signal } from "~/common/signal.js";
import { clearSession, readSession, saveSession } from "../session.js";

/**
 * @import {Track} from "~/definitions/types.d.ts"
 * @import {ScrobbleElement} from "@specs/components/supplement/types.d.ts"
 */

////////////////////////////////////////////
// CONSTANTS
////////////////////////////////////////////

const API_BASE = "https://api.listenbrainz.org/1/";
const STORAGE_KEY = "diffuse/supplement/listenbrainz/session";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ScrobbleElement}
 */
class ListenBrainzScrobbler extends BroadcastableDiffuseElement {
  static NAME = "diffuse/supplement/listenbrainz";

  // SIGNALS

  #handle = signal(/** @type {string | null} */ (null));
  #userToken = signal(/** @type {string | null} */ (null));
  #isAuthenticating = signal(false);

  // STATE

  handle = this.#handle.get;
  isAuthenticated = computed(() => this.#userToken.value !== null);
  isAuthenticating = this.#isAuthenticating.get;

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        nowPlaying: { strategy: "leaderOnly", fn: this.nowPlaying },
        scrobble: { strategy: "leaderOnly", fn: this.scrobble },

        setHandle: { strategy: "replicate", fn: this.#handle.set },
        setUserToken: { strategy: "replicate", fn: this.#userToken.set },
      });

      if (actions) {
        this.nowPlaying = actions.nowPlaying;
        this.scrobble = actions.scrobble;

        this.#handle.set = actions.setHandle;
        this.#userToken.set = actions.setUserToken;
      }
    }

    super.connectedCallback();

    this.#tryRestore();
  }

  async #tryRestore() {
    await this.whenConnected();

    const stored = await readSession(this, STORAGE_KEY);

    if (stored) {
      try {
        const { token, username } = JSON.parse(stored);

        if (await this.isLeader()) {
          this.#userToken.set(token);
          this.#handle.set(username);
        } else {
          this.#userToken.value = token;
          this.#handle.value = username;
        }
      } catch {
        await clearSession(this, STORAGE_KEY);
      }
    }
  }

  // AUTH

  /**
   * Validate a ListenBrainz user token and store the session.
   *
   * @param {string} token
   */
  async signIn(token) {
    this.#isAuthenticating.set(true);
    try {
      const username = await this.#validateToken(token);
      this.#userToken.set(token);
      this.#handle.set(username);
      await saveSession(this, STORAGE_KEY, JSON.stringify({ token, username }));
    } catch (err) {
      console.warn("listenbrainz: failed to authenticate", err);
      throw err;
    } finally {
      this.#isAuthenticating.set(false);
    }
  }

  /**
   * Clear the stored session.
   */
  async signOut() {
    this.#userToken.set(null);
    this.#handle.set(null);
    await clearSession(this, STORAGE_KEY);
  }

  // SCROBBLE ACTIONS

  /**
   * @param {Track} track
   */
  async nowPlaying(track) {
    return this.#submit("playing_now", [
      { track_metadata: this.#trackMetadata(track) },
    ]);
  }

  /**
   * @param {Track} track
   * @param {number} startedAt Unix timestamp in milliseconds
   * @param {{ duration?: number }} [options] duration in milliseconds
   */
  async scrobble(track, startedAt, options) {
    return this.#submit("single", [
      {
        listened_at: Math.floor(startedAt / 1000),
        track_metadata: this.#trackMetadata(track, options?.duration),
      },
    ]);
  }

  // API

  /**
   * @param {Track} track
   * @param {number} [durationMs]
   * @returns {Record<string, unknown>}
   */
  #trackMetadata(track, durationMs) {
    const tags = track.tags ?? {};
    /** @type {Record<string, unknown>} */
    const additional_info = { submission_client: "Diffuse" };

    const duration = track.stats?.duration ?? durationMs;
    if (duration != null) {
      additional_info.duration_ms = duration;
    }
    if (tags.track?.no != null) {
      additional_info.tracknumber = tags.track.no;
    }

    /** @type {Record<string, unknown>} */
    const metadata = { additional_info };

    if (tags.title) metadata.track_name = tags.title;
    if (tags.artist) metadata.artist_name = tags.artist;
    if (tags.album) metadata.release_name = tags.album;

    return metadata;
  }

  /**
   * @param {string} token
   * @returns {Promise<string>} username
   */
  async #validateToken(token) {
    const response = await fetch(`${API_BASE}validate-token`, {
      headers: { Authorization: `Token ${token}` },
    });
    const data = await response.json();

    if (!data.valid) {
      throw new Error(
        `listenbrainz: invalid token — ${data.message ?? "unknown error"}`,
      );
    }

    return data.user_name;
  }

  /**
   * @param {string} listen_type
   * @param {unknown[]} payload
   * @returns {Promise<any>}
   */
  async #submit(listen_type, payload) {
    const token = this.#userToken.value;
    if (!token) throw new Error("Not authenticated with ListenBrainz");

    const response = await fetch(`${API_BASE}submit-listens`, {
      method: "POST",
      headers: {
        Authorization: `Token ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ listen_type, payload }),
    });

    const data = await response.json();

    if (data.code && data.code !== 200) {
      throw new Error(`listenbrainz error ${data.code}: ${data.error}`);
    }

    return data;
  }
}

export default ListenBrainzScrobbler;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ListenBrainzScrobbler;
export const NAME = "ds-listenbrainz-scrobbler";

defineElement(NAME, CLASS);
```
### Rocksky Scrobbler

Scrobbles track plays to Rocksky.

```js
import { now as tidNow } from "@atcute/tid";

import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js";
import { signal } from "~/common/signal.js";
import { clearSession, readSession, saveSession } from "../session.js";

import {
  clearStoredSession,
  DID_STORAGE_KEY,
  getSession,
  login,
  logout,
  OAuthUserAgent,
  restoreOrFinalize,
} from "./oauth.js";

/**
 * @import {Track} from "~/definitions/types.d.ts"
 * @import {ScrobbleElement} from "@specs/components/supplement/types.d.ts"
 */

////////////////////////////////////////////
// CONSTANTS
////////////////////////////////////////////

const STORAGE_KEY = "diffuse/supplement/rocksky/session";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @implements {ScrobbleElement}
 */
class RockskyScrobbler extends BroadcastableDiffuseElement {
  static NAME = "diffuse/supplement/rocksky";

  // SIGNALS

  #handle = signal(/** @type {string | null} */ (null));
  #connected = signal(false);
  #isAuthenticating = signal(false);

  // STATE

  handle = this.#handle.get;
  isAuthenticated = this.#connected.get;
  isAuthenticating = this.#isAuthenticating.get;

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(this.identifier, {
        nowPlaying: { strategy: "leaderOnly", fn: this.nowPlaying },
        scrobble: { strategy: "leaderOnly", fn: this.scrobble },

        setHandle: { strategy: "replicate", fn: this.#handle.set },
        setConnected: { strategy: "replicate", fn: this.#connected.set },
      });

      if (actions) {
        this.nowPlaying = actions.nowPlaying;
        this.scrobble = actions.scrobble;

        this.#handle.set = actions.setHandle;
        this.#connected.set = actions.setConnected;
      }
    }

    super.connectedCallback();

    this.#tryRestore();
  }

  async #tryRestore() {
    await this.whenConnected();

    try {
      const session = await restoreOrFinalize();

      if (session) {
        const did = session.info.sub;

        if (await this.isLeader()) {
          this.#connected.set(true);
          this.#handle.set(did);
        } else {
          this.#connected.value = true;
          this.#handle.value = did;
        }

        await saveSession(this, STORAGE_KEY, JSON.stringify({ did }));
        return;
      }
    } catch (err) {
      console.warn("Rocksky: Failed to restore/finalize session", err);
    }

    // Restore previously stored connection state.
    const stored = await readSession(this, STORAGE_KEY);

    if (stored) {
      try {
        const { did } = JSON.parse(stored);

        if (await this.isLeader()) {
          this.#connected.set(true);
          this.#handle.set(did);
        } else {
          this.#connected.value = true;
          this.#handle.value = did;
        }
      } catch {
        await clearSession(this, STORAGE_KEY);
      }
    }
  }

  // AUTH

  /**
   * Connect to Rocksky by initiating the AT Protocol OAuth flow for the given handle.
   * Navigates the browser away to the authorization server.
   *
   * @param {string} handle
   */
  async signIn(handle) {
    this.#isAuthenticating.set(true);

    try {
      await login(handle);
    } finally {
      this.#isAuthenticating.set(false);
    }
  }

  /**
   * Disconnect from Rocksky.
   */
  async signOut() {
    const did = localStorage.getItem(DID_STORAGE_KEY);

    if (did) {
      getSession(/** @type {`did:${string}:${string}`} */ (did))
        .then((session) => logout(new OAuthUserAgent(session)))
        .catch(() => clearStoredSession());
    } else {
      clearStoredSession();
    }

    this.#connected.set(false);
    this.#handle.set(null);
    await clearSession(this, STORAGE_KEY);
  }

  // SCROBBLE ACTIONS

  /**
   * @param {Track} _track
   */
  // deno-lint-ignore no-unused-vars
  async nowPlaying(_track) {
    // Rocksky has no now-playing PDS record type; scrobbles are the source of truth.
  }

  /**
   * @param {Track} track
   * @param {number} startedAt Unix timestamp in milliseconds
   * @param {{ duration?: number }} [options] duration in milliseconds
   */
  async scrobble(track, startedAt, { duration: durationMs } = {}) {
    if (!this.#connected.value) return;

    const did = localStorage.getItem(DID_STORAGE_KEY);
    if (!did) return;

    const session = await getSession(/** @type {`did:${string}:${string}`} */ (did));
    const agent = new OAuthUserAgent(session);

    const tags = track.tags ?? {};
    const duration = track.stats?.duration ?? durationMs;

    // duration has no meaningful fallback value; skip rather than create an invalid record
    if (duration == null) return;

    /** @type {Record<string, unknown>} */
    const record = {
      $type: "app.rocksky.scrobble",
      createdAt: new Date(startedAt).toISOString(),
      title: tags.title || "Unknown",
      artist: tags.artist || "Unknown",
      album: tags.album || "Unknown",
      albumArtist: tags.albumartist || tags.artist || "Unknown",
      duration,
    };

    if (tags.track?.no != null) record.trackNumber = tags.track.no;
    if (tags.disc?.no != null) record.discNumber = tags.disc.no;

    const response = await agent.handle("/xrpc/com.atproto.repo.putRecord", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        repo: did,
        collection: "app.rocksky.scrobble",
        rkey: tidNow(),
        record,
        validate: false,
      }),
    });

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new Error(`rocksky: scrobble failed ${response.status}: ${error.message ?? ""}`);
    }
  }
}

export default RockskyScrobbler;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = RockskyScrobbler;
export const NAME = "ds-rocksky-scrobbler";

defineElement(NAME, CLASS);
```
## Transformers
### Output / Bytes / Automerge

Translate data to and from an Automerge CRDT.

```js
import * as Automerge from "@automerge/automerge";
import { ifDefined } from "lit-html/directives/if-defined.js";
import { isUint8Array } from "iso-base/utils";

import "~/components/output/polymorphic/indexed-db/element.js";

import { computed, signal } from "~/common/signal.js";
import {
  recursivelyCloneRecords,
  removeUndefinedValuesFromRecord,
} from "~/common/utils.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";
import {
  INITIAL_FACETS_DOCUMENT,
  INITIAL_PLAYLIST_ITEMS_DOCUMENT,
  INITIAL_SETTINGS_DOCUMENT,
  INITIAL_TRACKS_DOCUMENT,
} from "./constants.js";

/**
 * @import { RenderArg } from "~/common/element.d.ts"
 * @import { SignalReader } from "~/common/signal.d.ts";
 * @import { OutputElement } from "@specs/components/output/types.d.ts";
 */

/**
 * @extends {OutputTransformer<Uint8Array>}
 */
class AutomergeBytesOutputTransformer extends OutputTransformer {
  constructor() {
    super();

    const remote = this.base();
    const local = this.#localOutput.get;

    /**
     * @template T
     * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} localCollection
     * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} remoteCollection
     * @param {Automerge.Doc<T>} initial
     * @returns {SignalReader<{ doc: Automerge.Doc<T>; diverged: boolean; local: boolean; remote: boolean; remoteLoaded: boolean; }>}
     */
    const state = (localCollection, remoteCollection, initial) =>
      computed(() => {
        const lc = localCollection();
        const rc = remote.ready() ? remoteCollection() : undefined;

        const l = loadDocument(lc?.state === "loaded" ? lc.data : undefined);
        const r = rc?.state === "loaded" ? loadDocument(rc.data) : undefined;
        const remoteLoaded = rc?.state === "loaded";

        if (!r) {
          return l
            ? {
              doc: l,
              diverged: true,
              local: false,
              remote: true,
              remoteLoaded,
            }
            : {
              doc: initial,
              diverged: false,
              local: false,
              remote: false,
              remoteLoaded,
            };
        } else if (!l) {
          return {
            doc: r,
            diverged: true,
            local: true,
            remote: false,
            remoteLoaded,
          };
        }

        const lh = Automerge.getHeads(l)[0];
        const rh = Automerge.getHeads(r)[0];
        const diverged = lh !== rh;

        return {
          doc: diverged
            ? Automerge.merge(Automerge.clone(l), Automerge.clone(r))
            : r,
          diverged,
          local: Automerge.hasHeads(r, [lh]),
          remote: Automerge.hasHeads(l, [rh]),
          remoteLoaded,
        };
      });

    const facets = state(
      computed(() => local()?.facets?.collection() ?? { state: "loading" }),
      remote.facets.collection,
      INITIAL_FACETS_DOCUMENT,
    );

    const playlistItems = state(
      computed(() =>
        local()?.playlistItems?.collection() ?? { state: "loading" }
      ),
      remote.playlistItems.collection,
      INITIAL_PLAYLIST_ITEMS_DOCUMENT,
    );

    const settings = state(
      computed(() => local()?.settings?.collection() ?? { state: "loading" }),
      remote.settings.collection,
      INITIAL_SETTINGS_DOCUMENT,
    );

    const tracks = state(
      computed(() => local()?.tracks?.collection() ?? { state: "loading" }),
      remote.tracks.collection,
      INITIAL_TRACKS_DOCUMENT,
    );

    this.facets = automergeEntry(
      computed(() => local()?.facets),
      remote.facets,
      computed(() => facets().doc),
      {
        stripUndefined: true,
      },
    );

    this.playlistItems = automergeEntry(
      computed(() => local()?.playlistItems),
      remote.playlistItems,
      computed(() => playlistItems().doc),
    );

    this.settings = automergeEntry(
      computed(() => local()?.settings),
      remote.settings,
      computed(() => settings().doc),
    );

    this.tracks = automergeEntry(
      computed(() => local()?.tracks),
      remote.tracks,
      computed(() => tracks().doc),
    );

    this.ready = () => true;

    // Effects
    this.effect(() => {
      const l = local();
      if (!l) return;

      this.effect(() => {
        if (!facets().remoteLoaded) return;
        const s = facets();
        if (s.diverged) {
          const bytes = Automerge.save(s.doc);
          if (l && s.local) l.facets.save(bytes);
          if (s.remote) remote.facets.save(bytes);
        }
      });

      this.effect(() => {
        if (!playlistItems().remoteLoaded) return;
        const s = playlistItems();
        if (s.diverged) {
          const bytes = Automerge.save(s.doc);
          if (l && s.local) l.playlistItems.save(bytes);
          if (s.remote) remote.playlistItems.save(bytes);
        }
      });

      this.effect(() => {
        if (!settings().remoteLoaded) return;
        const s = settings();
        if (s.diverged) {
          const bytes = Automerge.save(s.doc);
          if (l && s.local) l.settings.save(bytes);
          if (s.remote) remote.settings.save(bytes);
        }
      });

      this.effect(() => {
        if (!tracks().remoteLoaded) return;
        const s = tracks();
        if (s.diverged) {
          const bytes = Automerge.save(s.doc);
          if (l && s.local) l.tracks.save(bytes);
          if (s.remote) remote.tracks.save(bytes);
        }
      });
    });
  }

  // SIGNALS

  #localOutput = signal(
    /** @type {OutputElement<Uint8Array | undefined> | undefined} */ (undefined),
  );

  // LIFECYCLE

  /**
   * @override
   */
  connectedCallback() {
    super.connectedCallback();

    /** @type {OutputElement<Uint8Array | undefined> | null} */
    const local = this.root().querySelector("dop-indexed-db");
    if (!local) throw new Error("Can't find local output");

    // When defined
    customElements.whenDefined(local.localName).then(() => {
      this.#localOutput.value = local;
    });
  }

  // RENDER

  /**
   * @param {RenderArg} _
   */
  render({ html }) {
    return html`
      <dop-indexed-db
        namespace="${ifDefined(this.getAttribute(`namespace`))}"
      ></dop-indexed-db>
    `;
  }
}

export default AutomergeBytesOutputTransformer;

////////////////////////////////////////////
// 🛠️
////////////////////////////////////////////

/**
 * @template T
 * @param {Uint8Array | undefined} value
 * @returns {Automerge.Doc<T> | undefined}
 */
export function loadDocument(value) {
  if (isUint8Array(value)) {
    return Automerge.load(value);
  } else if (value == undefined) {
    return undefined;
  } else {
    throw new Error("Invalid data type");
  }
}

/**
 * @template {Record<string, any>} T
 * @param {SignalReader<{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void> } | undefined>} local
 * @param {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void> }} remote
 * @param {SignalReader<Automerge.Doc<{ collection: T[] }>>} document
 * @param {{ stripUndefined?: boolean }} [opts]
 * @returns {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: T[] }>, reload: () => Promise<void>, save: (items: T[]) => Promise<void> }}
 */
export function automergeEntry(local, remote, document, opts) {
  return {
    collection: computed(() => {
      const col = local()?.collection();
      if (!col || col.state !== "loaded") {
        return { state: col?.state ?? "loading" };
      }
      return { state: "loaded", data: document().collection };
    }),
    reload: remote.reload,
    save: async (/** @type {T[]} */ newItems) => {
      const doc = Automerge.change(document(), (d) => {
        d.collection = newItems.map((item) => {
          const cloned = recursivelyCloneRecords(item);
          return opts?.stripUndefined
            ? removeUndefinedValuesFromRecord(cloned)
            : cloned;
        });
      });

      const bytes = Automerge.save(doc);
      await local()?.save(bytes);
    },
  };
}

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = AutomergeBytesOutputTransformer;
export const NAME = "dtob-automerge";

defineElement(NAME, CLASS);
```
### Output / Bytes / DASL Sync

Syncs data between local and remote using CID-based diffing and performs union merges with tombstone tracking when both sides have diverged.

```js
import { decode, encode } from "@atcute/cbor";
import { ifDefined } from "lit-html/directives/if-defined.js";
import deepDiff from "@fry69/deep-diff";

import "~/components/output/polymorphic/indexed-db/element.js";

import * as CID from "~/common/cid.js";
import { diff, strictEquality } from "~/common/compare.js";
import { computed, signal } from "~/common/signal.js";
import { compareTimestamps } from "~/common/temporal.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";

/**
 * @import { SignalReader } from "~/common/signal.d.ts";
 * @import { RenderArg } from "~/common/element.d.ts"
 * @import { OutputElement } from "@specs/components/output/types.d.ts"
 *
 * @import { Container } from "@specs/components/transformer/output/bytes/dasl-sync/types.d.ts"
 */

/** @type {Container<any>} */
const EMPTY = {
  cid: undefined,
  data: [],
  inventory: { current: {}, removed: [] },
};

/**
 * @extends {OutputTransformer<Uint8Array>}
 */
class DaslBytesSyncOutputTransformer extends OutputTransformer {
  static NAME = "diffuse/transformer/output/bytes/dasl-sync";

  constructor() {
    super();

    const remote = this.base();
    const local = this.#localOutput.get;

    /**
     * @template {{ id: string; updatedAt: string }} T
     * @param {string} kind
     * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} localCollection
     * @param {SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>} remoteCollection
     * @param {{ saveLocal: (bytes: Uint8Array) => Promise<void>; saveRemote: (bytes: Uint8Array) => Promise<void> }} sync
     */
    const state = (
      kind,
      localCollection,
      remoteCollection,
      { saveLocal, saveRemote },
    ) => {
      const container = signal(
        /** @type {Container<T>} */ (EMPTY),
        { compare: strictEquality },
      );

      const isReady = signal(false);
      const merging = signal({ isBusy: false, lastCID: "" }, {
        compare: diff,
      });

      this.effect(() => {
        if (!isReady.value) return;
        if (merging.value.isBusy) return;

        const lc = localCollection();
        const rc = remote.ready() ? remoteCollection() : undefined;

        const lb = lc?.state === "loaded" ? lc.data : undefined;
        const rb = rc?.state === "loaded" ? rc.data : undefined;
        const rs = rc?.state;

        /** @type {Container<T> | undefined} */
        const l = lb ? decode(lb) : undefined;

        /** @type {Container<T> | undefined} */
        const r = rb && rs === "loaded" ? decode(rb) : undefined;

        if (!r) {
          if (l) {
            container.value = l;

            if (remote.ready() && rs === "loaded") {
              this.isLeader().then((isLeader) => {
                if (!isLeader) return;
                const bytes = this.save(l);
                saveRemote(bytes);
              });
            }
          }
        } else if (!l) {
          container.value = r;

          this.isLeader().then((isLeader) => {
            if (!isLeader) return;
            const bytes = this.save(r);
            saveLocal(bytes);
          });
        } else if (
          rs === "loaded" && this.hasDiverged({ local: l, remote: r })
        ) {
          // Async merge
          this.isLeader().then((isLeader) => {
            if (!isLeader) return;

            merging.value = { isBusy: true, lastCID: merging.value.lastCID };

            this.merge(l, r).then(async (c) => {
              try {
                container.value = c;

                if (c.cid === merging.value.lastCID) return;

                const bytes = this.save(c);

                if (c.cid !== l.cid) {
                  await saveLocal(bytes);
                }

                if (remote.ready() && rs === "loaded" && c.cid !== r.cid) {
                  await saveRemote(bytes);
                }
              } finally {
                merging.value = { isBusy: false, lastCID: c.cid ?? "" };
              }
            });
          });
        } else {
          container.value = l;
        }
      });

      return computed(() => {
        if (!isReady.get()) isReady.value = true;
        return container.get();
      });
    };

    // Container signals
    const facets = state(
      "facets",
      computed(() => local()?.facets.collection() ?? { state: "loading" }),
      remote.facets.collection,
      {
        saveLocal: async (v) => local()?.facets.save(v),
        saveRemote: remote.facets.save,
      },
    );

    const playlistItems = state(
      "playlistItems",
      computed(() =>
        local()?.playlistItems.collection() ?? { state: "loading" }
      ),
      remote.playlistItems.collection,
      {
        saveLocal: async (v) => local()?.playlistItems.save(v),
        saveRemote: remote.playlistItems.save,
      },
    );

    const settings = state(
      "settings",
      computed(() => local()?.settings.collection() ?? { state: "loading" }),
      remote.settings.collection,
      {
        saveLocal: async (v) => local()?.settings.save(v),
        saveRemote: remote.settings.save,
      },
    );

    const tracks = state(
      "tracks",
      computed(() => local()?.tracks.collection() ?? { state: "loading" }),
      remote.tracks.collection,
      {
        saveLocal: async (v) => local()?.tracks.save(v),
        saveRemote: remote.tracks.save,
      },
    );

    // Output manager
    this.facets = this.managerProp(
      { save: async (v) => local()?.facets.save(v) },
      remote.facets,
      remote.ready,
      facets,
    );

    this.playlistItems = this.managerProp(
      { save: async (v) => local()?.playlistItems.save(v) },
      remote.playlistItems,
      remote.ready,
      playlistItems,
    );

    this.settings = this.managerProp(
      { save: async (v) => local()?.settings.save(v) },
      remote.settings,
      remote.ready,
      settings,
    );

    this.tracks = this.managerProp(
      { save: async (v) => local()?.tracks.save(v) },
      remote.tracks,
      remote.ready,
      tracks,
    );

    this.ready = () => true;
  }

  // SIGNALS

  #localOutput = signal(
    /** @type {OutputElement<any> | undefined} */ (undefined),
  );

  // LIFECYCLE

  /**
   * @override
   */
  async connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      this.broadcast(this.identifier, {});
    }

    super.connectedCallback();

    /** @type {OutputElement<any> | null} */
    const local = this.root().querySelector("dop-indexed-db");
    if (!local) throw new Error("Can't find local output");

    customElements.whenDefined(local.localName).then(() => {
      this.#localOutput.value = local;
    });
  }

  // DATA FUNCTIONS

  /**
   * @template {{ id: string; updatedAt: string }} T
   * @param {{ previous: Container<T>, collection: T[] }} _
   * @returns {Promise<Container<T>>}
   */
  async updateContainer({ previous, collection }) {
    const inventory = previous.inventory;

    const collIds = collection.map(({ id }) => id);

    const currSet = new Set(Object.keys(inventory.current));
    const collSet = new Set(collIds);

    const newSet = collSet.difference(currSet);
    const remSet = currSet.difference(collSet);

    const alreadyRemoved = new Set(inventory.removed);
    const allRemoved = alreadyRemoved.union(remSet);

    /** @type {Record<string, string>} */
    const current = { ...inventory.current };

    remSet.forEach((id) => {
      delete current[id];
    });

    /** @type Promise<void>[] */
    const promises = [];

    collection.forEach((a) => {
      const encoded = encode(a);

      promises.push((async () => {
        const cid = await CID.create(0x71, encoded);
        current[a.id] = cid;
      })());
    });

    await Promise.all(promises);

    const newInventory = {
      current,
      removed: Array.from(allRemoved),
    };

    return {
      cid: await CID.create(0x71, encode(newInventory)),
      data: collection,
      inventory: newInventory,
    };
  }

  /**
   * @template {{ id: string; updatedAt: string }} T
   * @param {{ local: Container<T>, remote: Container<T> }} _
   */
  hasDiverged({ local, remote }) {
    return local.cid !== remote.cid;
  }

  /**
   * @template {{ id: string; updatedAt: string }} T
   * @param {Container<T>} a
   * @param {Container<T>} b
   * @returns {Promise<Container<T>>}
   */
  async merge(a, b) {
    const removedA = new Set(a.inventory.removed);
    const removedB = new Set(b.inventory.removed);
    const allRemoved = removedA.union(removedB);

    const currentA = a.inventory.current;
    const currentB = b.inventory.current;

    const mapA = new Map(a.data.map((item) => [item.id, item]));
    const mapB = new Map(b.data.map((item) => [item.id, item]));

    // Combine all known ids from both sides
    const allIds = new Set([
      ...Object.keys(currentA),
      ...Object.keys(currentB),
    ]);

    /** @type {Record<string, string>} */
    const current = {};

    /** @type {T[]} */
    const data = [];

    // Construct `current` and `data`
    /** @type {Promise<void>[]} */
    const cidPromises = [];

    for (const id of allIds) {
      if (allRemoved.has(id)) continue;

      if (id in currentA && id in currentB) {
        const itemA = mapA.get(id);
        const itemB = mapB.get(id);

        if (!itemA || !itemB) {
          console.warn("Should have found both items but didn't!");
          continue;
        }

        // Items are identical, no merge or CID recomputation needed
        if (currentA[id] === currentB[id]) {
          data.push(itemA);
          current[id] = currentA[id];
          continue;
        }

        const isANewerThanB = itemA.updatedAt && itemB.updatedAt
          ? compareTimestamps(itemA.updatedAt, itemB.updatedAt) > 0
          : false;

        const newestItem = isANewerThanB ? itemA : itemB;
        const oldItem = isANewerThanB ? itemB : itemA;

        /** @type {T} */
        const mergedItem = { ...oldItem };

        deepDiff.applyDiff(mergedItem, newestItem);

        data.push(mergedItem);

        cidPromises.push(
          CID.create(0x71, encode(mergedItem)).then((cid) => {
            current[id] = cid;
          }),
        );
      } else {
        const item = mapA.get(id) ?? mapB.get(id);

        if (item) {
          data.push(item);
          current[id] = currentA[id] ?? currentB[id];
        }
      }
    }

    await Promise.all(cidPromises);

    // New inventory
    const updatedInventory = { current, removed: Array.from(allRemoved) };

    return {
      cid: await CID.create(0x71, encode(updatedInventory)),
      data,
      inventory: updatedInventory,
    };
  }

  /**
   * @template {{ id: string; updatedAt: string }} T
   * @param {Container<T>} container
   * @returns {Uint8Array}
   */
  save(container) {
    return encode(container);
  }

  // OUTPUT MANAGER FUNCTIONS

  /**
   * @template {{ id: string; updatedAt: string }} T
   * @param {{ save: (bytes: Uint8Array) => Promise<void> | void }} local
   * @param {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: Uint8Array | undefined }>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void> }} remote
   * @param {SignalReader<boolean>} remoteReady
   * @param {SignalReader<Container<T>>} container
   * @returns {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: T[] }>, reload: () => Promise<void>, save: (items: T[]) => Promise<void> }}
   */
  managerProp(local, remote, remoteReady, container) {
    return {
      collection: computed(() => {
        const c = container();

        if (c.cid === undefined && remoteReady() && remote.collection().state === "loading") {
          return { state: "loading" };
        }

        return { state: "loaded", data: c.data };
      }),
      reload: remote.reload,
      save: async (/** @type {T[]} */ newItems) => {
        const adjustedContainer = await this.updateContainer({
          collection: newItems,
          previous: container(),
        });

        const bytes = this.save(adjustedContainer);
        await local.save(bytes);
      },
    };
  }

  // RENDER

  /**
   * @param {RenderArg} _
   */
  render({ html }) {
    return html`
      <dop-indexed-db
        group="${ifDefined(this.getAttribute(`group`))}"
        namespace="${ifDefined(this.getAttribute(`namespace`))}"
      ></dop-indexed-db>
    `;
  }
}

export default DaslBytesSyncOutputTransformer;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = DaslBytesSyncOutputTransformer;
export const NAME = "dtob-dasl-sync";

defineElement(NAME, CLASS);
```
### Output / Bytes / JSON

Raw data schema output to and from JSON Uint8Array.

```js
import { computed } from "~/common/signal.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";

/**
 * @import { OutputManagerDeputy } from "@specs/components/output/types.d.ts"
 * @import { Facet, PlaylistItem, Setting, Track } from "~/definitions/types.d.ts"
 */

/**
 * @extends {OutputTransformer<Uint8Array>}
 */
class JsonStringOutputTransformer extends OutputTransformer {
  constructor() {
    super();

    const base = this.base();

    /** @type {OutputManagerDeputy} */
    const manager = {
      facets: {
        ...base.facets,
        collection: computed(() => {
          const col = base.facets.collection();
          if (col.state !== "loaded") return col;
          /** @type {Facet[]} */
          const data = parseArray(col.data);
          return { state: "loaded", data };
        }),
        save: async (newFacets) => {
          const json = JSON.stringify(newFacets);
          const encoder = new TextEncoder();
          const bytes = encoder.encode(json);
          await base.facets.save(bytes);
        },
      },
      playlistItems: {
        ...base.playlistItems,
        collection: computed(() => {
          const col = base.playlistItems.collection();
          if (col.state !== "loaded") return col;
          /** @type {PlaylistItem[]} */
          const data = parseArray(col.data);
          return { state: "loaded", data };
        }),
        save: async (newPlaylistItems) => {
          const json = JSON.stringify(newPlaylistItems);
          const encoder = new TextEncoder();
          const bytes = encoder.encode(json);
          await base.playlistItems.save(bytes);
        },
      },
      settings: {
        ...base.settings,
        collection: computed(() => {
          const col = base.settings.collection();
          if (col.state !== "loaded") return col;
          /** @type {Setting[]} */
          const data = parseArray(col.data);
          return { state: "loaded", data };
        }),
        save: async (newSettings) => {
          const json = JSON.stringify(newSettings);
          const encoder = new TextEncoder();
          const bytes = encoder.encode(json);
          await base.settings.save(bytes);
        },
      },
      tracks: {
        ...base.tracks,
        collection: computed(() => {
          const col = base.tracks.collection();
          if (col.state !== "loaded") return col;
          /** @type {Track[]} */
          const data = parseArray(col.data);
          return { state: "loaded", data };
        }),
        save: async (newTracks) => {
          const json = JSON.stringify(newTracks);
          const encoder = new TextEncoder();
          const bytes = encoder.encode(json);
          await base.tracks.save(bytes);
        },
      },

      // Other
      ready: base.ready,
    };

    // Assign manager properties to class
    this.facets = manager.facets;
    this.playlistItems = manager.playlistItems;
    this.settings = manager.settings;
    this.tracks = manager.tracks;
    this.ready = manager.ready;
  }
}

/**
 * @param {Uint8Array | string | undefined} data
 */
function parseArray(data) {
  let json;

  if (data instanceof Uint8Array) {
    const decoder = new TextDecoder();
    json = decoder.decode(data);
  } else if (data === undefined) {
    return [];
  } else {
    json = data;
  }

  try {
    return JSON.parse(json);
  } catch (err) {
    console.error(err);
    return [];
  }
}

export default JsonStringOutputTransformer;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = JsonStringOutputTransformer;
export const NAME = "dtob-json";

defineElement(NAME, CLASS);
```
### Output / Raw / AT Protocol Sync

Wraps an AT Protocol output with a local IndexedDB cache. Uses the repo revision to skip unnecessary fetches and performs union merges with tombstone tracking when both local and remote have diverged.

```js
import { ifDefined } from "lit-html/directives/if-defined.js";

import "~/components/output/polymorphic/indexed-db/element.js";

import { computed, signal } from "~/common/signal.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";
import * as Output from "~/common/output.js";

/**
 * @import { RenderArg } from "~/common/element.d.ts"
 * @import { OutputElement } from "@specs/components/output/types.d.ts"
 * @import { ATProtoOutputElement } from "@specs/components/output/raw/atproto/types.d.ts"
 */

const COLLECTIONS = /** @type {const} */ ([
  "facets",
  "playlistItems",
  "settings",
  "tracks",
]);

/** @type {Record<string, string>} */
const NSID_TO_COLLECTION = {
  "sh.diffuse.output.facet": "facets",
  "sh.diffuse.output.playlistItemBundle": "playlistItems",
  "sh.diffuse.output.setting": "settings",
  "sh.diffuse.output.trackBundle": "tracks",
};

const STORAGE_PREFIX = "diffuse/transformer/output/atproto-sync";

/**
 * Wraps an AT Protocol output with a local IndexedDB cache.
 *
 * Uses the repo `rev` (revision) from the AT Protocol to skip
 * unnecessary fetches when nothing changed remotely.
 *
 * When both local and remote have diverged, performs a union merge
 * by record `id`: records from both sides are combined, with
 * `updatedAt` used as a tiebreaker for conflicts on the same id
 * (falling back to local wins).
 *
 * Maintains a per-collection tombstone set of deleted record ids
 * so that records deleted on one side are not re-introduced by
 * the other during merge.
 *
 * @extends {OutputTransformer<null>}
 */
class ATProtoOutputSyncTransformer extends OutputTransformer {
  static NAME = "diffuse/transformer/output/raw/atproto-sync";

  constructor() {
    super();

    const remote = this.base();
    const local = this.#localOutput.get;

    for (const name of COLLECTIONS) {
      /** @ts-ignore */
      this[name] = {
        collection: computed(() => {
          const l = local();
          if (!l) return { state: "loading" };
          const c = l[name].collection();
          if (c.state === "loading") return c;
          return { state: "loaded", data: c.data ?? [] };
        }),
        reload: async () => {
          await this.#sync();
        },
        save: async (/** @type {any} */ newData) => {
          const l = local();
          if (!l) return;

          const newIds = new Set(newData.map((/** @type {any} */ r) => r.id));

          // Update tombstones in one pass: add for records deleted from local,
          // remove for records being (re-)added — so that fixed-ID records can
          // be recreated after deletion without the tombstone blocking them.
          const tombstones = this.#getTombstones(name);
          let tombstonesChanged = false;

          const existing = l[name].collection();
          const existingArr =
            existing.state === "loaded" && Array.isArray(existing.data)
              ? existing.data
              : [];

          for (const record of existingArr) {
            if (!newIds.has(record.id) && !tombstones.has(record.id)) {
              tombstones.add(record.id);
              tombstonesChanged = true;
            }
          }

          for (const record of newData) {
            if (tombstones.has(record.id)) {
              tombstones.delete(record.id);
              tombstonesChanged = true;
            }
          }

          if (tombstonesChanged) {
            localStorage.setItem(
              `${STORAGE_PREFIX}/tombstones/${name}`,
              JSON.stringify([...tombstones]),
            );
          }

          this.#markDirty();
          await l[name].save(newData);

          if (remote.ready()) {
            // Merge with any records added remotely since the last sync so we
            // don't accidentally overwrite them with our local-only view.
            const remoteCol = remote[name].collection();
            const dataForRemote =
              remoteCol.state === "loaded" && Array.isArray(remoteCol.data)
                ? this.#mergeRecords(name, newData, /** @type {typeof newData} */ (remoteCol.data))
                : newData;

            remote[name].save(dataForRemote).then(() => {
              const rev = this.#atproto()?.rev();
              if (rev) this.#storeRev(rev);
              this.#clearDirty();
            }).catch((err) => {
              console.error(err);
            });
          }
        },
      };
    }

    this.ready = () => true;

    // Sync when remote becomes ready
    this.effect(() => {
      const l = local();
      if (!l) return;

      this.effect(async () => {
        if (!remote.ready()) return;
        if (!(await this.isLeader())) return;
        this.#sync();
      });

      // Re-sync when firehose detects a remote change
      this.effect(async () => {
        const atproto = this.#atproto();
        if (!atproto) return;
        const firehose = atproto.firehoseRev();
        if (!firehose) return;
        if (!remote.ready()) return;
        if (!(await this.isLeader())) return;
        const touched = /** @type {string[]} */ (
          [...firehose.collections]
            .map((nsid) => NSID_TO_COLLECTION[nsid])
            .filter((name) => name !== undefined)
        );
        if (touched.length > 0) this.#sync(touched, firehose.rev);
      });
    });
  }

  // SIGNALS

  #localOutput = signal(
    /** @type {OutputElement<any> | undefined} */ (undefined),
  );

  #syncing = false;

  /**
   * @returns {ATProtoOutputElement | undefined}
   */
  #atproto() {
    return /** @type {any} */ (this.output.signal());
  }

  // SYNC

  /**
   * @param {readonly string[]} collections
   * @param {string | null} [knownRev]
   */
  async #sync(collections = COLLECTIONS, knownRev = null) {
    if (this.#syncing) return;
    this.#syncing = true;

    try {
      const l = this.#localOutput.get();
      const remote = this.base();
      const atproto = this.#atproto();

      if (!l || !atproto || !remote.ready()) return;

      const isFull = collections === COLLECTIONS;
      /** @type {Record<string, any>} */
      const lAny = l;
      /** @type {Record<string, any>} */
      const remoteAny = remote;

      const remoteRev = knownRev ?? await atproto.getLatestCommit();
      if (!remoteRev) return;

      if (isFull) {
        const localRev = this.#getStoredRev();
        const dirty = this.#isDirty();
        if (localRev === remoteRev && !dirty) return;
      }

      // Fetch remote data for the affected collections only
      for (const name of collections) {
        await remoteAny[name].reload();
      }

      const localCollections = await Promise.all(
        collections.map((name) => Output.data(lAny[name])),
      );

      const localHasData = localCollections.some(
        (data) => Array.isArray(data) && data.length > 0,
      );

      if (!localHasData && !this.#isDirty()) {
        // Local is empty and clean — just pull remote
        for (const name of collections) {
          const remoteCol = remoteAny[name].collection();
          if (
            remoteCol.state === "loaded" && Array.isArray(remoteCol.data) &&
            remoteCol.data.length > 0
          ) {
            this.#trackIds(name, remoteCol.data);
            await lAny[name].save(remoteCol.data);
          }
        }
      } else {
        // Union merge
        for (const name of collections) {
          const localCol = lAny[name].collection();
          const remoteCol = remoteAny[name].collection();
          const localArr =
            localCol.state === "loaded" && Array.isArray(localCol.data)
              ? localCol.data
              : [];
          const remoteArr =
            remoteCol.state === "loaded" && Array.isArray(remoteCol.data)
              ? remoteCol.data
              : [];

          const merged = this.#mergeRecords(name, localArr, remoteArr);

          await lAny[name].save(merged);

          if (this.#differFromRemote(merged, remoteArr)) {
            await remoteAny[name].save(merged);
          }
          this.#trackIds(name, merged);
        }
      }

      this.#storeRev(atproto.rev() ?? remoteRev);
      if (isFull) this.#clearDirty();
    } catch (err) {
      console.warn("Sync failed:", err);
    } finally {
      this.#syncing = false;
    }
  }

  /**
   * Union merge two record arrays by `id`.
   *
   * - Records only in local → keep (unless tombstoned)
   * - Records only in remote → keep (unless tombstoned)
   * - Records in both → pick the one with the later `updatedAt`,
   *   falling back to local wins
   * - Records whose id is in the tombstone set are excluded
   *
   * @template {Record<string, any> & { id: string }} T
   * @param {string} collection
   * @param {T[]} localArr
   * @param {T[]} remoteArr
   * @returns {T[]}
   */
  #mergeRecords(collection, localArr, remoteArr) {
    const tombstones = this.#getTombstones(collection);
    const knownIds = this.#getKnownIds(collection);
    const remoteIds = new Set(remoteArr.map((r) => r.id));

    /** @type {Map<string, T>} */
    const merged = new Map();

    // Start with local records
    for (const record of localArr) {
      if (tombstones.has(record.id)) continue;
      // If previously synced but now absent from remote, it was deleted remotely.
      if (knownIds.has(record.id) && !remoteIds.has(record.id)) continue;
      merged.set(record.id, record);
    }

    // Merge remote records
    for (const record of remoteArr) {
      if (tombstones.has(record.id)) continue;

      // If this id was previously known but is absent from local,
      // it was deleted locally — skip it. Only apply this heuristic when
      // localArr is non-empty; an empty localArr could mean the local cache
      // was cleared rather than the user deleting everything.
      if (
        localArr.length > 0 &&
        knownIds.has(record.id) &&
        !merged.has(record.id)
      ) continue;

      const existing = merged.get(record.id);

      if (!existing) {
        merged.set(record.id, record);
      } else {
        // Both sides have this record — pick by updatedAt
        const lt = existing.updatedAt;
        const rt = record.updatedAt;
        if (lt && rt && rt > lt) {
          merged.set(record.id, record);
        }
      }
    }

    return [...merged.values()];
  }

  /**
   * @param {Array<{ id: string, updatedAt?: string }>} merged
   * @param {Array<{ id: string, updatedAt?: string }>} remote
   * @returns {boolean}
   */
  #differFromRemote(merged, remote) {
    if (merged.length !== remote.length) return true;
    const remoteMap = new Map(remote.map((r) => [r.id, r.updatedAt]));
    return merged.some((r) => remoteMap.get(r.id) !== r.updatedAt);
  }

  // TOMBSTONES & KNOWN IDS

  /**
   * @param {string} collection
   * @returns {Set<string>}
   */
  #getTombstones(collection) {
    const raw = localStorage.getItem(
      `${STORAGE_PREFIX}/tombstones/${collection}`,
    );
    return raw ? new Set(JSON.parse(raw)) : new Set();
  }

  /**
   * @param {string} collection
   * @returns {Set<string>}
   */
  #getKnownIds(collection) {
    const raw = localStorage.getItem(`${STORAGE_PREFIX}/known/${collection}`);
    return raw ? new Set(JSON.parse(raw)) : new Set();
  }

  /**
   * Record all ids from a data array as known.
   *
   * @param {string} collection
   * @param {Array<{ id: string }>} data
   */
  #trackIds(collection, data) {
    const known = this.#getKnownIds(collection);
    for (const record of data) {
      known.add(record.id);
    }
    localStorage.setItem(
      `${STORAGE_PREFIX}/known/${collection}`,
      JSON.stringify([...known]),
    );
  }

  // REV & DIRTY FLAG

  /** @returns {string | null} */
  #getStoredRev() {
    return localStorage.getItem(`${STORAGE_PREFIX}/rev`);
  }

  /** @param {string | null} rev */
  #storeRev(rev) {
    if (rev) {
      localStorage.setItem(`${STORAGE_PREFIX}/rev`, rev);
    }
  }

  #markDirty() {
    localStorage.setItem(`${STORAGE_PREFIX}/dirty`, "1");
  }

  #clearDirty() {
    localStorage.removeItem(`${STORAGE_PREFIX}/dirty`);
  }

  /** @returns {boolean} */
  #isDirty() {
    return localStorage.getItem(`${STORAGE_PREFIX}/dirty`) === "1";
  }

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    // Broadcast if needed
    if (this.hasAttribute("group")) {
      this.broadcast(this.identifier, {});
    }

    super.connectedCallback();

    /** @type {OutputElement<any> | null} */
    const local = this.root().querySelector("dop-indexed-db");
    if (!local) throw new Error("Can't find local output");

    customElements.whenDefined(local.localName).then(() => {
      this.#localOutput.value = local;
    });
  }

  // RENDER

  /**
   * @param {RenderArg} _
   */
  render({ html }) {
    return html`
      <dop-indexed-db
        group="${ifDefined(this.getAttribute(`group`))}"
        namespace="${ifDefined(this.getAttribute(`namespace`))}"
      ></dop-indexed-db>
    `;
  }
}

export default ATProtoOutputSyncTransformer;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = ATProtoOutputSyncTransformer;
export const NAME = "dtor-atproto-sync";

defineElement(NAME, CLASS);
```
### Output / Refiner / Default

Removes output state that is not meant to be saved to storage, such as ephemeral tracks. Ideally part of every theme.

```js
import * as IDB from "idb-keyval";

import { computed, signal } from "~/common/signal.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";

const IDB_KEY_PLAYLISTS =
  "diffuse/transformer/output/refiner/default/playlistItems/ephemeral";
const IDB_KEY_TRACKS =
  "diffuse/transformer/output/refiner/default/tracks/ephemeral";

/**
 * @import { OutputManagerDeputy } from "@specs/components/output/types.d.ts"
 * @import { PlaylistItem, Track } from "~/definitions/types.d.ts"
 */

/**
 * @extends {OutputTransformer}
 */
class DefaultOutputRefinerTransformer extends OutputTransformer {
  constructor() {
    super();

    const base = this.base();

    // Restore stored ephemeral items
    IDB.get(IDB_KEY_PLAYLISTS).then((items) => {
      if (items) this.#ephemeralPlaylistItems.set(items);
    });

    IDB.get(IDB_KEY_TRACKS).then((items) => {
      if (items) this.#ephemeralTracks.set(items);
    });

    /** @type {OutputManagerDeputy} */
    const manager = {
      facets: {
        ...base.facets,
        collection: computed(() => {
          const col = base.facets.collection();
          if (col.state !== "loaded") return col;
          return { state: "loaded", data: col.data };
        }),
      },
      playlistItems: {
        ...base.playlistItems,
        collection: computed(() => {
          const col = base.playlistItems.collection();
          if (col.state !== "loaded") return col;
          return {
            state: "loaded",
            data: [...col.data, ...this.#ephemeralPlaylistItems.get()],
          };
        }),
        save: async (newPlaylists) => {
          /** @type {PlaylistItem[]} */
          const ephemeral = [];

          const filtered = newPlaylists.filter((p) => {
            if (p.ephemeral) {
              ephemeral.push(p);
              return false;
            } else {
              return true;
            }
          });

          await IDB.set(IDB_KEY_PLAYLISTS, ephemeral);
          this.#ephemeralPlaylistItems.set(ephemeral);

          await base.playlistItems.save(filtered);
        },
      },
      tracks: {
        ...base.tracks,
        collection: computed(() => {
          const col = base.tracks.collection();
          if (col.state !== "loaded") return col;
          return {
            state: "loaded",
            data: [...col.data, ...this.#ephemeralTracks.get()],
          };
        }),
        save: async (newTracks) => {
          /** @type {Track[]} */
          const ephemeral = [];

          const filtered = newTracks.filter((t) => {
            if (t.ephemeral) {
              ephemeral.push(t);
              return false;
            } else {
              return true;
            }
          });

          await IDB.set(IDB_KEY_TRACKS, ephemeral);
          this.#ephemeralTracks.set(ephemeral);

          await base.tracks.save(filtered);
        },
      },

      settings: base.settings,

      // Other
      ready: base.ready,
    };

    // Assign manager properties to class
    this.facets = manager.facets;
    this.playlistItems = manager.playlistItems;
    this.settings = manager.settings;
    this.tracks = manager.tracks;
    this.ready = manager.ready;
  }

  // SIGNALS

  #ephemeralPlaylistItems = signal(/** @type {PlaylistItem[]} */ ([]));
  #ephemeralTracks = signal(/** @type {Track[]} */ ([]));

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    if (this.hasAttribute("group")) {
      const actions = this.broadcast(IDB_KEY_TRACKS, {
        getEphemeralPlaylistItems: {
          strategy: "leaderOnly",
          fn: this.#ephemeralPlaylistItems.get,
        },
        setEphemeralPlaylistItems: {
          strategy: "replicate",
          fn: this.#ephemeralPlaylistItems.set,
        },
        getEphemeralTracks: {
          strategy: "leaderOnly",
          fn: this.#ephemeralTracks.get,
        },
        setEphemeralTracks: {
          strategy: "replicate",
          fn: this.#ephemeralTracks.set,
        },
      });

      if (actions) {
        this.#ephemeralPlaylistItems.set = actions.setEphemeralPlaylistItems;
        this.#ephemeralTracks.set = actions.setEphemeralTracks;

        actions.getEphemeralPlaylistItems().then((items) => {
          this.#ephemeralPlaylistItems.value = items;
        });

        actions.getEphemeralTracks().then((tracks) => {
          this.#ephemeralTracks.value = tracks;
        });
      }
    }

    super.connectedCallback();
  }
}

export default DefaultOutputRefinerTransformer;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = DefaultOutputRefinerTransformer;
export const NAME = "dtor-default";

defineElement(NAME, CLASS);
```
### Output / Refiner / Initial Contents

Sets the initial contents for an output on first load.

```js
import * as IDB from "idb-keyval";
import { xxh32r } from "xxh32/dist/raw.js";

import { batch, computed, signal, untracked } from "~/common/signal.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";

import {
  STARTING_SET_DISABLED,
  STARTING_SET_URIS,
  TYPE,
} from "~/common/facets/constants.js";
import facets from "~/_data/facets.json" with {
  type: "json",
};

/**
 * @import {OutputManagerDeputy} from "@specs/components/output/types.d.ts"
 */

const IDB_KEY =
  "diffuse/transformer/output/refiner/initial-contents/initialized";

////////////////////////////////////////////
// ELEMENT
////////////////////////////////////////////

/**
 * @extends {OutputTransformer}
 */
class InitialContentsTransformer extends OutputTransformer {
  static NAME = "diffuse/transformer/output/refiner/initial-contents";

  #flagLoaded = signal(false);
  #initialized = signal(false);

  constructor() {
    super();

    const base = this.base();

    // Load flag from IDB; gate collection() until resolved to prevent
    // a flash of defaults for a user who has previously cleared their collection.
    IDB.get(IDB_KEY).then((v) => {
      batch(() => {
        this.#initialized.value = !!v;
        this.#flagLoaded.value = true;
      });
    });

    /** @type {OutputManagerDeputy} */
    const manager = {
      facets: {
        ...base.facets,
        collection: computed(() => {
          if (!this.#flagLoaded.get()) return { state: "loading" };

          const col = base.facets.collection();
          if (col.state !== "loaded") return col;

          if (col.data.length > 0) {
            // Set the flag the first time we observe non-empty data.
            // Covers data arriving from another device via sync.
            // untracked read avoids a circular dependency; the write still
            // notifies subscribers and queues a re-evaluation.
            if (!untracked(() => this.#initialized.value)) {
              this.#initialized.value = true;
              IDB.set(IDB_KEY, true); // fire-and-forget
            }

            return { state: "loaded", data: col.data };
          }

          // Tracked read: keeps the computed subscribed to #initialized
          // so it re-runs if save() sets it to true with an empty array.
          if (this.#initialized.get()) {
            return { state: "loaded", data: col.data };
          }

          // Determine starting set
          const data = facets.flatMap((facet) => {
            if (STARTING_SET_URIS.includes(facet.url)) {
              return [{
                $type: TYPE,
                id: uriToRkey("diffuse://" + facet.url),
                description: facet.desc,
                enabled: STARTING_SET_DISABLED.includes(facet.url)
                  ? false
                  : true,
                kind: facet.kind === "prelude"
                  ? /** @type {const} */ ("prelude")
                  : /** @type {const} */ ("interactive"),
                name: facet.title,
                tags: facet.tags?.length ? facet.tags : undefined,
                uri: "diffuse://" + facet.url,
              }];
            }

            return [];
          });

          return { state: "loaded", data };
        }),

        save: async (newFacets) => {
          // Set the flag on any explicit save (covers the case where the
          // user's first action is removing a default from the list).
          if (!this.#initialized.value) {
            this.#initialized.value = true;
            IDB.set(IDB_KEY, true); // fire-and-forget
          }

          await base.facets.save(newFacets);
        },
      },

      playlistItems: base.playlistItems,
      settings: base.settings,
      tracks: base.tracks,
      ready: base.ready,
    };

    this.facets = manager.facets;
    this.playlistItems = manager.playlistItems;
    this.settings = manager.settings;
    this.tracks = manager.tracks;
    this.ready = manager.ready;
  }
}

export default InitialContentsTransformer;

/** @param {string} uri */
function uriToRkey(uri) {
  return xxh32r(new TextEncoder().encode(uri)).toString(16).padStart(8, "0");
}

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = InitialContentsTransformer;
export const NAME = "dtor-initial-contents";

defineElement(NAME, CLASS);
```
### Output / Refiner / Track URI Passkey

Encrypts track URIs using a passkey-derived PRF key. On read, decrypts encrypted:// URIs transparently; on write, re-encrypts all URIs before passing downstream.

```js
import { computed, signal } from "~/common/signal.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";

import {
  adoptPasskeyPrfResult,
  createPasskey,
  decryptUri,
  deriveCipherKey,
  encryptUri,
  isEncryptedUri,
  loadStoredCipherKey,
  removeStoredPasskey,
  storeCipherKey,
} from "./passkey.js";

/**
 * @import { Setting, Track } from "~/definitions/types.d.ts"
 * @import { OutputManager } from "@specs/components/output/types.d.ts"
 */

/**
 * Output transformer that encrypts track URIs and setting values using a
 * passkey-derived key.
 *
 * On read, decrypts `encrypted://` URIs in tracks and `encrypted://`-encoded
 * JSON in setting values transparently. On write, re-encrypts before passing
 * downstream.
 *
 * Tracks/settings that cannot be decrypted (no key in memory) are held in
 * `lockedTracks`/`lockedSettings` and excluded from the visible collection.
 *
 * @extends {OutputTransformer}
 */
class PasskeyEncryptionTransformer extends OutputTransformer {
  static NAME = "diffuse/transformer/output/refiner/passkey-encryption";

  #tracks;

  constructor() {
    super();

    const base = this.base();

    const encryptionKey = this.#encryptionKey;
    const lockedSettings = this.#lockedSettings;
    const lockedTracks = this.#lockedTracks;

    this.facets = base.facets;
    this.playlistItems = base.playlistItems;
    this.ready = this.#keyReady.get;

    // Settings
    /** @type {OutputManager["settings"]} */
    this.settings = {
      ...base.settings,

      collection: computed(() => {
        const col = base.settings.collection();
        if (col?.state !== "loaded") return { state: "loading" };

        const key = encryptionKey.get();

        /** @type {Setting[]} */
        const unlocked = [];

        /** @type {Setting[]} */
        const locked = [];

        for (const setting of col.data) {
          const value = setting.value;
          if (typeof value === "string" && isEncryptedUri(value)) {
            if (key) {
              try {
                unlocked.push({
                  ...setting,
                  value: decryptUri(key, value),
                });
              } catch {
                locked.push(setting);
              }
            } else {
              locked.push(setting);
            }
          } else {
            unlocked.push(setting);
          }
        }

        lockedSettings.set(locked);
        return { state: "loaded", data: unlocked };
      }),

      save: async (/** @type {Setting[]} */ newSettings) => {
        const key = encryptionKey.get();

        if (key) {
          newSettings = newSettings.map((setting) => ({
            ...setting,
            value: encryptUri(key, setting.value),
          }));

          // Re-append still-locked settings so they are not lost
          newSettings = newSettings.concat(lockedSettings.value);
        }

        await base.settings.save(newSettings);
      },
    };

    // Tracks
    this.#tracks = () => {
      const col = base.tracks.collection();

      if (col?.state !== "loaded") {
        return { state: "loading", locked: [], unlocked: [] };
      }

      const key = encryptionKey.get();

      /** @type {Track[]} */
      const unlocked = [];

      /** @type {Track[]} */
      const locked = [];

      for (const track of col.data) {
        if (!isEncryptedUri(track.uri)) {
          unlocked.push(track);
        } else if (key) {
          try {
            unlocked.push({ ...track, uri: decryptUri(key, track.uri) });
          } catch {
            locked.push(track);
          }
        } else {
          locked.push(track);
        }
      }

      return { state: "loaded", locked, unlocked };
    };

    /** @type {OutputManager["tracks"]} */
    this.tracks = {
      ...base.tracks,

      collection: computed(() => {
        const result = this.#tracks();
        if (result.state === "loading") return { state: "loading" };
        lockedTracks.set(result.locked);
        return { state: "loaded", data: result.unlocked };
      }),

      save: async (/** @type {Track[]} */ newTracks) => {
        const key = encryptionKey.get();

        if (key) {
          newTracks = newTracks.map((track) => ({
            ...track,
            uri: encryptUri(key, track.uri),
          }));

          // Re-append still-locked tracks so they are not lost
          newTracks = newTracks.concat(lockedTracks.value);
        }

        await base.tracks.save(newTracks);
      },
    };
  }

  // SIGNALS

  #encryptionKey = signal(/** @type {Uint8Array | null} */ (null));
  #keyReady = signal(false);
  #lockedSettings = signal(/** @type {Setting[]} */ ([]));
  #lockedTracks = signal(/** @type {Track[]} */ ([]));

  passkeyActive = computed(() => this.#encryptionKey.get() !== null);
  lockedSettings = this.#lockedSettings.get;
  lockedTracks = this.#lockedTracks.get;

  // LIFECYCLE

  /** @override */
  connectedCallback() {
    if (this.hasAttribute("group")) {
      const channelName = this.namespace?.length
        ? `${PasskeyEncryptionTransformer.NAME}/${this.namespace}/${this.group}`
        : `${PasskeyEncryptionTransformer.NAME}/${this.group}`;

      const actions = this.broadcast(channelName, {
        getLockedSettings: {
          strategy: "leaderOnly",
          fn: this.#lockedSettings.get,
        },
        setLockedSettings: {
          strategy: "replicate",
          fn: this.#lockedSettings.set,
        },
        getLockedTracks: {
          strategy: "leaderOnly",
          fn: this.#lockedTracks.get,
        },
        setLockedTracks: {
          strategy: "replicate",
          fn: this.#lockedTracks.set,
        },
      });

      if (actions) {
        this.#lockedSettings.set = actions.setLockedSettings;
        this.#lockedTracks.set = actions.setLockedTracks;

        actions.getLockedSettings().then((locked) => {
          this.#lockedSettings.value = locked;
        });

        actions.getLockedTracks().then((locked) => {
          this.#lockedTracks.value = locked;
        });
      }
    }

    super.connectedCallback();

    loadStoredCipherKey(this.namespace ?? "").then((key) => {
      if (key) {
        this.#encryptionKey.value = key;
      }

      this.#keyReady.value = true;
    });
  }

  // PASSKEY

  /**
   * Register a new passkey for track URI encryption.
   * Throws if the authenticator does not support the PRF extension.
   */
  async setupPasskey() {
    const namespace = this.namespace ?? "";
    const result = await createPasskey(namespace);

    if (!result.supported) {
      throw new Error(result.reason);
    }

    const key = await deriveCipherKey(result.prfSecond);
    await storeCipherKey(namespace, key);
    this.#encryptionKey.value = key;

    let savedSettings = false;
    let savedTracks = false;

    const stopSettings = this.effect(() => {
      if (savedSettings) { stopSettings(); return; }
      const col = this.settings.collection();
      if (col.state === "loading") return;
      savedSettings = true;
      this.settings.save(col.data);
    });

    const stopTracks = this.effect(() => {
      if (savedTracks) { stopTracks(); return; }
      const col = this.tracks.collection();
      if (col.state === "loading") return;
      savedTracks = true;
      this.tracks.save(col.data);
    });
  }

  /**
   * Adopt an existing passkey from another device via discoverable-credential
   * lookup. Stores the credential ID locally and derives the cipher key.
   */
  async adoptPasskey() {
    const namespace = this.namespace ?? "";
    const result = await adoptPasskeyPrfResult(namespace);

    if (!result.supported) {
      throw new Error(result.reason);
    }

    const key = await deriveCipherKey(result.prfSecond);
    await storeCipherKey(namespace, key);
    this.#encryptionKey.value = key;

    let savedSettings = false;
    let savedTracks = false;

    const stopSettings = this.effect(() => {
      if (savedSettings) { stopSettings(); return; }
      const col = this.settings.collection();
      if (col.state !== "loaded") return;
      savedSettings = true;
      this.settings.save(col.data);
    });

    const stopTracks = this.effect(() => {
      if (savedTracks) { stopTracks(); return; }
      const col = this.tracks.collection();
      if (col.state !== "loaded") return;
      savedTracks = true;
      this.tracks.save(col.data);
    });
  }

  /**
   * Remove the stored passkey credential and clear in-memory key material.
   */
  async removePasskey() {
    const namespace = this.namespace ?? "";
    await removeStoredPasskey(namespace);

    // Both collections must be captured in the same reactive snapshot before
    // clearing the key. If the key were cleared between the two reads, the
    // second collection would evaluate with key=null and show encrypted items
    // as locked (invisible), causing them to be silently dropped on save.
    let removed = false;

    const stop = this.effect(() => {
      if (removed) { stop(); return; }

      const settingsCol = this.settings.collection();
      const tracksCol = this.tracks.collection();

      if (settingsCol.state !== "loaded" || tracksCol.state !== "loaded") return;

      removed = true;

      this.#encryptionKey.value = null;

      this.settings.save(settingsCol.data);
      this.tracks.save(tracksCol.data);
    });
  }
}

export default PasskeyEncryptionTransformer;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = PasskeyEncryptionTransformer;
export const NAME = "dtor-passkey-encryption";

defineElement(NAME, CLASS);
```
### Output / String / JSON

Raw data schema output to and from JSON UTF8 string.

```js
import { computed } from "~/common/signal.js";
import { OutputTransformer } from "../../base.js";
import { defineElement } from "~/common/element.js";

/**
 * @import { OutputManagerDeputy } from "@specs/components/output/types.d.ts"
 */

/**
 * @extends {OutputTransformer<string>}
 */
class JsonStringOutputTransformer extends OutputTransformer {
  constructor() {
    super();

    const base = this.base();

    /** @type {OutputManagerDeputy} */
    const manager = {
      facets: {
        ...base.facets,
        collection: computed(() => {
          const col = base.facets.collection();
          if (col.state !== "loaded") return col;
          return {
            state: "loaded",
            data: typeof col.data === "string" ? parseArray(col.data) : [],
          };
        }),
        save: async (newFacets) => {
          const json = JSON.stringify(newFacets);
          await base.facets.save(json);
        },
      },
      playlistItems: {
        ...base.playlistItems,
        collection: computed(() => {
          const col = base.playlistItems.collection();
          if (col.state !== "loaded") return col;
          return {
            state: "loaded",
            data: typeof col.data === "string" ? parseArray(col.data) : [],
          };
        }),
        save: async (newPlaylistItems) => {
          const json = JSON.stringify(newPlaylistItems);
          await base.playlistItems.save(json);
        },
      },
      settings: {
        ...base.settings,
        collection: computed(() => {
          const col = base.settings.collection();
          if (col.state !== "loaded") return col;
          return {
            state: "loaded",
            data: typeof col.data === "string" ? parseArray(col.data) : [],
          };
        }),
        save: async (newSettings) => {
          const json = JSON.stringify(newSettings);
          await base.settings.save(json);
        },
      },
      tracks: {
        ...base.tracks,
        collection: computed(() => {
          const col = base.tracks.collection();
          if (col.state !== "loaded") return col;
          return {
            state: "loaded",
            data: typeof col.data === "string" ? parseArray(col.data) : [],
          };
        }),
        save: async (newTracks) => {
          const json = JSON.stringify(newTracks);
          await base.tracks.save(json);
        },
      },

      // Other
      ready: base.ready,
    };

    // Assign manager properties to class
    this.facets = manager.facets;
    this.playlistItems = manager.playlistItems;
    this.settings = manager.settings;
    this.tracks = manager.tracks;
    this.ready = manager.ready;
  }
}

/**
 * @param {string} json
 */
function parseArray(json) {
  try {
    return JSON.parse(json);
  } catch (err) {
    console.error(err);
    return [];
  }
}

export default JsonStringOutputTransformer;

////////////////////////////////////////////
// REGISTER
////////////////////////////////////////////

export const CLASS = JsonStringOutputTransformer;
export const NAME = "dtos-json";

defineElement(NAME, CLASS);
```
