From 713fba62831049b33d50ea83d3096540430fc9a6 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 12 Oct 2026 21:43:15 +0200 Subject: [PATCH] feat: path-collections facet + path-collections component rework --- deno.jsonc | 2 +- src/_data/elements.js | 13 +- src/_data/facets.json | 7 + src/common/foundation.js | 26 --- src/components/index.js | 4 +- src/components/transformer/output/base.js | 28 +++ .../refiner}/path-collections/element.js | 51 +++--- .../data/path-playlists/prelude/facet.js | 44 +++++ .../data/path-playlists/prelude/index.html | 5 + src/facets/themes/ipod/facet/facet.js | 5 +- .../output/refiner}/path-collections/test.ts | 160 ++++++++++++++++-- 11 files changed, 271 insertions(+), 74 deletions(-) rename src/components/{orchestrator => transformer/output/refiner}/path-collections/element.js (70%) create mode 100644 src/facets/data/path-playlists/prelude/facet.js create mode 100644 src/facets/data/path-playlists/prelude/index.html rename tests/components/{orchestrator => transformer/output/refiner}/path-collections/test.ts (54%) diff --git a/deno.jsonc b/deno.jsonc index 94db4387..a5f02c05 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -173,7 +173,6 @@ "./components/orchestrator/favourites/element.js": "./src/components/orchestrator/favourites/element.js", "./components/orchestrator/media-session/element.js": "./src/components/orchestrator/media-session/element.js", "./components/orchestrator/output/element.js": "./src/components/orchestrator/output/element.js", - "./components/orchestrator/path-collections/element.js": "./src/components/orchestrator/path-collections/element.js", "./components/orchestrator/process-tracks/element.js": "./src/components/orchestrator/process-tracks/element.js", "./components/orchestrator/process-tracks/worker.js": "./src/components/orchestrator/process-tracks/worker.js", "./components/orchestrator/queue-audio/element.js": "./src/components/orchestrator/queue-audio/element.js", @@ -211,6 +210,7 @@ "./components/transformer/output/refiner/default/element.js": "./src/components/transformer/output/refiner/default/element.js", "./components/transformer/output/refiner/initial-contents/element.js": "./src/components/transformer/output/refiner/initial-contents/element.js", "./components/transformer/output/refiner/passkey-encryption/element.js": "./src/components/transformer/output/refiner/passkey-encryption/element.js", + "./components/transformer/output/refiner/path-collections/element.js": "./src/components/transformer/output/refiner/path-collections/element.js", "./components/transformer/output/string/json/element.js": "./src/components/transformer/output/string/json/element.js", "./components/upload/dropbox/common.js": "./src/components/upload/dropbox/common.js", "./components/upload/dropbox/element.js": "./src/components/upload/dropbox/element.js", diff --git a/src/_data/elements.js b/src/_data/elements.js index 2af3b8c3..21f03a1f 100644 --- a/src/_data/elements.js +++ b/src/_data/elements.js @@ -213,12 +213,6 @@ export default { desc: "A default output configuration. Contains all the outputs provided here along with the relevant transformers.", }, - { - url: "components/orchestrator/path-collections/element.js", - title: "Path Collections", - desc: - "Wraps an output element to generate ephemeral playlists based on a path part of each track's URI. The 1-based part can be set using the `path-part` attribute (defaults to the first). Ephemeral items are excluded from storage.", - }, { url: "components/orchestrator/process-tracks/element.js", title: "Process Inputs Into Tracks", @@ -355,6 +349,13 @@ export default { desc: "Encrypts track URIs using a passkey-derived PRF key. On read, decrypts encrypted:// URIs transparently; on write, re-encrypts all URIs before passing downstream.", }, + { + url: + "components/transformer/output/refiner/path-collections/element.js", + title: "Output / Refiner / Path Collections", + desc: + "Generates ephemeral playlists based on a path part of each track's URI. The 1-based part can be set using the `path-part` attribute (defaults to the first). Percent-encoded parts are decoded. Items with an id prefixed `path-collections/` are excluded from storage.", + }, { url: "components/transformer/output/string/json/element.js", title: "Output / String / JSON", diff --git a/src/_data/facets.json b/src/_data/facets.json index ea47a7cb..e61d3c9c 100644 --- a/src/_data/facets.json +++ b/src/_data/facets.json @@ -220,6 +220,13 @@ "author": "A theme by [@tokono.ma](https://bsky.app/profile/tokono.ma)", "desc": "A recreation of the click-wheel iPod UI. Spin the wheel to scroll, tap the ring for menu / prev / next / play, and press the centre button to select. Alternatively, use your keyboard arrow keys." }, + { + "url": "facets/data/path-playlists/prelude/index.tile", + "title": "Path Playlists", + "kind": "prelude", + "category": "Data", + "desc": "Automatically generate playlists from the folder structure of your audio sources. Each unique first path part of a track's URI becomes a playlist containing its tracks. These playlists are generated on the fly and are not stored. Edit the `PATH_PART` code to change which path part to use as the unique playlist name." + }, { "url": "facets/data/playlists/index.tile", "title": "Playlists", diff --git a/src/common/foundation.js b/src/common/foundation.js index d191b8b3..b308cad3 100644 --- a/src/common/foundation.js +++ b/src/common/foundation.js @@ -68,9 +68,6 @@ const signals = { output: signal( /** @type {import("~/components/orchestrator/output/element.js").CLASS | null} */ (null), ), - pathCollections: signal( - /** @type {import("~/components/orchestrator/path-collections/element.js").CLASS | null} */ (null), - ), processTracks: signal( /** @type {import("~/components/orchestrator/process-tracks/element.js").CLASS | null} */ (null), ), @@ -122,7 +119,6 @@ export const config = { favourites, mediaSession, output, - pathCollections, processTracks, queueAudio, scopedTracks, @@ -158,7 +154,6 @@ export const config = { favourites: signals.orchestrator.favourites.get, mediaSession: signals.orchestrator.mediaSession.get, output: signals.orchestrator.output.get, - pathCollections: signals.orchestrator.pathCollections.get, processTracks: signals.orchestrator.processTracks.get, queueAudio: signals.orchestrator.queueAudio.get, scopedTracks: signals.orchestrator.scopedTracks.get, @@ -548,27 +543,6 @@ async function spectrogramAudio() { return findExistingOrAdd(sao, signals.orchestrator.spectrogramAudio); } -/** - * @param {Object} [options] - Options - * @param {number} [options.pathPart] - 1-based path part of each track's URI to group playlists by. Defaults to 1. - */ -async function pathCollections(options) { - const [{ CLASS: PathCollectionsOrchestrator }, o] = await Promise.all([ - import("~/components/orchestrator/path-collections/element.js"), - output(), - ]); - - const pco = new PathCollectionsOrchestrator(); - pco.setAttribute("group", GROUP); - pco.setAttribute("output-selector", o.selector); - - if (options?.pathPart) { - pco.setAttribute("path-part", String(options.pathPart)); - } - - return findExistingOrAdd(pco, signals.orchestrator.pathCollections); -} - async function sources() { const [{ CLASS: SourcesOrchestrator }, i, o] = await Promise.all([ import("~/components/orchestrator/sources/element.js"), diff --git a/src/components/index.js b/src/components/index.js index ff2649a2..e69ea0f0 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -29,7 +29,6 @@ import * as _OrchestratorCoverGroups from "./orchestrator/cover-groups/element.j import * as _OrchestratorFavourites from "./orchestrator/favourites/element.js" import * as _OrchestratorMediaSession from "./orchestrator/media-session/element.js" import * as _OrchestratorOutput from "./orchestrator/output/element.js" -import * as _OrchestratorPathCollections from "./orchestrator/path-collections/element.js" import * as _OrchestratorProcessTracks from "./orchestrator/process-tracks/element.js" import * as _OrchestratorQueueAudio from "./orchestrator/queue-audio/element.js" import * as _OrchestratorScopedTracks from "./orchestrator/scoped-tracks/element.js" @@ -48,6 +47,7 @@ import * as _TransformerOutputRawAtprotoSync from "./transformer/output/raw/atpr import * as _TransformerOutputRefinerDefault from "./transformer/output/refiner/default/element.js" import * as _TransformerOutputRefinerInitialContents from "./transformer/output/refiner/initial-contents/element.js" import * as _TransformerOutputRefinerPasskeyEncryption from "./transformer/output/refiner/passkey-encryption/element.js" +import * as _TransformerOutputRefinerPathCollections from "./transformer/output/refiner/path-collections/element.js" import * as _TransformerOutputStringJson from "./transformer/output/string/json/element.js" import * as _UploadDropbox from "./upload/dropbox/element.js" @@ -98,7 +98,6 @@ export const orchestrator = { favourites: _OrchestratorFavourites, mediaSession: _OrchestratorMediaSession, output: _OrchestratorOutput, - pathCollections: _OrchestratorPathCollections, processTracks: _OrchestratorProcessTracks, queueAudio: _OrchestratorQueueAudio, scopedTracks: _OrchestratorScopedTracks, @@ -138,6 +137,7 @@ export const transformer = { default: _TransformerOutputRefinerDefault, initialContents: _TransformerOutputRefinerInitialContents, passkeyEncryption: _TransformerOutputRefinerPasskeyEncryption, + pathCollections: _TransformerOutputRefinerPathCollections, }, string: { json: _TransformerOutputStringJson, diff --git a/src/components/transformer/output/base.js b/src/components/transformer/output/base.js index 0341be38..a5677d08 100644 --- a/src/components/transformer/output/base.js +++ b/src/components/transformer/output/base.js @@ -9,6 +9,8 @@ import { computed, signal } from "~/common/signal.js"; * @template [T=null] */ export class OutputTransformer extends BroadcastableDiffuseElement { + static observedAttributes = ["output-selector"]; + // SIGNALS #output = signal(/** @type {OutputElement | undefined} */ (undefined)); @@ -37,6 +39,32 @@ export class OutputTransformer extends BroadcastableDiffuseElement { }); } + /** + * @override + * @param {string} name + * @param {string} oldValue + * @param {string} newValue + */ + attributeChangedCallback(name, oldValue, newValue) { + super.attributeChangedCallback(name, oldValue, newValue); + + // Re-resolve the output when rewired at runtime (eg. when a transformer + // is inserted into an existing output chain). Before connecting, the + // initial query is handled by `connectedCallback`. + if ( + name === "output-selector" && this.isConnected && + oldValue !== newValue + ) { + /** @type {OutputElement} */ + const output = query(this, "output-selector"); + + customElements.whenDefined(output.localName).then(() => { + this.#output.value = output; + this.#outputWhenDefined.resolve(null); + }); + } + } + // MANAGER base() { diff --git a/src/components/orchestrator/path-collections/element.js b/src/components/transformer/output/refiner/path-collections/element.js similarity index 70% rename from src/components/orchestrator/path-collections/element.js rename to src/components/transformer/output/refiner/path-collections/element.js index 4ec8e3bd..5ee35079 100644 --- a/src/components/orchestrator/path-collections/element.js +++ b/src/components/transformer/output/refiner/path-collections/element.js @@ -1,19 +1,33 @@ import { computed, signal } from "~/common/signal.js"; -import { OutputTransformer } from "~/components/transformer/output/base.js"; +import { OutputTransformer } from "../../base.js"; import { defineElement } from "~/common/element.js"; /** * @import {PlaylistItem, Track} from "~/definitions/types.d.ts" */ +//////////////////////////////////////////// +// CONSTANTS +//////////////////////////////////////////// + +// Id prefix of the ephemeral items this transformer generates. Items with +// an id starting with this prefix are excluded from storage on save. +const ID_PREFIX = "path-collections"; + //////////////////////////////////////////// // ELEMENT //////////////////////////////////////////// -class PathCollectionsOrchestrator extends OutputTransformer { - static NAME = "diffuse/orchestrator/path-collections"; +/** + * @extends {OutputTransformer} + */ +class PathCollectionsTransformer extends OutputTransformer { + static NAME = "diffuse/transformer/output/refiner/path-collections"; - static observedAttributes = ["path-part"]; + /** + * @override + */ + static observedAttributes = ["output-selector", "path-part"]; /** 1-based path part of each track's URI to group playlists by. Defaults to 1. */ #pathPart = signal(1); @@ -43,7 +57,7 @@ class PathCollectionsOrchestrator extends OutputTransformer { /** @type {(typeof base.playlistItems.save)} */ save: async (items) => { await base.playlistItems.save( - (items || []).filter((p) => !p.ephemeral), + (items || []).filter((p) => !p.id.startsWith(`${ID_PREFIX}/`)), ); }, }; @@ -66,6 +80,12 @@ class PathCollectionsOrchestrator extends OutputTransformer { } /** + * One ephemeral playlist item per track: the playlist is named after the + * track's nth path part, and the item matches exactly that track (criteria + * only support exact equality). Grouping the items by their `playlist` name + * (see `Playlist.gather`) then yields one playlist per path part containing + * all of its tracks. + * * @param {Track[]} tracks * @param {number} pathPart * @returns {PlaylistItem[]} @@ -74,26 +94,17 @@ function createEphemeralItems(tracks, pathPart) { /** @type {PlaylistItem[]} */ const items = []; - /** @type {Set} */ - const segmentsAdded = new Set(); - tracks.forEach((track) => { const segment = pathSegment(track.uri, pathPart); if (!segment) return; - if (segmentsAdded.has(segment)) return; - - /** @type {PlaylistItem} */ - const item = { + items.push({ $type: "sh.diffuse.output.playlistItem", - id: `path-collections/${track.id}`, + id: `${ID_PREFIX}/${track.id}`, playlist: segment, criteria: [{ field: "uri", value: /** @type {any} */ (track.uri) }], ephemeral: true, - }; - - items.push(item); - segmentsAdded.add(segment); + }); }); return items; @@ -130,13 +141,13 @@ function parsePathPart(value) { return Number.isNaN(parsed) || parsed < 1 ? 1 : parsed; } -export default PathCollectionsOrchestrator; +export default PathCollectionsTransformer; //////////////////////////////////////////// // REGISTER //////////////////////////////////////////// -export const CLASS = PathCollectionsOrchestrator; -export const NAME = "do-path-collections"; +export const CLASS = PathCollectionsTransformer; +export const NAME = "dtor-path-collections"; defineElement(NAME, CLASS); diff --git a/src/facets/data/path-playlists/prelude/facet.js b/src/facets/data/path-playlists/prelude/facet.js new file mode 100644 index 00000000..423101bc --- /dev/null +++ b/src/facets/data/path-playlists/prelude/facet.js @@ -0,0 +1,44 @@ +import foundation from "~/common/foundation.js"; +import { + CLASS as PathCollectionsTransformer, +} from "~/components/transformer/output/refiner/path-collections/element.js"; + +/** + * @param {Object} [args] - Options + * @param {number} [args.pathPart=1] - 1-based path part of each track's URI to group playlists by. + */ +export async function setup({ pathPart = 1 } = {}) { + // The output orchestrator. On loader pages this returns the element the + // loader already created. + const output = await foundation.orchestrator.output(); + + await customElements.whenDefined(output.localName); + + // Insert the path collections refiner into the orchestrator's internal + // output chain, so every consumer of the orchestrator sees the generated + // collections. + let transformer = output.querySelector("dtor-path-collections"); + + if (!transformer) { + transformer = new PathCollectionsTransformer(); + transformer.id = "do-output__path-collections"; + transformer.setAttribute("group", foundation.GROUP); + transformer.setAttribute("path-part", String(pathPart)); + + // The entry point of the orchestrator's internal output chain. The + // transformer takes over its `output-selector`, so the chain becomes: + // entry → transformer → previous target. + const entry = output.output; + const entrySelector = entry.getAttribute("output-selector"); + + if (!entrySelector) { + throw new Error("Output entry point is missing its output-selector"); + } + + transformer.setAttribute("output-selector", entrySelector); + entry.after(transformer); + entry.setAttribute("output-selector", `#${transformer.id}`); + } + + await customElements.whenDefined(transformer.localName); +} diff --git a/src/facets/data/path-playlists/prelude/index.html b/src/facets/data/path-playlists/prelude/index.html new file mode 100644 index 00000000..2bc9f658 --- /dev/null +++ b/src/facets/data/path-playlists/prelude/index.html @@ -0,0 +1,5 @@ + diff --git a/src/facets/themes/ipod/facet/facet.js b/src/facets/themes/ipod/facet/facet.js index b63394a1..56653328 100644 --- a/src/facets/themes/ipod/facet/facet.js +++ b/src/facets/themes/ipod/facet/facet.js @@ -27,7 +27,6 @@ await Promise.all([ const scopedTracks = await foundation.orchestrator.scopedTracks(); const output = await foundation.orchestrator.output(); -const pathCollections = await foundation.orchestrator.pathCollections(); await foundation.orchestrator.sources(); await foundation.orchestrator.processTracks({ disableWhenReady: true }); await foundation.orchestrator.queueAudio(); @@ -464,7 +463,7 @@ const songsMenu = () => { /** @returns {MenuFrame} */ const playlistsMenu = () => { const $items = computed(() => { - const col = pathCollections.playlistItems.collection(); + const col = output.playlistItems.collection(); const items = col.state === "loaded" ? col.data : []; const map = Playlist.gather(items); const names = sortNames([...map.values()].map((p) => p.name)); @@ -489,7 +488,7 @@ const playlistsMenu = () => { */ const playlistMenu = (name) => { const $items = computed(() => { - const col = pathCollections.playlistItems.collection(); + const col = output.playlistItems.collection(); const allItems = col.state === "loaded" ? col.data : []; const playlistItems = allItems.filter((item) => item.playlist === name); const ordered = Playlist.sort(playlistItems); diff --git a/tests/components/orchestrator/path-collections/test.ts b/tests/components/transformer/output/refiner/path-collections/test.ts similarity index 54% rename from tests/components/orchestrator/path-collections/test.ts rename to tests/components/transformer/output/refiner/path-collections/test.ts index cef80330..c33839d9 100644 --- a/tests/components/orchestrator/path-collections/test.ts +++ b/tests/components/transformer/output/refiner/path-collections/test.ts @@ -3,13 +3,13 @@ import { expect } from "@std/expect"; import { testWeb } from "@tests/common/index.ts"; -describe("components/orchestrator/path-collections", () => { +describe("components/transformer/output/refiner/path-collections", () => { // Disconnected defaults it("ready returns false when no output is connected", async () => { const result = await testWeb(async () => { const mod = await import( - "~/components/orchestrator/path-collections/element.js" + "~/components/transformer/output/refiner/path-collections/element.js" ); const t = new mod.CLASS(); return t.ready(); @@ -21,7 +21,7 @@ describe("components/orchestrator/path-collections", () => { it("playlistItems.collection is loading when no output is connected", async () => { const result = await testWeb(async () => { const mod = await import( - "~/components/orchestrator/path-collections/element.js" + "~/components/transformer/output/refiner/path-collections/element.js" ); const t = new mod.CLASS(); return t.playlistItems.collection().state; @@ -32,13 +32,13 @@ describe("components/orchestrator/path-collections", () => { // Ephemeral item generation - it("generates one ephemeral item per unique path segment from loaded tracks", async () => { + it("generates one ephemeral item per track from loaded tracks", async () => { const result = await testWeb(async () => { const idbMod = await import( "~/components/output/polymorphic/indexed-db/element.js" ); const mod = await import( - "~/components/orchestrator/path-collections/element.js" + "~/components/transformer/output/refiner/path-collections/element.js" ); const output = new idbMod.CLASS(); @@ -75,13 +75,13 @@ describe("components/orchestrator/path-collections", () => { expect(result).toEqual(["music", "podcasts"]); }); - it("deduplicates tracks that share the same path segment", async () => { + it("groups tracks that share the same path part into one playlist", async () => { const result = await testWeb(async () => { const idbMod = await import( "~/components/output/polymorphic/indexed-db/element.js" ); const mod = await import( - "~/components/orchestrator/path-collections/element.js" + "~/components/transformer/output/refiner/path-collections/element.js" ); const output = new idbMod.CLASS(); @@ -108,12 +108,15 @@ describe("components/orchestrator/path-collections", () => { const col = t.playlistItems.collection(); if (col.state !== "loaded") return null; - return (col.data as Array<{ ephemeral?: boolean; playlist: string }>) + return (col.data as Array<{ ephemeral?: boolean; id: string; playlist: string }>) .filter((p) => p.ephemeral) - .map((p) => p.playlist); + .map((p) => [p.id, p.playlist]); }); - expect(result).toEqual(["music"]); + expect(result).toEqual([ + ["path-collections/t1", "music"], + ["path-collections/t2", "music"], + ]); }); it("supports grouping by a configurable path part, percent-decoded", async () => { @@ -122,7 +125,7 @@ describe("components/orchestrator/path-collections", () => { "~/components/output/polymorphic/indexed-db/element.js" ); const mod = await import( - "~/components/orchestrator/path-collections/element.js" + "~/components/transformer/output/refiner/path-collections/element.js" ); const output = new idbMod.CLASS(); @@ -171,7 +174,7 @@ describe("components/orchestrator/path-collections", () => { "~/components/output/polymorphic/indexed-db/element.js" ); const mod = await import( - "~/components/orchestrator/path-collections/element.js" + "~/components/transformer/output/refiner/path-collections/element.js" ); const output = new idbMod.CLASS(); @@ -202,15 +205,127 @@ describe("components/orchestrator/path-collections", () => { expect(result).toBe(0); }); + // Runtime rewiring + + it("re-resolves its output when the output-selector is rewired at runtime", async () => { + const result = await testWeb(async () => { + const idbMod = await import( + "~/components/output/polymorphic/indexed-db/element.js" + ); + const mod = await import( + "~/components/transformer/output/refiner/path-collections/element.js" + ); + + const outputA = new idbMod.CLASS(); + outputA.id = "test-idb-rewire-a"; + document.body.append(outputA); + + const outputB = new idbMod.CLASS(); + outputB.id = "test-idb-rewire-b"; + document.body.append(outputB); + + const t = new mod.CLASS(); + t.setAttribute("output-selector", "#test-idb-rewire-a"); + document.body.append(t); + + await outputA.playlistItems.save([]); + await outputB.playlistItems.save([]); + + await t.tracks.save([ + { + $type: "sh.diffuse.output.track", + id: "t1", + uri: "https://example.com/music/x.mp3", + }, + ]); + + const before = t.playlistItems.collection(); + if (before.state !== "loaded") return null; + + t.setAttribute("output-selector", "#test-idb-rewire-b"); + await new Promise((r) => setTimeout(r, 100)); + + const after = t.playlistItems.collection(); + if (after.state !== "loaded") return null; + + return { countBefore: before.data.length, countAfter: after.data.length }; + }); + + expect(result?.countBefore).toBe(1); + expect(result?.countAfter).toBe(0); + }); + + // Chain insertion (as done by the "path playlists" prelude) + + it("can be inserted into the output orchestrator's chain manually", async () => { + const result = await testWeb(async () => { + const { CLASS } = await import( + "~/components/transformer/output/refiner/path-collections/element.js" + ); + const { CLASS: OutputOrchestrator } = await import( + "~/components/orchestrator/output/element.js" + ); + + const orch = new OutputOrchestrator(); + document.body.append(orch); + await customElements.whenDefined("dc-output"); + await customElements.whenDefined("dtor-path-collections"); + + const entry = orch.output; + const entrySelector = entry.getAttribute("output-selector"); + if (!entrySelector) return null; + + const transformer = new CLASS(); + transformer.id = "do-output__path-collections"; + transformer.setAttribute("path-part", "2"); + transformer.setAttribute("output-selector", entrySelector); + entry.after(transformer); + entry.setAttribute("output-selector", `#${transformer.id}`); + + await orch.tracks.save([ + { + $type: "sh.diffuse.output.track", + id: "t1", + uri: + "https://example.com/Primary/techno%20.%20minimal%20.%20acid/album/track.mp3", + }, + ]); + // Force the playlistItems collection to load before reading it back. + await orch.playlistItems.save([]); + await new Promise((r) => setTimeout(r, 100)); + + const col = orch.playlistItems.collection(); + const ephemeral = col.state === "loaded" + ? (col.data as Array<{ ephemeral?: boolean; playlist: string }>) + .filter((p) => p.ephemeral) + .map((p) => p.playlist) + : null; + + return { + entrySelector: entry.getAttribute("output-selector"), + transformerSelector: transformer.getAttribute("output-selector"), + insertedAfterEntry: entry.nextElementSibling === transformer, + ephemeral, + }; + }); + + expect(result?.entrySelector).toBe("#do-output__path-collections"); + expect(result?.transformerSelector).toBe( + "#do-output__dtor-initial-contents", + ); + expect(result?.insertedAfterEntry).toBe(true); + expect(result?.ephemeral).toEqual(["techno . minimal . acid"]); + }); + // save filters ephemeral items - it("save strips ephemeral items before writing to the backing output", async () => { + it("save strips items with the path-collections id prefix before writing to the backing output", async () => { const result = await testWeb(async () => { const idbMod = await import( "~/components/output/polymorphic/indexed-db/element.js" ); const mod = await import( - "~/components/orchestrator/path-collections/element.js" + "~/components/transformer/output/refiner/path-collections/element.js" ); const output = new idbMod.CLASS(); @@ -230,11 +345,24 @@ describe("components/orchestrator/path-collections", () => { }, { $type: "sh.diffuse.output.playlistItem", - id: "dropped", + id: "kept-ephemeral", + playlist: "Other", + criteria: [], + ephemeral: true, + }, + { + $type: "sh.diffuse.output.playlistItem", + id: "path-collections/t1", playlist: "Generated", criteria: [], ephemeral: true, }, + { + $type: "sh.diffuse.output.playlistItem", + id: "path-collections/t2", + playlist: "Generated", + criteria: [], + }, ]); const backingCol = output.playlistItems.collection(); @@ -242,6 +370,6 @@ describe("components/orchestrator/path-collections", () => { return (backingCol.data as Array<{ id: string }>).map((p) => p.id); }); - expect(result).toEqual(["kept"]); + expect(result).toEqual(["kept", "kept-ephemeral"]); }); }); -- 2.54.0