@freqhole/playlistz 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.changeset/nice-wolves-thank.md +5 -0
  3. package/.freqhole-versions.json +4 -0
  4. package/.github/copilot-instructions.md +201 -0
  5. package/.github/workflows/changesets.yml +50 -0
  6. package/.github/workflows/npm-publish.yml +124 -0
  7. package/.github/workflows/pr-checks.yml +103 -0
  8. package/README.md +30 -0
  9. package/build-component.js +141 -0
  10. package/build-zip-bundle-lib.js +44 -0
  11. package/config/playwright.config.ts +47 -0
  12. package/config/vite.config.ts +44 -0
  13. package/config/vitest.config.ts +39 -0
  14. package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
  15. package/dist/assets/index-CbOXzGiA.js +216 -0
  16. package/dist/assets/index-CbOXzGiA.js.map +1 -0
  17. package/dist/assets/index-TvJ6RFpy.css +1 -0
  18. package/dist/assets/midden-DceCrT_L.js +2 -0
  19. package/dist/assets/midden-DceCrT_L.js.map +1 -0
  20. package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
  21. package/dist/index.html +55 -0
  22. package/dist/sw.js +134 -0
  23. package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
  24. package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
  25. package/docs/E2E_TESTID_PLAN.md +234 -0
  26. package/docs/IROH_P2P_PLAN.md +302 -0
  27. package/docs/ROADMAP.md +695 -0
  28. package/docs/TODO.md +167 -0
  29. package/docs/bundle-embedding-plan.md +134 -0
  30. package/docs/standalone-refactor.md +184 -0
  31. package/e2e/all-playlists.spec.ts +220 -0
  32. package/e2e/audio-player.spec.ts +226 -0
  33. package/e2e/collaborative-features.spec.ts +229 -0
  34. package/e2e/contexts.ts +238 -0
  35. package/e2e/edit-panel.spec.ts +87 -0
  36. package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
  37. package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
  38. package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
  39. package/e2e/fixtures/chord-stack-3s.wav +0 -0
  40. package/e2e/fixtures/cover-anim.gif +0 -0
  41. package/e2e/fixtures/cover-blue.png +0 -0
  42. package/e2e/fixtures/cover-checkers.png +0 -0
  43. package/e2e/fixtures/cover-gradient.jpg +0 -0
  44. package/e2e/fixtures/cover-mono.gif +0 -0
  45. package/e2e/fixtures/cover-noise.png +0 -0
  46. package/e2e/fixtures/cover-plasma.webp +0 -0
  47. package/e2e/fixtures/cover-portrait.jpg +0 -0
  48. package/e2e/fixtures/cover-red.png +0 -0
  49. package/e2e/fixtures/cover-thumb.jpg +0 -0
  50. package/e2e/fixtures/cover-wide.webp +0 -0
  51. package/e2e/fixtures/generate.mjs +257 -0
  52. package/e2e/fixtures/long-drone-90s.mp3 +0 -0
  53. package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
  54. package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
  55. package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
  56. package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
  57. package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
  58. package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
  59. package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
  60. package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
  61. package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
  62. package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
  63. package/e2e/fixtures/tone-220hz-10s.wav +0 -0
  64. package/e2e/fixtures/tone-440hz-2s.wav +0 -0
  65. package/e2e/fixtures/tone-880hz-5s.wav +0 -0
  66. package/e2e/fixtures/tone-stereo-3s.wav +0 -0
  67. package/e2e/fixtures/user-provided/README.md +1 -0
  68. package/e2e/helpers/app.ts +143 -0
  69. package/e2e/helpers/hooks.ts +133 -0
  70. package/e2e/helpers/index.ts +12 -0
  71. package/e2e/helpers/media.ts +125 -0
  72. package/e2e/helpers.ts +10 -0
  73. package/e2e/p2p-collaboration.spec.ts +356 -0
  74. package/e2e/p2p-multi-peer.spec.ts +723 -0
  75. package/e2e/p2p-states.spec.ts +302 -0
  76. package/e2e/playback.spec.ts +56 -0
  77. package/e2e/playlist-crud.spec.ts +126 -0
  78. package/e2e/share-link-autoplay.spec.ts +129 -0
  79. package/e2e/sharing-access.spec.ts +205 -0
  80. package/e2e/sharing.spec.ts +195 -0
  81. package/e2e/song-cache-state.spec.ts +202 -0
  82. package/e2e/zip-bundle.spec.ts +855 -0
  83. package/eslint.config.js +114 -0
  84. package/index.html +54 -0
  85. package/package.json +119 -0
  86. package/public/sw.js +134 -0
  87. package/scripts/use-local.mjs +37 -0
  88. package/scripts/use-published.mjs +37 -0
  89. package/src/App.tsx +9 -0
  90. package/src/cli/check.ts +164 -0
  91. package/src/cli/generate.ts +184 -0
  92. package/src/cli/http.ts +88 -0
  93. package/src/cli/index.ts +65 -0
  94. package/src/cli/init.ts +18 -0
  95. package/src/components/AllPlaylistsPanel.tsx +713 -0
  96. package/src/components/AudioPlayer.tsx +122 -0
  97. package/src/components/MarqueeText.tsx +101 -0
  98. package/src/components/PlaylistCoverModal.tsx +519 -0
  99. package/src/components/PlaylistEditPanel.tsx +803 -0
  100. package/src/components/PlaylistSharePanel.tsx +1020 -0
  101. package/src/components/ShareLinkKnockPanel.tsx +144 -0
  102. package/src/components/SharePanel.tsx +584 -0
  103. package/src/components/SongEditModal.tsx +453 -0
  104. package/src/components/SongEditPanel.tsx +578 -0
  105. package/src/components/SongRow.tsx +689 -0
  106. package/src/components/index.tsx +494 -0
  107. package/src/components/playlist/index.tsx +1203 -0
  108. package/src/context/PlaylistzContext.tsx +74 -0
  109. package/src/dev-hooks.ts +35 -0
  110. package/src/hooks/createDocIndexQuery.ts +53 -0
  111. package/src/hooks/createDocStore.test.ts +303 -0
  112. package/src/hooks/createDocStore.ts +90 -0
  113. package/src/hooks/useDragAndDrop.test.ts +474 -0
  114. package/src/hooks/useDragAndDrop.ts +400 -0
  115. package/src/hooks/useImageModal.test.ts +174 -0
  116. package/src/hooks/useImageModal.ts +201 -0
  117. package/src/hooks/usePlaylistManager.test.ts +453 -0
  118. package/src/hooks/usePlaylistManager.ts +685 -0
  119. package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
  120. package/src/hooks/usePlaylistsQuery.ts +44 -0
  121. package/src/hooks/useSongState.test.ts +236 -0
  122. package/src/hooks/useSongState.ts +114 -0
  123. package/src/hooks/useUIState.ts +71 -0
  124. package/src/index.tsx +18 -0
  125. package/src/services/audioService.dev.ts +22 -0
  126. package/src/services/audioService.test.ts +1226 -0
  127. package/src/services/audioService.ts +1395 -0
  128. package/src/services/automergeRepo.test.ts +269 -0
  129. package/src/services/automergeRepo.ts +226 -0
  130. package/src/services/blobTransferService.dev.ts +119 -0
  131. package/src/services/blobTransferService.test.ts +441 -0
  132. package/src/services/blobTransferService.ts +702 -0
  133. package/src/services/docIndexService.test.ts +179 -0
  134. package/src/services/docIndexService.ts +118 -0
  135. package/src/services/fileProcessingService.test.ts +554 -0
  136. package/src/services/fileProcessingService.ts +239 -0
  137. package/src/services/imageService.test.ts +701 -0
  138. package/src/services/imageService.ts +365 -0
  139. package/src/services/indexedDBService.integration.test.ts +104 -0
  140. package/src/services/indexedDBService.test.ts +202 -0
  141. package/src/services/indexedDBService.ts +436 -0
  142. package/src/services/offlineService.test.ts +661 -0
  143. package/src/services/offlineService.ts +382 -0
  144. package/src/services/p2pService.test.ts +305 -0
  145. package/src/services/p2pService.ts +344 -0
  146. package/src/services/playlistDocService.test.ts +448 -0
  147. package/src/services/playlistDocService.ts +707 -0
  148. package/src/services/playlistDownloadService.test.ts +674 -0
  149. package/src/services/playlistDownloadService.ts +389 -0
  150. package/src/services/sharingService.test.ts +812 -0
  151. package/src/services/sharingService.ts +1073 -0
  152. package/src/services/sharingState.ts +161 -0
  153. package/src/services/songReactivity.test.ts +620 -0
  154. package/src/services/songReactivity.ts +145 -0
  155. package/src/services/standaloneService.test.ts +1025 -0
  156. package/src/services/standaloneService.ts +588 -0
  157. package/src/services/streamingAudioService.test.ts +275 -0
  158. package/src/services/streamingAudioService.ts +166 -0
  159. package/src/styles.css +428 -0
  160. package/src/test-setup.ts +547 -0
  161. package/src/types/global.d.ts +40 -0
  162. package/src/types/playlist.ts +99 -0
  163. package/src/utils/hashUtils.ts +41 -0
  164. package/src/utils/log.ts +97 -0
  165. package/src/utils/m3u.test.ts +172 -0
  166. package/src/utils/m3u.ts +136 -0
  167. package/src/utils/mockData.ts +166 -0
  168. package/src/utils/standaloneTemplates.test.ts +175 -0
  169. package/src/utils/standaloneTemplates.ts +83 -0
  170. package/src/utils/swTemplate.ts +84 -0
  171. package/src/utils/timeUtils.ts +166 -0
  172. package/src/utils/typeGuards.ts +171 -0
  173. package/src/web-component.tsx +98 -0
  174. package/src/zip-bundle/index.ts +7 -0
  175. package/src/zip-bundle/m3u.ts +45 -0
  176. package/src/zip-bundle/types.ts +50 -0
  177. package/src/zip-bundle/utils.ts +33 -0
  178. package/src/zip-bundle/zipBuilder.ts +309 -0
  179. package/tailwind.config.js +55 -0
  180. package/tsconfig.json +43 -0
@@ -0,0 +1,620 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { createRoot } from "solid-js";
3
+ import {
4
+ songUpdateTrigger,
5
+ triggerSongUpdate,
6
+ getSongUpdateTrigger,
7
+ getLastUpdateTime,
8
+ getSongSpecificTrigger,
9
+ triggerSpecificSongUpdate,
10
+ clearUpdateHistory,
11
+ getUpdateStats,
12
+ triggerSongUpdateWithOptions,
13
+ } from "./songReactivity.js";
14
+
15
+ describe("Song Reactivity System", () => {
16
+ beforeEach(() => {
17
+ // Clear update history before each test to ensure clean state
18
+ clearUpdateHistory();
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ afterEach(() => {
23
+ // Clean up after each test
24
+ clearUpdateHistory();
25
+ });
26
+
27
+ describe("Basic Reactivity", () => {
28
+ it("should start with a numeric trigger value", () => {
29
+ createRoot(() => {
30
+ const triggerValue = getSongUpdateTrigger();
31
+ expect(typeof triggerValue).toBe("number");
32
+ expect(songUpdateTrigger()).toBe(triggerValue);
33
+ });
34
+ });
35
+
36
+ it("should increment trigger when triggerSongUpdate is called", () => {
37
+ createRoot(() => {
38
+ const initialValue = getSongUpdateTrigger();
39
+
40
+ triggerSongUpdate();
41
+ expect(getSongUpdateTrigger()).toBe(initialValue + 1);
42
+
43
+ triggerSongUpdate();
44
+ expect(getSongUpdateTrigger()).toBe(initialValue + 2);
45
+ });
46
+ });
47
+
48
+ it("should track song updates with timestamps", () => {
49
+ createRoot(() => {
50
+ const songId = "test-song-123";
51
+ const beforeTime = Date.now();
52
+
53
+ triggerSongUpdate(songId);
54
+
55
+ const afterTime = Date.now();
56
+ const updateTime = getLastUpdateTime(songId);
57
+
58
+ expect(updateTime).toBeDefined();
59
+ expect(updateTime).toBeGreaterThanOrEqual(beforeTime);
60
+ expect(updateTime).toBeLessThanOrEqual(afterTime);
61
+ });
62
+ });
63
+
64
+ it("should return undefined for songs that haven't been updated", () => {
65
+ createRoot(() => {
66
+ const updateTime = getLastUpdateTime("non-existent-song");
67
+ expect(updateTime).toBeUndefined();
68
+ });
69
+ });
70
+
71
+ it("should handle multiple song updates", () => {
72
+ createRoot(() => {
73
+ const initialValue = getSongUpdateTrigger();
74
+
75
+ triggerSongUpdate("song1");
76
+ triggerSongUpdate("song2");
77
+ triggerSongUpdate("song1"); // Update song1 again
78
+
79
+ expect(getLastUpdateTime("song1")).toBeDefined();
80
+ expect(getLastUpdateTime("song2")).toBeDefined();
81
+ expect(getSongUpdateTrigger()).toBe(initialValue + 3);
82
+ });
83
+ });
84
+ });
85
+
86
+ describe("Song-Specific Signals", () => {
87
+ it("should create and return song-specific triggers", () => {
88
+ createRoot(() => {
89
+ const songId = "test-song-456";
90
+ const trigger1 = getSongSpecificTrigger(songId);
91
+ const trigger2 = getSongSpecificTrigger(songId);
92
+
93
+ // Should return the same trigger for the same song ID
94
+ expect(trigger1).toBe(trigger2);
95
+ expect(typeof trigger1).toBe("function");
96
+ });
97
+ });
98
+
99
+ it("should create different triggers for different songs", () => {
100
+ createRoot(() => {
101
+ const trigger1 = getSongSpecificTrigger("song1");
102
+ const trigger2 = getSongSpecificTrigger("song2");
103
+
104
+ expect(trigger1).not.toBe(trigger2);
105
+ });
106
+ });
107
+
108
+ it("should start song-specific triggers at 0", () => {
109
+ createRoot(() => {
110
+ const songId = "test-song-789";
111
+ const trigger = getSongSpecificTrigger(songId);
112
+
113
+ expect(trigger()).toBe(0);
114
+ });
115
+ });
116
+
117
+ it("should increment song-specific triggers independently", () => {
118
+ createRoot(() => {
119
+ const song1Trigger = getSongSpecificTrigger("song1");
120
+ const song2Trigger = getSongSpecificTrigger("song2");
121
+
122
+ // Initial state
123
+ expect(song1Trigger()).toBe(0);
124
+ expect(song2Trigger()).toBe(0);
125
+
126
+ // Update song1 specifically
127
+ triggerSpecificSongUpdate("song1");
128
+ expect(song1Trigger()).toBe(1);
129
+ expect(song2Trigger()).toBe(0); // Should remain unchanged
130
+
131
+ // Update song2 specifically
132
+ triggerSpecificSongUpdate("song2");
133
+ expect(song1Trigger()).toBe(1); // Should remain unchanged
134
+ expect(song2Trigger()).toBe(1);
135
+ });
136
+ });
137
+
138
+ it("should update timestamp when triggering specific song update", () => {
139
+ createRoot(() => {
140
+ const songId = "test-song-timestamp";
141
+ const beforeTime = Date.now();
142
+
143
+ triggerSpecificSongUpdate(songId);
144
+
145
+ const afterTime = Date.now();
146
+ const updateTime = getLastUpdateTime(songId);
147
+
148
+ expect(updateTime).toBeDefined();
149
+ expect(updateTime).toBeGreaterThanOrEqual(beforeTime);
150
+ expect(updateTime).toBeLessThanOrEqual(afterTime);
151
+ });
152
+ });
153
+
154
+ it("should not affect global trigger when using triggerSpecificSongUpdate", () => {
155
+ createRoot(() => {
156
+ const initialGlobalTrigger = getSongUpdateTrigger();
157
+
158
+ triggerSpecificSongUpdate("test-song");
159
+
160
+ expect(getSongUpdateTrigger()).toBe(initialGlobalTrigger);
161
+ });
162
+ });
163
+ });
164
+
165
+ describe("Enhanced Trigger Options", () => {
166
+ it("should trigger global update by default", () => {
167
+ createRoot(() => {
168
+ const initialTrigger = getSongUpdateTrigger();
169
+
170
+ triggerSongUpdateWithOptions({ songId: "test-song" });
171
+
172
+ expect(getSongUpdateTrigger()).toBe(initialTrigger + 1);
173
+ });
174
+ });
175
+
176
+ it("should only trigger specific song when specificOnly is true", () => {
177
+ createRoot(() => {
178
+ const songId = "test-song-specific";
179
+ const initialGlobalTrigger = getSongUpdateTrigger();
180
+ const songTrigger = getSongSpecificTrigger(songId);
181
+ const initialSongTrigger = songTrigger();
182
+
183
+ triggerSongUpdateWithOptions({
184
+ songId,
185
+ specificOnly: true,
186
+ });
187
+
188
+ // Global trigger should not change
189
+ expect(getSongUpdateTrigger()).toBe(initialGlobalTrigger);
190
+
191
+ // Song-specific trigger should increment
192
+ expect(songTrigger()).toBe(initialSongTrigger + 1);
193
+
194
+ // Timestamp should be updated
195
+ expect(getLastUpdateTime(songId)).toBeDefined();
196
+ });
197
+ });
198
+
199
+ it("should handle different update types", () => {
200
+ createRoot(() => {
201
+ const songId = "test-song-types";
202
+ const initialTrigger = getSongUpdateTrigger();
203
+
204
+ // Test different update types
205
+ const updateTypes = ["edit", "create", "delete", "reorder"] as const;
206
+
207
+ updateTypes.forEach((type, index) => {
208
+ triggerSongUpdateWithOptions({
209
+ songId: `${songId}-${type}`,
210
+ type,
211
+ metadata: { operation: type },
212
+ });
213
+
214
+ expect(getSongUpdateTrigger()).toBe(initialTrigger + index + 1);
215
+ expect(getLastUpdateTime(`${songId}-${type}`)).toBeDefined();
216
+ });
217
+ });
218
+ });
219
+
220
+ it("should handle options without songId", () => {
221
+ createRoot(() => {
222
+ const initialTrigger = getSongUpdateTrigger();
223
+
224
+ triggerSongUpdateWithOptions({
225
+ type: "edit",
226
+ metadata: { batch: true },
227
+ });
228
+
229
+ expect(getSongUpdateTrigger()).toBe(initialTrigger + 1);
230
+ });
231
+ });
232
+
233
+ it("should handle metadata in options", () => {
234
+ createRoot(() => {
235
+ const songId = "test-song-metadata";
236
+ const metadata = {
237
+ reason: "user-edit",
238
+ batch: false,
239
+ timestamp: Date.now(),
240
+ };
241
+
242
+ // The function should not throw with metadata
243
+ expect(() => {
244
+ triggerSongUpdateWithOptions({
245
+ songId,
246
+ type: "edit",
247
+ metadata,
248
+ });
249
+ }).not.toThrow();
250
+
251
+ expect(getLastUpdateTime(songId)).toBeDefined();
252
+ });
253
+ });
254
+ });
255
+
256
+ describe("Update Statistics and History", () => {
257
+ it("should provide accurate update statistics", () => {
258
+ createRoot(() => {
259
+ // Initial stats after clearing
260
+ let stats = getUpdateStats();
261
+ const initialUpdates = stats.totalUpdates;
262
+ expect(stats.trackedSongs).toBe(0);
263
+ expect(stats.recentUpdates).toHaveLength(0);
264
+
265
+ // Add some updates
266
+ triggerSongUpdate("song1");
267
+ triggerSongUpdate("song2");
268
+ triggerSongUpdate("song1"); // Update song1 again
269
+
270
+ stats = getUpdateStats();
271
+ expect(stats.totalUpdates).toBe(initialUpdates + 3);
272
+ expect(stats.trackedSongs).toBe(2); // Only 2 unique songs
273
+ expect(stats.recentUpdates).toHaveLength(2);
274
+ });
275
+ });
276
+
277
+ it("should order recent updates by timestamp", () => {
278
+ createRoot(() => {
279
+ // Add updates in sequence
280
+ triggerSongUpdate("song1");
281
+ triggerSongUpdate("song2");
282
+
283
+ const stats = getUpdateStats();
284
+ const recentUpdates = stats.recentUpdates;
285
+
286
+ if (recentUpdates.length >= 2) {
287
+ // More recent update should have higher or equal timestamp
288
+ expect(recentUpdates[0]?.[1]).toBeGreaterThanOrEqual(
289
+ recentUpdates[1]?.[1] || 0
290
+ );
291
+ }
292
+ });
293
+ });
294
+
295
+ it("should limit recent updates to 10 entries", () => {
296
+ createRoot(() => {
297
+ // Add more than 10 updates
298
+ for (let i = 0; i < 15; i++) {
299
+ triggerSongUpdate(`song${i}`);
300
+ }
301
+
302
+ const stats = getUpdateStats();
303
+ expect(stats.recentUpdates.length).toBeLessThanOrEqual(10);
304
+ expect(stats.trackedSongs).toBe(15);
305
+ });
306
+ });
307
+
308
+ it("should clear all update history", () => {
309
+ createRoot(() => {
310
+ // Add some updates
311
+ triggerSongUpdate("song1");
312
+ triggerSongUpdate("song2");
313
+ const songTrigger = getSongSpecificTrigger("song3");
314
+ triggerSpecificSongUpdate("song3");
315
+
316
+ // Verify updates exist
317
+ expect(getLastUpdateTime("song1")).toBeDefined();
318
+ expect(getLastUpdateTime("song2")).toBeDefined();
319
+ expect(getLastUpdateTime("song3")).toBeDefined();
320
+ expect(songTrigger()).toBe(1);
321
+
322
+ // Clear history
323
+ clearUpdateHistory();
324
+
325
+ // Verify everything is cleared
326
+ expect(getLastUpdateTime("song1")).toBeUndefined();
327
+ expect(getLastUpdateTime("song2")).toBeUndefined();
328
+ expect(getLastUpdateTime("song3")).toBeUndefined();
329
+
330
+ const stats = getUpdateStats();
331
+ expect(stats.trackedSongs).toBe(0);
332
+ expect(stats.recentUpdates).toHaveLength(0);
333
+ });
334
+ });
335
+ });
336
+
337
+ describe("Edge Cases and Error Handling", () => {
338
+ it("should handle empty song IDs gracefully", () => {
339
+ createRoot(() => {
340
+ expect(() => {
341
+ triggerSongUpdate("");
342
+ triggerSpecificSongUpdate("");
343
+ getSongSpecificTrigger("");
344
+ getLastUpdateTime("");
345
+ }).not.toThrow();
346
+ });
347
+ });
348
+
349
+ it("should handle undefined song IDs", () => {
350
+ createRoot(() => {
351
+ expect(() => {
352
+ triggerSongUpdate(undefined);
353
+ getLastUpdateTime(undefined as any);
354
+ }).not.toThrow();
355
+ });
356
+ });
357
+
358
+ it("should handle rapid successive updates", () => {
359
+ createRoot(() => {
360
+ const songId = "rapid-update-song";
361
+ const initialTrigger = getSongUpdateTrigger();
362
+
363
+ // Trigger multiple rapid updates
364
+ for (let i = 0; i < 100; i++) {
365
+ triggerSongUpdate(songId);
366
+ }
367
+
368
+ expect(getSongUpdateTrigger()).toBe(initialTrigger + 100);
369
+ expect(getLastUpdateTime(songId)).toBeDefined();
370
+ });
371
+ });
372
+
373
+ it("should handle concurrent song-specific updates", () => {
374
+ createRoot(() => {
375
+ const songs = ["song1", "song2", "song3"];
376
+ const triggers = songs.map((id) => getSongSpecificTrigger(id));
377
+
378
+ // Simulate concurrent updates
379
+ songs.forEach((songId) => {
380
+ triggerSpecificSongUpdate(songId);
381
+ });
382
+
383
+ // All song-specific triggers should be incremented
384
+ triggers.forEach((trigger) => {
385
+ expect(trigger()).toBe(1);
386
+ });
387
+
388
+ // All songs should have timestamps
389
+ songs.forEach((songId) => {
390
+ expect(getLastUpdateTime(songId)).toBeDefined();
391
+ });
392
+ });
393
+ });
394
+
395
+ it("should maintain separate state for each song", () => {
396
+ createRoot(() => {
397
+ const song1Trigger = getSongSpecificTrigger("song1");
398
+ const song2Trigger = getSongSpecificTrigger("song2");
399
+
400
+ // Update song1 multiple times
401
+ triggerSpecificSongUpdate("song1");
402
+ triggerSpecificSongUpdate("song1");
403
+ triggerSpecificSongUpdate("song1");
404
+
405
+ // Update song2 once
406
+ triggerSpecificSongUpdate("song2");
407
+
408
+ expect(song1Trigger()).toBe(3);
409
+ expect(song2Trigger()).toBe(1);
410
+ });
411
+ });
412
+ });
413
+
414
+ describe("Integration Scenarios", () => {
415
+ it("should handle mixed global and specific updates", () => {
416
+ createRoot(() => {
417
+ const songId = "mixed-update-song";
418
+ const songTrigger = getSongSpecificTrigger(songId);
419
+ const initialGlobalTrigger = getSongUpdateTrigger();
420
+
421
+ // Mix of update types
422
+ triggerSongUpdate(songId); // Global + tracking
423
+ triggerSpecificSongUpdate(songId); // Specific only
424
+ triggerSongUpdate(); // Global only, no songId
425
+ triggerSongUpdateWithOptions({ songId, specificOnly: true }); // Specific only
426
+
427
+ expect(getSongUpdateTrigger()).toBe(initialGlobalTrigger + 2); // Only 2 global updates
428
+ expect(songTrigger()).toBe(2); // 2 specific updates
429
+ expect(getLastUpdateTime(songId)).toBeDefined();
430
+ });
431
+ });
432
+
433
+ it("should work correctly after clearing history", () => {
434
+ createRoot(() => {
435
+ const songId = "post-clear-song";
436
+
437
+ // Add initial updates
438
+ triggerSongUpdate(songId);
439
+ expect(getLastUpdateTime(songId)).toBeDefined();
440
+
441
+ // Clear history
442
+ clearUpdateHistory();
443
+ expect(getLastUpdateTime(songId)).toBeUndefined();
444
+
445
+ // Add new updates after clearing
446
+ triggerSongUpdate(songId);
447
+ expect(getLastUpdateTime(songId)).toBeDefined();
448
+
449
+ const songTrigger = getSongSpecificTrigger(songId);
450
+ triggerSpecificSongUpdate(songId);
451
+ expect(songTrigger()).toBe(1);
452
+ });
453
+ });
454
+
455
+ it("should handle realistic user workflow", () => {
456
+ createRoot(() => {
457
+ const playlistId = "playlist123";
458
+ const songIds = ["song1", "song2", "song3"];
459
+ const initialTrigger = getSongUpdateTrigger();
460
+
461
+ // User creates playlist and adds songs
462
+ songIds.forEach((songId) => {
463
+ triggerSongUpdateWithOptions({
464
+ songId,
465
+ type: "create",
466
+ metadata: { playlistId },
467
+ });
468
+ });
469
+
470
+ // User edits song1
471
+ triggerSongUpdateWithOptions({
472
+ songId: "song1",
473
+ type: "edit",
474
+ metadata: { field: "title", playlistId },
475
+ });
476
+
477
+ // User reorders songs
478
+ triggerSongUpdateWithOptions({
479
+ type: "reorder",
480
+ metadata: { playlistId, newOrder: ["song2", "song1", "song3"] },
481
+ });
482
+
483
+ // Verify all operations were tracked
484
+ expect(getSongUpdateTrigger()).toBeGreaterThanOrEqual(
485
+ initialTrigger + 4
486
+ ); // 3 creates + 1 edit + 1 reorder
487
+ songIds.forEach((songId) => {
488
+ expect(getLastUpdateTime(songId)).toBeDefined();
489
+ });
490
+
491
+ const stats = getUpdateStats();
492
+ expect(stats.trackedSongs).toBe(3);
493
+ expect(stats.totalUpdates).toBeGreaterThanOrEqual(4);
494
+ });
495
+ });
496
+ });
497
+
498
+ describe("Development Helpers", () => {
499
+ it("should expose debugging functions in development mode", () => {
500
+ createRoot(() => {
501
+ // Mock development environment
502
+ const originalDEV = (globalThis as any).__DEV__;
503
+ (globalThis as any).__DEV__ = true;
504
+
505
+ try {
506
+ // Simulate development helper setup
507
+ const mockGlobal = globalThis as any;
508
+ mockGlobal.__songReactivity = {
509
+ getSongUpdateTrigger,
510
+ getUpdateStats,
511
+ clearUpdateHistory,
512
+ triggerSongUpdate,
513
+ };
514
+
515
+ expect(mockGlobal.__songReactivity).toBeDefined();
516
+ expect(typeof mockGlobal.__songReactivity.getSongUpdateTrigger).toBe(
517
+ "function"
518
+ );
519
+ expect(typeof mockGlobal.__songReactivity.getUpdateStats).toBe(
520
+ "function"
521
+ );
522
+ expect(typeof mockGlobal.__songReactivity.clearUpdateHistory).toBe(
523
+ "function"
524
+ );
525
+ expect(typeof mockGlobal.__songReactivity.triggerSongUpdate).toBe(
526
+ "function"
527
+ );
528
+
529
+ // Test that debugging functions work
530
+ expect(() => {
531
+ mockGlobal.__songReactivity.triggerSongUpdate("debug-song");
532
+ mockGlobal.__songReactivity.getUpdateStats();
533
+ }).not.toThrow();
534
+ } finally {
535
+ (globalThis as any).__DEV__ = originalDEV;
536
+ delete (globalThis as any).__songReactivity;
537
+ }
538
+ });
539
+ });
540
+ });
541
+
542
+ describe("Performance Considerations", () => {
543
+ it("should handle many song updates efficiently", () => {
544
+ createRoot(() => {
545
+ const startTime = performance.now();
546
+ const initialTrigger = getSongUpdateTrigger();
547
+
548
+ // Simulate many updates
549
+ for (let i = 0; i < 1000; i++) {
550
+ triggerSongUpdate(`song${i % 100}`); // 1000 updates across 100 songs
551
+ }
552
+
553
+ const endTime = performance.now();
554
+ const duration = endTime - startTime;
555
+
556
+ // Should complete in reasonable time (less than 100ms on most systems)
557
+ expect(duration).toBeLessThan(100);
558
+
559
+ // Verify state is correct
560
+ expect(getSongUpdateTrigger()).toBe(initialTrigger + 1000);
561
+ expect(getUpdateStats().trackedSongs).toBe(100);
562
+ });
563
+ });
564
+
565
+ it("should handle many song-specific triggers efficiently", () => {
566
+ createRoot(() => {
567
+ const songCount = 500;
568
+ const triggers: (() => number)[] = [];
569
+
570
+ const startTime = performance.now();
571
+
572
+ // Create many song-specific triggers
573
+ for (let i = 0; i < songCount; i++) {
574
+ triggers.push(getSongSpecificTrigger(`song${i}`));
575
+ }
576
+
577
+ // Update each one
578
+ for (let i = 0; i < songCount; i++) {
579
+ triggerSpecificSongUpdate(`song${i}`);
580
+ }
581
+
582
+ const endTime = performance.now();
583
+ const duration = endTime - startTime;
584
+
585
+ // Should complete in reasonable time
586
+ expect(duration).toBeLessThan(200);
587
+
588
+ // Verify all triggers were updated
589
+ triggers.forEach((trigger) => {
590
+ expect(trigger()).toBe(1);
591
+ });
592
+ });
593
+ });
594
+ });
595
+
596
+ describe("Function Exports", () => {
597
+ it("should export all required functions", () => {
598
+ expect(typeof songUpdateTrigger).toBe("function");
599
+ expect(typeof triggerSongUpdate).toBe("function");
600
+ expect(typeof getSongUpdateTrigger).toBe("function");
601
+ expect(typeof getLastUpdateTime).toBe("function");
602
+ expect(typeof getSongSpecificTrigger).toBe("function");
603
+ expect(typeof triggerSpecificSongUpdate).toBe("function");
604
+ expect(typeof clearUpdateHistory).toBe("function");
605
+ expect(typeof getUpdateStats).toBe("function");
606
+ expect(typeof triggerSongUpdateWithOptions).toBe("function");
607
+ });
608
+
609
+ it("should maintain signal functionality", () => {
610
+ createRoot(() => {
611
+ // Test that signals work as expected
612
+ const initialValue = songUpdateTrigger();
613
+ triggerSongUpdate();
614
+ const newValue = songUpdateTrigger();
615
+
616
+ expect(newValue).toBeGreaterThan(initialValue);
617
+ });
618
+ });
619
+ });
620
+ });