forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import { BroadcastableDiffuseElement } from "~/common/element.js";
2import { batch, computed, signal, untracked } from "~/common/signal.js";
3import { strictEquality } from "~/common/compare.js";
4
5/**
6 * @import {Facet, PlaylistItem, Setting, Track} from "~/definitions/types.d.ts"
7 * @import {SignalWriter} from "~/common/signal.d.ts";
8 * @import {OutputManager, OutputManagerProperties} from "./types.d.ts"
9 */
10
11export class BroadcastedOutputElement extends BroadcastableDiffuseElement {
12 /**
13 * @param {OutputManager<any>} manager
14 */
15 replicateSavedData(manager) {
16 // Broadcast if needed
17 if (!this.hasAttribute("group")) return;
18
19 /**
20 * @template T
21 * @param {{ save: (data: T) => Promise<void>; set: SignalWriter<T> }} _
22 * @returns {(data: T) => Promise<void>}
23 */
24 const fn = ({ save, set }) => async (data) => {
25 await untracked(async () => {
26 if (await this.isLeader()) {
27 return save(data);
28 } else {
29 return set(data);
30 }
31 });
32 };
33
34 const ogFacetsSave = manager.facets.save.bind(this);
35 const ogPlaylistItemsSave = manager.playlistItems.save.bind(this);
36 const ogSettingsSave = manager.settings.save.bind(this);
37 const ogTracksSave = manager.tracks.save.bind(this);
38
39 const actions = this.broadcast(this.identifier, {
40 saveFacets: {
41 strategy: "replicate",
42 fn: fn({ save: ogFacetsSave, set: manager.signals.facets.set }),
43 },
44 savePlaylistItems: {
45 strategy: "replicate",
46 fn: fn({
47 save: ogPlaylistItemsSave,
48 set: manager.signals.playlistItems.set,
49 }),
50 },
51 saveSettings: {
52 strategy: "replicate",
53 fn: fn({ save: ogSettingsSave, set: manager.signals.settings.set }),
54 },
55 saveTracks: {
56 strategy: "replicate",
57 fn: fn({ save: ogTracksSave, set: manager.signals.tracks.set }),
58 },
59 });
60
61 if (actions) {
62 manager.facets.save = actions.saveFacets;
63 manager.playlistItems.save = actions.savePlaylistItems;
64 manager.settings.save = actions.saveSettings;
65 manager.tracks.save = actions.saveTracks;
66 }
67 }
68}
69
70/**
71 * @template [Encoding=null]
72 * @param {OutputManagerProperties<Encoding>} _
73 * @returns {OutputManager<Encoding>}
74 */
75export function outputManager(
76 { init, facets, playlistItems, settings, tracks },
77) {
78 const c = signal(
79 /** @type {Encoding extends null ? Facet[] : Encoding} */ (facets
80 .empty()),
81 );
82 const cs = signal(
83 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"),
84 { compare: strictEquality },
85 );
86
87 const pl = signal(
88 /** @type {Encoding extends null ? PlaylistItem[] : Encoding} */ (playlistItems
89 .empty()),
90 );
91 const pls = signal(
92 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"),
93 { compare: strictEquality },
94 );
95
96 const s = signal(
97 /** @type {Encoding extends null ? Setting[] : Encoding} */ (settings
98 .empty()),
99 );
100 const ss = signal(
101 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"),
102 { compare: strictEquality },
103 );
104
105 const t = signal(
106 /** @type {Encoding extends null ? Track[] : Encoding} */ (tracks.empty()),
107 );
108 const ts = signal(
109 /** @type {"loading" | "loaded" | "sleeping"} */ ("sleeping"),
110 { compare: strictEquality },
111 );
112
113 async function loadFacets() {
114 if (init && (await init()) === false) return;
115 cs.value = "loading";
116 c.value = await facets.get();
117 cs.value = "loaded";
118 }
119
120 async function loadPlaylistItems() {
121 if (init && (await init()) === false) return;
122 pls.value = "loading";
123 pl.value = await playlistItems.get();
124 pls.value = "loaded";
125 }
126
127 async function loadSettings() {
128 if (init && (await init()) === false) return;
129 ss.value = "loading";
130 s.value = await settings.get();
131 ss.value = "loaded";
132 }
133
134 async function loadTracks() {
135 if (init && (await init()) === false) return;
136 ts.value = "loading";
137 t.value = await tracks.get();
138 ts.value = "loaded";
139 }
140
141 return {
142 facets: {
143 collection: computed(() => {
144 if (untracked(() => cs.value === "sleeping")) loadFacets();
145 return cs.value === "loaded"
146 ? { state: "loaded", data: c.value }
147 : { state: "loading" };
148 }),
149 reload: loadFacets,
150 save: async (newFacets) => {
151 batch(() => {
152 if (untracked(() => cs.value === "sleeping")) cs.value = "loaded";
153 c.value = newFacets;
154 });
155 await facets.put(newFacets);
156 },
157 },
158 playlistItems: {
159 collection: computed(() => {
160 if (untracked(() => pls.value === "sleeping")) loadPlaylistItems();
161 return pls.value === "loaded"
162 ? { state: "loaded", data: pl.value }
163 : { state: "loading" };
164 }),
165 reload: loadPlaylistItems,
166 save: async (newPlaylistItems) => {
167 batch(() => {
168 if (untracked(() => pls.value === "sleeping")) pls.value = "loaded";
169 pl.value = newPlaylistItems;
170 });
171 await playlistItems.put(newPlaylistItems);
172 },
173 },
174 settings: {
175 collection: computed(() => {
176 if (untracked(() => ss.value === "sleeping")) loadSettings();
177 return ss.value === "loaded"
178 ? { state: "loaded", data: s.value }
179 : { state: "loading" };
180 }),
181 reload: loadSettings,
182 save: async (newSettings) => {
183 batch(() => {
184 if (untracked(() => ss.value === "sleeping")) ss.value = "loaded";
185 s.value = newSettings;
186 });
187 await settings.put(newSettings);
188 },
189 },
190 tracks: {
191 collection: computed(() => {
192 if (untracked(() => ts.value === "sleeping")) loadTracks();
193 return ts.value === "loaded"
194 ? { state: "loaded", data: t.value }
195 : { state: "loading" };
196 }),
197 reload: loadTracks,
198 save: async (newTracks) => {
199 batch(() => {
200 if (untracked(() => ts.value === "sleeping")) ts.value = "loaded";
201 t.value = newTracks;
202 });
203 await tracks.put(newTracks);
204 },
205 },
206 signals: {
207 facets: c,
208 playlistItems: pl,
209 settings: s,
210 tracks: t,
211 },
212 };
213}