@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.
- package/.changeset/config.json +11 -0
- package/.changeset/nice-wolves-thank.md +5 -0
- package/.freqhole-versions.json +4 -0
- package/.github/copilot-instructions.md +201 -0
- package/.github/workflows/changesets.yml +50 -0
- package/.github/workflows/npm-publish.yml +124 -0
- package/.github/workflows/pr-checks.yml +103 -0
- package/README.md +30 -0
- package/build-component.js +141 -0
- package/build-zip-bundle-lib.js +44 -0
- package/config/playwright.config.ts +47 -0
- package/config/vite.config.ts +44 -0
- package/config/vitest.config.ts +39 -0
- package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
- package/dist/assets/index-CbOXzGiA.js +216 -0
- package/dist/assets/index-CbOXzGiA.js.map +1 -0
- package/dist/assets/index-TvJ6RFpy.css +1 -0
- package/dist/assets/midden-DceCrT_L.js +2 -0
- package/dist/assets/midden-DceCrT_L.js.map +1 -0
- package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
- package/dist/index.html +55 -0
- package/dist/sw.js +134 -0
- package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
- package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
- package/docs/E2E_TESTID_PLAN.md +234 -0
- package/docs/IROH_P2P_PLAN.md +302 -0
- package/docs/ROADMAP.md +695 -0
- package/docs/TODO.md +167 -0
- package/docs/bundle-embedding-plan.md +134 -0
- package/docs/standalone-refactor.md +184 -0
- package/e2e/all-playlists.spec.ts +220 -0
- package/e2e/audio-player.spec.ts +226 -0
- package/e2e/collaborative-features.spec.ts +229 -0
- package/e2e/contexts.ts +238 -0
- package/e2e/edit-panel.spec.ts +87 -0
- package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
- package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
- package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
- package/e2e/fixtures/chord-stack-3s.wav +0 -0
- package/e2e/fixtures/cover-anim.gif +0 -0
- package/e2e/fixtures/cover-blue.png +0 -0
- package/e2e/fixtures/cover-checkers.png +0 -0
- package/e2e/fixtures/cover-gradient.jpg +0 -0
- package/e2e/fixtures/cover-mono.gif +0 -0
- package/e2e/fixtures/cover-noise.png +0 -0
- package/e2e/fixtures/cover-plasma.webp +0 -0
- package/e2e/fixtures/cover-portrait.jpg +0 -0
- package/e2e/fixtures/cover-red.png +0 -0
- package/e2e/fixtures/cover-thumb.jpg +0 -0
- package/e2e/fixtures/cover-wide.webp +0 -0
- package/e2e/fixtures/generate.mjs +257 -0
- package/e2e/fixtures/long-drone-90s.mp3 +0 -0
- package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
- package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
- package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
- package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
- package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
- package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
- package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
- package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
- package/e2e/fixtures/tone-220hz-10s.wav +0 -0
- package/e2e/fixtures/tone-440hz-2s.wav +0 -0
- package/e2e/fixtures/tone-880hz-5s.wav +0 -0
- package/e2e/fixtures/tone-stereo-3s.wav +0 -0
- package/e2e/fixtures/user-provided/README.md +1 -0
- package/e2e/helpers/app.ts +143 -0
- package/e2e/helpers/hooks.ts +133 -0
- package/e2e/helpers/index.ts +12 -0
- package/e2e/helpers/media.ts +125 -0
- package/e2e/helpers.ts +10 -0
- package/e2e/p2p-collaboration.spec.ts +356 -0
- package/e2e/p2p-multi-peer.spec.ts +723 -0
- package/e2e/p2p-states.spec.ts +302 -0
- package/e2e/playback.spec.ts +56 -0
- package/e2e/playlist-crud.spec.ts +126 -0
- package/e2e/share-link-autoplay.spec.ts +129 -0
- package/e2e/sharing-access.spec.ts +205 -0
- package/e2e/sharing.spec.ts +195 -0
- package/e2e/song-cache-state.spec.ts +202 -0
- package/e2e/zip-bundle.spec.ts +855 -0
- package/eslint.config.js +114 -0
- package/index.html +54 -0
- package/package.json +119 -0
- package/public/sw.js +134 -0
- package/scripts/use-local.mjs +37 -0
- package/scripts/use-published.mjs +37 -0
- package/src/App.tsx +9 -0
- package/src/cli/check.ts +164 -0
- package/src/cli/generate.ts +184 -0
- package/src/cli/http.ts +88 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/init.ts +18 -0
- package/src/components/AllPlaylistsPanel.tsx +713 -0
- package/src/components/AudioPlayer.tsx +122 -0
- package/src/components/MarqueeText.tsx +101 -0
- package/src/components/PlaylistCoverModal.tsx +519 -0
- package/src/components/PlaylistEditPanel.tsx +803 -0
- package/src/components/PlaylistSharePanel.tsx +1020 -0
- package/src/components/ShareLinkKnockPanel.tsx +144 -0
- package/src/components/SharePanel.tsx +584 -0
- package/src/components/SongEditModal.tsx +453 -0
- package/src/components/SongEditPanel.tsx +578 -0
- package/src/components/SongRow.tsx +689 -0
- package/src/components/index.tsx +494 -0
- package/src/components/playlist/index.tsx +1203 -0
- package/src/context/PlaylistzContext.tsx +74 -0
- package/src/dev-hooks.ts +35 -0
- package/src/hooks/createDocIndexQuery.ts +53 -0
- package/src/hooks/createDocStore.test.ts +303 -0
- package/src/hooks/createDocStore.ts +90 -0
- package/src/hooks/useDragAndDrop.test.ts +474 -0
- package/src/hooks/useDragAndDrop.ts +400 -0
- package/src/hooks/useImageModal.test.ts +174 -0
- package/src/hooks/useImageModal.ts +201 -0
- package/src/hooks/usePlaylistManager.test.ts +453 -0
- package/src/hooks/usePlaylistManager.ts +685 -0
- package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
- package/src/hooks/usePlaylistsQuery.ts +44 -0
- package/src/hooks/useSongState.test.ts +236 -0
- package/src/hooks/useSongState.ts +114 -0
- package/src/hooks/useUIState.ts +71 -0
- package/src/index.tsx +18 -0
- package/src/services/audioService.dev.ts +22 -0
- package/src/services/audioService.test.ts +1226 -0
- package/src/services/audioService.ts +1395 -0
- package/src/services/automergeRepo.test.ts +269 -0
- package/src/services/automergeRepo.ts +226 -0
- package/src/services/blobTransferService.dev.ts +119 -0
- package/src/services/blobTransferService.test.ts +441 -0
- package/src/services/blobTransferService.ts +702 -0
- package/src/services/docIndexService.test.ts +179 -0
- package/src/services/docIndexService.ts +118 -0
- package/src/services/fileProcessingService.test.ts +554 -0
- package/src/services/fileProcessingService.ts +239 -0
- package/src/services/imageService.test.ts +701 -0
- package/src/services/imageService.ts +365 -0
- package/src/services/indexedDBService.integration.test.ts +104 -0
- package/src/services/indexedDBService.test.ts +202 -0
- package/src/services/indexedDBService.ts +436 -0
- package/src/services/offlineService.test.ts +661 -0
- package/src/services/offlineService.ts +382 -0
- package/src/services/p2pService.test.ts +305 -0
- package/src/services/p2pService.ts +344 -0
- package/src/services/playlistDocService.test.ts +448 -0
- package/src/services/playlistDocService.ts +707 -0
- package/src/services/playlistDownloadService.test.ts +674 -0
- package/src/services/playlistDownloadService.ts +389 -0
- package/src/services/sharingService.test.ts +812 -0
- package/src/services/sharingService.ts +1073 -0
- package/src/services/sharingState.ts +161 -0
- package/src/services/songReactivity.test.ts +620 -0
- package/src/services/songReactivity.ts +145 -0
- package/src/services/standaloneService.test.ts +1025 -0
- package/src/services/standaloneService.ts +588 -0
- package/src/services/streamingAudioService.test.ts +275 -0
- package/src/services/streamingAudioService.ts +166 -0
- package/src/styles.css +428 -0
- package/src/test-setup.ts +547 -0
- package/src/types/global.d.ts +40 -0
- package/src/types/playlist.ts +99 -0
- package/src/utils/hashUtils.ts +41 -0
- package/src/utils/log.ts +97 -0
- package/src/utils/m3u.test.ts +172 -0
- package/src/utils/m3u.ts +136 -0
- package/src/utils/mockData.ts +166 -0
- package/src/utils/standaloneTemplates.test.ts +175 -0
- package/src/utils/standaloneTemplates.ts +83 -0
- package/src/utils/swTemplate.ts +84 -0
- package/src/utils/timeUtils.ts +166 -0
- package/src/utils/typeGuards.ts +171 -0
- package/src/web-component.tsx +98 -0
- package/src/zip-bundle/index.ts +7 -0
- package/src/zip-bundle/m3u.ts +45 -0
- package/src/zip-bundle/types.ts +50 -0
- package/src/zip-bundle/utils.ts +33 -0
- package/src/zip-bundle/zipBuilder.ts +309 -0
- package/tailwind.config.js +55 -0
- package/tsconfig.json +43 -0
package/docs/ROADMAP.md
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
# playlistz roadmap
|
|
2
|
+
|
|
3
|
+
active work is at the top, completed reference is at the bottom.
|
|
4
|
+
|
|
5
|
+
status: `[ ]` not started, `[~]` in progress, `[x]` done.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## direction
|
|
10
|
+
|
|
11
|
+
### modularization strategy
|
|
12
|
+
|
|
13
|
+
the long-term aim is a clean package layering. currently a lot of logic lives
|
|
14
|
+
in `playlistz/src/services/` that either duplicates `tomb/client/spume/`, or
|
|
15
|
+
belongs in shared packages. rather than "moving things to tomb/", the goal is
|
|
16
|
+
extracting reusable pieces into packages that both apps can depend on.
|
|
17
|
+
|
|
18
|
+
current package layering:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
freqhole-api-client (framework-agnostic: storage, p2p wire, schemas, share links)
|
|
22
|
+
|
|
|
23
|
+
@freqhole/solid (SolidJS wrappers: reactive signals over IDB, playlist hooks)
|
|
24
|
+
|
|
|
25
|
+
playlistz/ + spume/ (app-specific UI and state)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`freqhole-api-client` already exports: blob storage (IDB+OPFS/Cache), p2p
|
|
29
|
+
identity, iroh automerge adapter, zod schemas for `PlaylistDoc` + `SongEntry`,
|
|
30
|
+
knock/blob protocol messages, share link encode/decode.
|
|
31
|
+
|
|
32
|
+
concrete next extractions:
|
|
33
|
+
|
|
34
|
+
- **p2p node lifecycle** - `startP2P`, `stopP2P`, `getNode`, `getIdentity`,
|
|
35
|
+
accept loop, ALPN routing from `p2pService.ts` into `freqhole-api-client`
|
|
36
|
+
- **blob transfer protocol** - `fetchBlobFromPeer` + `serveBlobRequest` BiStream
|
|
37
|
+
message exchange from `blobTransferService.ts` into `freqhole-api-client`
|
|
38
|
+
- **new `@freqhole/solid` package** - SolidJS-specific wrappers that shouldn't
|
|
39
|
+
live in `freqhole-api-client` (no framework deps there): reactive signals over
|
|
40
|
+
IDB, playlist hooks (`usePlaylistManager` shape), drag-and-drop primitives,
|
|
41
|
+
media session bridge. both playlistz and spume could depend on this.
|
|
42
|
+
- **shared e2e utilities** - `makeWav()`, `dropFiles()`, `waitForApp()` are
|
|
43
|
+
already reasonably generic; worth a `@freqhole/e2e-utils` package that playlistz
|
|
44
|
+
and any future tomb e2e suites share. `e2e/helpers/` is the extraction target.
|
|
45
|
+
- **audio layer** - spume has `playerState`, `backends/`, `mediaSessionBridge`,
|
|
46
|
+
`playbackOrchestrator`; playlistz has `audioService.ts` + `streamingAudioService.ts`.
|
|
47
|
+
media session plumbing belongs in `freqhole-api-client/src/audio/mediaSession.ts`
|
|
48
|
+
(no SolidJS deps); the backend abstraction is a candidate for `@freqhole/solid`.
|
|
49
|
+
|
|
50
|
+
for **`tomb/playlistz/`** specifically: it's an earlier copy of
|
|
51
|
+
`freqhole/playlistz/src/services/` that has quietly diverged (lacks p2pService,
|
|
52
|
+
blobTransferService, etc.). the goal is not to pick a "winner" and copy files
|
|
53
|
+
over, but to have both apps depend on the shared packages above so there's nothing
|
|
54
|
+
left to diverge. audit first, extract shared pieces, then the `tomb/playlistz/`
|
|
55
|
+
copy just goes away naturally.
|
|
56
|
+
|
|
57
|
+
for **two-browser `@p2p` e2e tests**: these need iroh relay + midden WASM
|
|
58
|
+
which are built and managed in `tomb/`. the tests themselves should be shared
|
|
59
|
+
or runnable from both sides; see the e2e infrastructure section.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## code conventions
|
|
64
|
+
|
|
65
|
+
### underscore-prefix internal/dev exports
|
|
66
|
+
|
|
67
|
+
there are currently two distinct reasons for underscore-prefix names; they
|
|
68
|
+
should be treated differently:
|
|
69
|
+
|
|
70
|
+
- **`_internal*`** (intended: module-private) - used inside a file but exported
|
|
71
|
+
by accident, or exported for a test that imports directly. these should NOT be
|
|
72
|
+
exported at all. move to a private helper or use vitest module mocking instead.
|
|
73
|
+
|
|
74
|
+
- **`_dev*`** (intentional: dev-hook injection points) - exported specifically
|
|
75
|
+
for use by the corresponding `.dev.ts` file. the rule is:
|
|
76
|
+
- `_dev*` exports in `service.ts` are consumed only by `service.dev.ts`
|
|
77
|
+
- `service.dev.ts` registers them as `window.__*` hooks
|
|
78
|
+
- nothing else should import `_dev*` directly
|
|
79
|
+
|
|
80
|
+
current state: `_devSetFetchOverride`, `_devEvictBlob`, `_devSetBlobFetchTimeout`,
|
|
81
|
+
`_devFetchBlobBySha` are in `blobTransferService.ts`; `_devSeekTo`,
|
|
82
|
+
`_devTriggerTrackEnd`, `_devTriggerAudioError` are in `audioService.ts`. all
|
|
83
|
+
consumed only by their `.dev.ts` counterparts. pattern is working.
|
|
84
|
+
|
|
85
|
+
- [ ] lint rule to enforce: `_dev*` symbols from service files may only be
|
|
86
|
+
imported by `*.dev.ts` files. use `eslint-plugin-import` `no-restricted-imports`
|
|
87
|
+
or a custom rule.
|
|
88
|
+
- [ ] audit for accidentally-exported `_internal*` helpers (not `_dev*`) - these
|
|
89
|
+
should lose the export keyword or be inlined.
|
|
90
|
+
|
|
91
|
+
### export hygiene
|
|
92
|
+
|
|
93
|
+
services currently leak too much through their public API surface. guiding rules:
|
|
94
|
+
|
|
95
|
+
- a service's public exports are what components and hooks need - no more
|
|
96
|
+
- `export *` is only allowed in shim/barrel files (`e2e/helpers.ts`,
|
|
97
|
+
`e2e/helpers/index.ts`, `src/components/index.tsx`)
|
|
98
|
+
- types used only inside a service file should not be exported
|
|
99
|
+
- when a type is needed in two places, export it from the canonical module
|
|
100
|
+
(usually `src/types/playlist.ts`) not from the service
|
|
101
|
+
|
|
102
|
+
- [ ] audit each service file: list exports, mark which are actually imported
|
|
103
|
+
elsewhere. remove or unexport any that aren't.
|
|
104
|
+
- [ ] `eslint-plugin-import` `no-unused-modules` rule to surface dead exports
|
|
105
|
+
|
|
106
|
+
### SolidJS reactivity patterns
|
|
107
|
+
|
|
108
|
+
we have a recurring class of bugs where reactive values are read in non-reactive
|
|
109
|
+
contexts. canonical mistakes and their fixes:
|
|
110
|
+
|
|
111
|
+
| mistake | symptom | fix |
|
|
112
|
+
| --------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
113
|
+
| destructure `props` | component never updates | use `props.foo` directly, or `splitProps` |
|
|
114
|
+
| `onMount` for derived state | state stale after async resolution | `createEffect` tracks signal deps and re-runs |
|
|
115
|
+
| read signal in event handler, no effect | stale closure over initial value | read inside the handler (closures over signals are fine - they call the getter each time) |
|
|
116
|
+
| `createMemo` with side effects | double-execution, stale deps | memos are for derived values only; side effects go in `createEffect` |
|
|
117
|
+
| spread props onto DOM element | unknown prop warnings + reactivity loss | use `splitProps` to separate known props from rest |
|
|
118
|
+
|
|
119
|
+
concrete example fixed this session: `PlaylistEditPanel` used `onMount` to set
|
|
120
|
+
cover image URL from `props.playlist.imageFilePath`. after page reload,
|
|
121
|
+
`docToPlaylistAsync` resolves `imageFilePath` asynchronously after the panel
|
|
122
|
+
mounts. the fix was `createEffect` which re-runs whenever `props.playlist.*`
|
|
123
|
+
changes. `onMount` is correct only when you genuinely want "run once on mount
|
|
124
|
+
with whatever the initial prop value is."
|
|
125
|
+
|
|
126
|
+
- [ ] add `eslint-plugin-solid` to the project
|
|
127
|
+
- `solid/no-destructure` - catches prop destructuring
|
|
128
|
+
- `solid/reactivity` - warns on reactive values used outside reactive scope
|
|
129
|
+
- `solid/event-handlers` - naming conventions
|
|
130
|
+
- evaluate `solid/prefer-show` - probably off for now (subjective)
|
|
131
|
+
- [ ] fix any violations that `eslint-plugin-solid` surfaces after adding it
|
|
132
|
+
|
|
133
|
+
### .dev.ts pattern (mock isolation)
|
|
134
|
+
|
|
135
|
+
services that need dev/test hooks follow this split:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
src/services/fooService.ts - production code. exports _dev* only as injection points
|
|
139
|
+
src/services/fooService.dev.ts - mock implementations + registerFooDevHooks()
|
|
140
|
+
src/dev-hooks.ts - thin glue: imports and calls all register*DevHooks()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`dev-hooks.ts` is dynamically imported DEV-only:
|
|
144
|
+
`void import("../dev-hooks.js")` inside a `if (import.meta.env.DEV)` guard
|
|
145
|
+
in `src/components/index.tsx`. never present in production builds.
|
|
146
|
+
|
|
147
|
+
the `window.__*` hooks declared in `src/types/global.d.ts` are the e2e-facing
|
|
148
|
+
API; `e2e/helpers/hooks.ts` provides typed wrappers over them.
|
|
149
|
+
|
|
150
|
+
### e2e test tagging + scripts
|
|
151
|
+
|
|
152
|
+
tests are tagged by transport dependency:
|
|
153
|
+
|
|
154
|
+
| tag | meaning | script |
|
|
155
|
+
| ------- | ----------------------------------------------------- | --------------- |
|
|
156
|
+
| (none) | real UI + real IDB/OPFS, no transport mock | `test:e2e:real` |
|
|
157
|
+
| `@mock` | uses `window.__mockBlobFetch` - transport substituted | `test:e2e:mock` |
|
|
158
|
+
| `@p2p` | real two-browser iroh transfer | `test:e2e:p2p` |
|
|
159
|
+
|
|
160
|
+
`@mock` tests are fast (run in-process, no relay), cover state-machine edge cases.
|
|
161
|
+
`@p2p` tests are slow (relay dependent, 30-120s), prove the wire protocol works.
|
|
162
|
+
both are needed; they test different things.
|
|
163
|
+
|
|
164
|
+
time-acceleration hooks (`seekTo`, `triggerTrackEnd`, `triggerAudioError`) are
|
|
165
|
+
NOT considered mock transport - don't tag tests that only use these as `@mock`.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## pick up here
|
|
172
|
+
|
|
173
|
+
last session (2026-06-12): 80/80 tests passing. mock infrastructure complete.
|
|
174
|
+
ROADMAP restructured. `PlaylistEditPanel` cover-image `onMount`→`createEffect` bug fixed.
|
|
175
|
+
|
|
176
|
+
**next immediate tasks (in rough priority order):**
|
|
177
|
+
|
|
178
|
+
1. **`eslint-plugin-solid`** - add the plugin, fix violations. fast win, improves
|
|
179
|
+
all future SolidJS work. see "code conventions: SolidJS reactivity patterns". update .github/copilot-instructions.md and try to make it a regular point to review and address lint issues (which might mean tuning lint rules config)
|
|
180
|
+
|
|
181
|
+
2. **initial blob prefetch on share link open** - wire `prefetchUpcoming()` call
|
|
182
|
+
into `sharingService.ts` after `handleShareFragment()` resolves. add one `@mock`
|
|
183
|
+
e2e test. see "active work: initial blob prefetch on share link open".
|
|
184
|
+
|
|
185
|
+
3. **song row progress fraction** - `blobTransferService` already fires `onProgress`.
|
|
186
|
+
wire into `SongRow` as a thin fill or `data-download-fraction` attr + test.
|
|
187
|
+
see "active work: song row download progress fraction".
|
|
188
|
+
|
|
189
|
+
4. **export audit** - run `eslint-plugin-import no-unused-modules`, trim dead exports.
|
|
190
|
+
low risk, improves readability before modularization work starts. also add ts-prune and madge circular dep check (see spume npm scripts for example) npm script and figure out a strategy, like lint runs, to review and address issues occasionally.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## active work
|
|
195
|
+
|
|
196
|
+
### p2p transport: UX gaps
|
|
197
|
+
|
|
198
|
+
#### initial blob prefetch on share link open
|
|
199
|
+
|
|
200
|
+
when a guest opens a share link, they should hear music as soon as possible.
|
|
201
|
+
the knock + automerge sync flow works, but blob transfers don't start until
|
|
202
|
+
playback is triggered.
|
|
203
|
+
|
|
204
|
+
- [ ] after `handleShareFragment()` resolves and the playlist doc is confirmed,
|
|
205
|
+
call `prefetchUpcoming(playlist, firstSongId, totalPlaylistDuration)` to
|
|
206
|
+
kick off blob transfers immediately
|
|
207
|
+
- [ ] the trigger point is in `sharingService.ts` - wire into the existing
|
|
208
|
+
`pendingShareDocId` watch or a new `onPlaylistDocReady` callback
|
|
209
|
+
- [ ] e2e test: synthetic share fragment pointing at a local doc, assert
|
|
210
|
+
`data-download-state` starts then clears for the first song (mock transport)
|
|
211
|
+
- **modularization note**: the prefetch call is already in `blobTransferService.ts`;
|
|
212
|
+
only the trigger point in `sharingService.ts` needs wiring
|
|
213
|
+
|
|
214
|
+
#### player loading state on share link auto-play
|
|
215
|
+
|
|
216
|
+
when a guest opens a share link and the first blob isn't local yet, the player
|
|
217
|
+
should communicate why playback hasn't started rather than appearing broken.
|
|
218
|
+
|
|
219
|
+
- [x] `AudioPlayer.tsx` shows spinner + `aria-busy="true"` when `isFetchingBlob()`
|
|
220
|
+
is true - this covers the blob-fetch case already
|
|
221
|
+
- [ ] the blob-fetch spinner and the audio-buffering spinner are currently the same
|
|
222
|
+
visual - consider a distinct "waiting for p2p" indicator (tooltip, color, or
|
|
223
|
+
icon variant) so the user knows it's a network wait, not a decode issue
|
|
224
|
+
- [ ] if the initial blob fetch errors, show the error state from `blobDownloadStates`
|
|
225
|
+
on the play button rather than resetting to idle
|
|
226
|
+
- **note**: initial prefetch (above) landing first makes this properly testable
|
|
227
|
+
|
|
228
|
+
#### player stall detection
|
|
229
|
+
|
|
230
|
+
distinct from the blob-fetch spinner (pre-play). this covers post-fetch stalls
|
|
231
|
+
where the blob arrived but the audio element can't buffer fast enough.
|
|
232
|
+
|
|
233
|
+
- [ ] if `audio.readyState < HAVE_FUTURE_DATA` persists for > N seconds after
|
|
234
|
+
`play()` is called, surface a "stalling" indicator on the player button
|
|
235
|
+
- [ ] distinct from the normal buffering spinner - ideally a different icon or
|
|
236
|
+
tooltip explaining the stall
|
|
237
|
+
- [ ] `__triggerStall(durationMs)` dev hook for unit testing; skip for e2e
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### p2p transport: testing + fixtures
|
|
242
|
+
|
|
243
|
+
#### longer fixtures for realistic p2p testing
|
|
244
|
+
|
|
245
|
+
current committed fixtures top out at ~10s. longer tracks are needed to test
|
|
246
|
+
prefetch windows and rendering under sustained transfer load.
|
|
247
|
+
|
|
248
|
+
- [ ] add `long-drone-5min.mp3` to `e2e/fixtures/generate.mjs` - use ffmpeg
|
|
249
|
+
with `-t 300`, low bitrate mono to keep file size manageable
|
|
250
|
+
- [ ] add a fixture with a specific total duration suitable for testing the
|
|
251
|
+
"first 30 minutes" prefetch window boundary
|
|
252
|
+
- [ ] once longer fixtures exist: assert that `prefetchUpcoming` stops scheduling
|
|
253
|
+
new blobs once the 30-min budget is exhausted
|
|
254
|
+
|
|
255
|
+
#### remaining mocked transport test scenarios
|
|
256
|
+
|
|
257
|
+
`e2e/p2p-states.spec.ts` covers timeout, pending, retry, prefetch triggering,
|
|
258
|
+
cancellation, and seek recalculation. gaps:
|
|
259
|
+
|
|
260
|
+
- [ ] **progress chunks**: `__mockBlobFetch({ type: "progress", chunks: 5,
|
|
261
|
+
msPerChunk: 100 })` - assert `data-download-state` is present during fetch
|
|
262
|
+
and clears on completion. (progress fraction UI is a separate item below;
|
|
263
|
+
this test only needs state transitions.)
|
|
264
|
+
- [ ] once a progress fraction display exists in `SongRow`: assert the fraction
|
|
265
|
+
increments during a `{ type: "progress" }` mock fetch
|
|
266
|
+
|
|
267
|
+
#### real two-browser @p2p tests
|
|
268
|
+
|
|
269
|
+
mock tests cover state-machine edge cases fast; real tests prove the transport
|
|
270
|
+
actually works end-to-end. both are needed.
|
|
271
|
+
|
|
272
|
+
currently in `e2e/sharing.spec.ts` + `e2e/sharing-access.spec.ts`:
|
|
273
|
+
|
|
274
|
+
- host creates playlist, shares link, guest opens - playlist visible
|
|
275
|
+
- knock/public mode toggle (single-browser; no actual blob transfer yet)
|
|
276
|
+
|
|
277
|
+
new tests to add:
|
|
278
|
+
|
|
279
|
+
- [ ] **blob transfer smoke** (tagged `@p2p`, timeout 120s): host drops
|
|
280
|
+
`tone-440hz-2s.wav`, guest opens share link, wait for `data-download-state`
|
|
281
|
+
to clear, assert song plays. the canonical "does p2p actually work" test.
|
|
282
|
+
- [ ] **knock blocks then unblocks** (`@p2p`): host in knock mode, guest opens
|
|
283
|
+
share link, assert `data-download-state="error"` (knock_required), host
|
|
284
|
+
approves, guest retries, blob arrives
|
|
285
|
+
- [ ] **host offline mid-transfer** (`@p2p`): guest starts fetching, host closes
|
|
286
|
+
browser, assert guest shows `data-download-state="error"` gracefully
|
|
287
|
+
- [ ] **prefetch before playback** (`@p2p`): host drops 3 short songs, guest
|
|
288
|
+
waits for all 3 `data-download-state` attrs to clear (prefetch window),
|
|
289
|
+
then plays - assert no stall
|
|
290
|
+
|
|
291
|
+
**infrastructure note**: once blob transfer protocol is in `freqhole-api-client`,
|
|
292
|
+
the e2e harness for `@p2p` tests can be shared: `tomb/` supplies the iroh relay
|
|
293
|
+
and midden WASM build; playlistz e2e imports from a shared `@freqhole/e2e-utils`
|
|
294
|
+
package. mocked `@mock` tests stay in `freqhole/playlistz/e2e/` and need no tomb
|
|
295
|
+
infrastructure.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### share + access control UX
|
|
300
|
+
|
|
301
|
+
#### per-playlist visibility modes
|
|
302
|
+
|
|
303
|
+
current: one global mode (public / knock) applies to all playlists.
|
|
304
|
+
|
|
305
|
+
- [ ] `PlaylistDoc` gains a `visibility: "public" | "knock" | "private"` field
|
|
306
|
+
- [ ] `sharePolicy` in `automergeRepo.ts` checks per-doc visibility before
|
|
307
|
+
announcing to a peer
|
|
308
|
+
- [ ] `list_playlists` and `blob_request` handlers check per-doc visibility
|
|
309
|
+
instead of global settings mode
|
|
310
|
+
- [ ] ui: visibility toggle in the share panel, per-playlist
|
|
311
|
+
- [ ] migration: docs without `visibility` default to global mode setting
|
|
312
|
+
|
|
313
|
+
#### knock flow UX + two-browser coverage
|
|
314
|
+
|
|
315
|
+
- [ ] knock inbox: show pending knock with peer node id + optional display name
|
|
316
|
+
- [ ] approval ui: one-click accept / deny with "remember this peer" option
|
|
317
|
+
- [ ] `aria-live` region on knock inbox for screen readers
|
|
318
|
+
- [ ] two-browser `@p2p` test: knock mode, guest knocks, host approves, guest streams
|
|
319
|
+
- [ ] two-browser `@p2p` test: public mode, guest streams immediately
|
|
320
|
+
|
|
321
|
+
#### invite codes
|
|
322
|
+
|
|
323
|
+
- [ ] short-lived signed tokens: `${docId}:${nonce}:${expiry}` (HMAC) that
|
|
324
|
+
pre-authorise a peer without a knock
|
|
325
|
+
- [ ] reusable invite links: same shape, no nonce expiry, revocable by host
|
|
326
|
+
- [ ] share panel: "create invite link" button copies a `#invite/...` fragment
|
|
327
|
+
- [ ] invite link open flow: validates token, adds peer to ACL, auto-selects playlist
|
|
328
|
+
- [ ] revocation: stored in doc's `invites` map, host can invalidate
|
|
329
|
+
|
|
330
|
+
#### collaborative playlists
|
|
331
|
+
|
|
332
|
+
- [ ] per-playlist `collaborative: boolean` flag in `PlaylistDoc`
|
|
333
|
+
- [ ] when true: all peers with access can mutate the doc; "collaborative" badge;
|
|
334
|
+
edit panel shows `addedBy: string` per song
|
|
335
|
+
- [ ] when false: originating peer only; "fork this playlist" button in share panel
|
|
336
|
+
for non-owned playlists
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### ui + tooltips
|
|
341
|
+
|
|
342
|
+
no toast UI in this app. prefer `title` attributes and inline tooltip-style
|
|
343
|
+
affordances. kobalte may be worth adding for accessible popover/tooltip
|
|
344
|
+
primitives if the complexity warrants it.
|
|
345
|
+
|
|
346
|
+
#### zip download: p2p fetch state
|
|
347
|
+
|
|
348
|
+
- [ ] if not all blobs are locally cached when download-zip is pressed: show a
|
|
349
|
+
spinner on the button and set `title="waiting on p2p fetch..."` while
|
|
350
|
+
missing blobs are fetched
|
|
351
|
+
- [ ] if pressed again while waiting: show a confirmation tooltip (not modal) -
|
|
352
|
+
"not all songs are downloaded yet. download anyway?" with confirm/cancel
|
|
353
|
+
|
|
354
|
+
#### song row download progress fraction
|
|
355
|
+
|
|
356
|
+
- [ ] `blobTransferService` already fires `onProgress(fraction)` callbacks.
|
|
357
|
+
wire this into `SongRow` as a fill on the duration cell (or a thin bar)
|
|
358
|
+
so large-file downloads show incremental progress
|
|
359
|
+
- [ ] `data-download-fraction` attribute for e2e assertions
|
|
360
|
+
|
|
361
|
+
#### other polish
|
|
362
|
+
|
|
363
|
+
- [ ] all-playlists panel: search input when `playlists().length > 10`;
|
|
364
|
+
client-side filter; "new playlist" row stays sticky
|
|
365
|
+
- [ ] `PanelMiniHeader`: inline title editing and hover cover image
|
|
366
|
+
- [ ] share panel "open a share link" input: after resolving, show correct
|
|
367
|
+
player state (ties into initial prefetch above)
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### modularization: concrete next steps
|
|
372
|
+
|
|
373
|
+
each item is independently dispatchable. see "direction" above for the overall
|
|
374
|
+
package layering goal.
|
|
375
|
+
|
|
376
|
+
#### extract node lifecycle to freqhole-api-client
|
|
377
|
+
|
|
378
|
+
scope: `tomb/client-codegen/freqhole-api-client/`. no UI changes.
|
|
379
|
+
|
|
380
|
+
`playlistz/src/services/p2pService.ts` handles node startup, identity, ALPN
|
|
381
|
+
registration, and leader election. this logic is app-agnostic.
|
|
382
|
+
|
|
383
|
+
- [ ] move `startP2P`, `stopP2P`, `getNode`, `getIdentity`, `waitForNode`,
|
|
384
|
+
`onLeadershipChange`, `hasExistingIdentity` into
|
|
385
|
+
`freqhole-api-client/src/p2p/node.ts`
|
|
386
|
+
- [ ] `playlistz/p2pService.ts` becomes a thin wrapper that supplies the accept
|
|
387
|
+
loop callback and app-specific ALPN handlers
|
|
388
|
+
- [ ] spume can adopt the same shared node lifecycle
|
|
389
|
+
- [ ] update `freqhole-api-client` package exports + version
|
|
390
|
+
|
|
391
|
+
#### extract blob transfer protocol to freqhole-api-client
|
|
392
|
+
|
|
393
|
+
scope: `tomb/client-codegen/freqhole-api-client/` + `playlistz/src/services/`.
|
|
394
|
+
|
|
395
|
+
the BiStream wire protocol lives in `blobTransferService.ts`; the IDB/storage
|
|
396
|
+
layer is already in `freqhole-api-client`.
|
|
397
|
+
|
|
398
|
+
- [ ] extract `fetchBlobFromPeer` + `serveBlobRequest` (BiStream message exchange)
|
|
399
|
+
into `freqhole-api-client/src/playlistz/blobProtocol.ts`
|
|
400
|
+
- [ ] `blobTransferService.ts` retains app-level logic only: prefetch scheduling,
|
|
401
|
+
state signals (`blobDownloadStates`), inflight dedup, timeout, retry
|
|
402
|
+
- [ ] spume's blob transfer can adopt the shared protocol functions
|
|
403
|
+
- [ ] all existing `@mock` and `@p2p` e2e tests must continue to pass
|
|
404
|
+
|
|
405
|
+
#### create @freqhole/solid package
|
|
406
|
+
|
|
407
|
+
scope: new package in `tomb/` monorepo. no app-code changes initially.
|
|
408
|
+
|
|
409
|
+
a SolidJS-specific layer over `freqhole-api-client`. exports reactive wrappers
|
|
410
|
+
and hooks that both playlistz and spume can use.
|
|
411
|
+
|
|
412
|
+
- [ ] scaffold `tomb/packages/solid/` (or alongside `freqhole-api-client`)
|
|
413
|
+
with its own `package.json`, `tsconfig.json`, `vite.config.ts`
|
|
414
|
+
- [ ] move `usePlaylistManager`-shaped hook into the package (playlistz version
|
|
415
|
+
as reference; make it accept a `PlaylistDoc` observable rather than
|
|
416
|
+
app-specific state)
|
|
417
|
+
- [ ] reactive IDB wrapper: signal that re-emits when a doc handle fires a change
|
|
418
|
+
- [ ] media session bridge: `navigator.mediaSession` plumbing with no SolidJS
|
|
419
|
+
dep in `freqhole-api-client`, thin SolidJS `createEffect` wrapper in
|
|
420
|
+
`@freqhole/solid`
|
|
421
|
+
- [ ] both playlistz and spume depend on the package; no duplication
|
|
422
|
+
|
|
423
|
+
#### audit tomb/playlistz/ divergence
|
|
424
|
+
|
|
425
|
+
scope: read-only audit, then decision. low risk.
|
|
426
|
+
|
|
427
|
+
`tomb/playlistz/src/services/` is an earlier snapshot of playlistz services.
|
|
428
|
+
it lacks `p2pService.ts`, `blobTransferService.ts`, `streamingAudioService.ts`.
|
|
429
|
+
|
|
430
|
+
- [ ] run a diff: list which service files are in tomb/playlistz/ only, in
|
|
431
|
+
freqhole/playlistz/ only, and in both
|
|
432
|
+
- [ ] for files in both: diff them and note which direction is ahead
|
|
433
|
+
- [ ] output: a short audit doc or table summarising what to keep, migrate, or
|
|
434
|
+
drop. then this roadmap item updates with concrete action items.
|
|
435
|
+
- [ ] longer-term goal: `tomb/playlistz/` depends on `@freqhole/solid` rather
|
|
436
|
+
than carrying its own service copies
|
|
437
|
+
|
|
438
|
+
#### create @freqhole/e2e-utils package
|
|
439
|
+
|
|
440
|
+
scope: new package; no changes to existing tests.
|
|
441
|
+
|
|
442
|
+
`e2e/helpers/` contains utilities generic enough to share:
|
|
443
|
+
`makeWav`, `dropFiles`, `waitForApp`, `resetAppState`, `loadCommittedAudioFixtures`.
|
|
444
|
+
|
|
445
|
+
- [ ] scaffold `@freqhole/e2e-utils` package (alongside `freqhole-api-client`
|
|
446
|
+
or in a separate `packages/` dir)
|
|
447
|
+
- [ ] move the non-app-specific helpers from `e2e/helpers/media.ts` and
|
|
448
|
+
`e2e/helpers/app.ts` into the package
|
|
449
|
+
- [ ] playlistz `e2e/helpers/` becomes a thin wrapper that re-exports from
|
|
450
|
+
the package and adds app-specific helpers (`createPlaylistViaUI`, etc.)
|
|
451
|
+
- [ ] `tomb/` e2e suites can then import `@freqhole/e2e-utils` directly
|
|
452
|
+
|
|
453
|
+
#### audio layer convergence
|
|
454
|
+
|
|
455
|
+
scope: multi-step, depends on `@freqhole/solid` existing first.
|
|
456
|
+
|
|
457
|
+
- [ ] extract `navigator.mediaSession` plumbing into
|
|
458
|
+
`freqhole-api-client/src/audio/mediaSession.ts` (no SolidJS deps)
|
|
459
|
+
- [ ] thin `@freqhole/solid` wrapper: `createEffect` that syncs signals to
|
|
460
|
+
the media session API
|
|
461
|
+
- [ ] longer term: playlistz `audioService.ts` adopts spume's backend
|
|
462
|
+
abstraction (`playerState`, `backends/`) rather than its own raw
|
|
463
|
+
`HTMLAudioElement` wiring
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## backlog
|
|
468
|
+
|
|
469
|
+
items that are real but not the current focus.
|
|
470
|
+
|
|
471
|
+
- **eslint-plugin-solid**: add the plugin and fix any violations. see "code
|
|
472
|
+
conventions" above for the specific rules and what to look for.
|
|
473
|
+
- **export audit**: each service file has too many exports. run
|
|
474
|
+
`eslint-plugin-import no-unused-modules` and remove dead exports.
|
|
475
|
+
- **songReactivity.ts workaround**: `triggerSpecificSongUpdate` is a workaround
|
|
476
|
+
for `SongRow` not reacting to doc changes directly. proper fix: derive from a
|
|
477
|
+
`playlistSongs()` signal (deferred to `@freqhole/solid` work).
|
|
478
|
+
- **access-control two-browser tests**: `sharing-access.spec.ts` covers mode
|
|
479
|
+
toggles single-browser only. real two-browser knock/unblock tests are queued
|
|
480
|
+
under access control above.
|
|
481
|
+
- **playback position persistence**: `savePlaybackPosition` exists in
|
|
482
|
+
`audioService.ts`; no e2e test that a reload resumes from saved position.
|
|
483
|
+
- **blob GC e2e coverage**: deletion logic is implemented; no e2e test yet.
|
|
484
|
+
- **repeat + shuffle e2e coverage**: signals exist, no e2e tests yet.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## completed (reference)
|
|
489
|
+
|
|
490
|
+
### ui / panels
|
|
491
|
+
|
|
492
|
+
**sidebar refactor (§1)**
|
|
493
|
+
|
|
494
|
+
- `AllPlaylistsPanel.tsx` replaces `PlaylistSidebar.tsx` (deleted)
|
|
495
|
+
- hamburger toggle, `PanelMiniHeader` shared component, `MarqueeText`, row
|
|
496
|
+
actions (play/edit/share/download), `EmptyState` for fresh installs
|
|
497
|
+
- sidebar state removed from `useUIState.ts`
|
|
498
|
+
- e2e: `e2e/all-playlists.spec.ts` (10 tests)
|
|
499
|
+
|
|
500
|
+
**share link auto-play (§3a)**
|
|
501
|
+
|
|
502
|
+
- after `handleShareFragment()`: select playlist, auto-play if idle,
|
|
503
|
+
`pendingShareDocId` watches `playlists()` reactively for docs still syncing
|
|
504
|
+
|
|
505
|
+
**share panel inline layout (§3b)**
|
|
506
|
+
|
|
507
|
+
- share panel animates in the song-rows area (same pattern as edit panel)
|
|
508
|
+
- share button in playlist action row; `PlaylistSharePanel.tsx` inline with
|
|
509
|
+
`< >` nav, copy button, mode toggle
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### p2p transport + state machine
|
|
514
|
+
|
|
515
|
+
**per-song download state signal (§12a)**
|
|
516
|
+
|
|
517
|
+
- `blobDownloadStates`: reactive `ReadonlyMap<sha256, "downloading" | "pending" | "error">`
|
|
518
|
+
- `SongRow`: downloading - `text-blue-400 animate-pulse`; pending - `text-gray-500`;
|
|
519
|
+
error - `text-red-400 cursor-pointer`; `data-download-state` attr for e2e
|
|
520
|
+
- error retry: clicking duration cell calls `fetchSongBlob(song())` directly
|
|
521
|
+
|
|
522
|
+
**audio player loading state (§12b)**
|
|
523
|
+
|
|
524
|
+
- `isFetchingBlob()` checks `blobDownloadStates` for current song sha
|
|
525
|
+
- spinner + `aria-busy="true"` while blob is fetching or audio is buffering
|
|
526
|
+
|
|
527
|
+
**prefetch window (§12c)**
|
|
528
|
+
|
|
529
|
+
- `PREFETCH_CONCURRENCY = 3`, parallel batches with `Promise.allSettled`
|
|
530
|
+
- `seeked` event re-calls `prefetchUpcoming` with recalculated remaining time
|
|
531
|
+
- `prefetchRun` counter cancels stale runs on playlist switch
|
|
532
|
+
- `pendingShas` + `clearPending()` for pending-state cleanup on cancellation
|
|
533
|
+
|
|
534
|
+
**fetch timeout + retry (§15a-c)**
|
|
535
|
+
|
|
536
|
+
- `BlobDownloadState` includes `"pending"` (queued but not yet started)
|
|
537
|
+
- `BLOB_FETCH_TIMEOUT_MS = 30_000` (configurable via `_devSetBlobFetchTimeout`);
|
|
538
|
+
applies to both dev-override and real p2p paths
|
|
539
|
+
- `_devFetchBlobBySha` + `window.__fetchBlobBySha` for programmatic retry in tests
|
|
540
|
+
|
|
541
|
+
**dev hooks + .dev.ts mock isolation pattern**
|
|
542
|
+
|
|
543
|
+
- `src/dev-hooks.ts`: 18-line registration glue, dynamically imported DEV-only
|
|
544
|
+
(`void import("../dev-hooks.js")` in `src/components/index.tsx`). never in prod.
|
|
545
|
+
- each service that needs dev/test hooks has a paired `service.dev.ts` file:
|
|
546
|
+
- `src/services/audioService.dev.ts` - exports `registerAudioDevHooks()`
|
|
547
|
+
- `src/services/blobTransferService.dev.ts` - exports `registerBlobDevHooks()`,
|
|
548
|
+
contains `MockBlobBehaviour` type + `mockFetchBlob()` implementation
|
|
549
|
+
- `_dev*` exports in the service file are injection points; only their `.dev.ts`
|
|
550
|
+
counterpart imports them
|
|
551
|
+
- `src/types/global.d.ts` declares all `Window.__*` hook props
|
|
552
|
+
- hooks: `__seekTo`, `__triggerTrackEnd`, `__triggerAudioError`, `__evictBlob`,
|
|
553
|
+
`__mockBlobFetch`, `__clearMockBlobFetch`, `__setBlobFetchTimeout`, `__fetchBlobBySha`
|
|
554
|
+
|
|
555
|
+
**e2e helpers split**
|
|
556
|
+
|
|
557
|
+
- `e2e/helpers/media.ts` - `makeWav()`, fixture loading, no `Page` dep, pure Node
|
|
558
|
+
- `e2e/helpers/app.ts` - `resetAppState`, `createPlaylistViaUI`, `addSongs`,
|
|
559
|
+
`makePng`, `dropFiles`, `waitForApp`, `setPlaylistCover`
|
|
560
|
+
- `e2e/helpers/hooks.ts` - typed `window.__*` wrappers + `MockBlobBehaviour` type
|
|
561
|
+
- `@mock` convention docs
|
|
562
|
+
- `e2e/helpers/index.ts` - barrel re-export of all three
|
|
563
|
+
- `e2e/helpers.ts` - 10-line re-export shim so existing `"./helpers.js"` imports
|
|
564
|
+
continue to work without changes
|
|
565
|
+
|
|
566
|
+
**e2e: audio player + mocked p2p (§12d, §12e, §15d)**
|
|
567
|
+
|
|
568
|
+
- `e2e/audio-player.spec.ts` (7 tests): autoplay-next, triggerTrackEnd, end-of-playlist
|
|
569
|
+
stopped state, real 2s fixture playthrough, error recovery, delayed blob state,
|
|
570
|
+
mock error state. two tests tagged `@mock`.
|
|
571
|
+
- `e2e/p2p-states.spec.ts` (6 tests, all tagged `@mock`): pending during stall,
|
|
572
|
+
timeout→error, retry affordance, prefetch triggered on play, prefetch cancelled
|
|
573
|
+
on playlist switch, seek recalculation
|
|
574
|
+
|
|
575
|
+
**playlist cover image reactive after reload**
|
|
576
|
+
|
|
577
|
+
- `PlaylistEditPanel.tsx`: moved image URL loading from `onMount` to `createEffect`
|
|
578
|
+
- root cause: after page reload, `docToPlaylistAsync` resolves `imageFilePath`
|
|
579
|
+
asynchronously from OPFS/Cache. `onMount` ran once before resolution and saw
|
|
580
|
+
`undefined`. `createEffect` re-runs whenever `props.playlist.imageFilePath`
|
|
581
|
+
becomes non-null.
|
|
582
|
+
- lesson: `onMount` = "run once with initial prop value". `createEffect` = "run
|
|
583
|
+
whenever tracked signals change". when a prop may resolve asynchronously,
|
|
584
|
+
`createEffect` is almost always the right choice.
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
**dedup on import (§11b)**
|
|
589
|
+
|
|
590
|
+
- `addSongToPlaylist` checks sha256 before writing; same-sha songs skip upsert
|
|
591
|
+
- applies to both file-drop and zip re-import
|
|
592
|
+
- e2e: `zip reimport dedup` test in `zip-bundle.spec.ts`
|
|
593
|
+
|
|
594
|
+
**blob GC on playlist delete (§11d)**
|
|
595
|
+
|
|
596
|
+
- `deletePlaylist` enumerates sha refs, runs `shaRefsExcluding(docId)`, removes
|
|
597
|
+
unreferenced blobs (best-effort, `Promise.allSettled`)
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
### zip download (§5b-c)
|
|
602
|
+
|
|
603
|
+
- songs deduped by sha256 in zip; node_ids excluded
|
|
604
|
+
- `e2e/zip-bundle.spec.ts`: download trigger, zip structure, reimport roundtrip,
|
|
605
|
+
standalone http server, audio playback, `--http` CLI mode, dedup test
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
### access control (§4a-b)
|
|
610
|
+
|
|
611
|
+
- `list_playlists` + `blob_request` check `settings.mode` + `getAccessGrant`
|
|
612
|
+
- `e2e/sharing-access.spec.ts`: mode toggle, default knock-first, persistence,
|
|
613
|
+
knock inbox empty state, browse-a-peer section
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
### state correctness (§10a-b)
|
|
618
|
+
|
|
619
|
+
- `selectedPlaylistId` is the single writable signal; `selectedPlaylist` is a memo;
|
|
620
|
+
stale overwrites structurally impossible
|
|
621
|
+
- `window.__processFiles` dev hook in `components/index.tsx` (DEV only)
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
### logging (§9)
|
|
626
|
+
|
|
627
|
+
- `src/utils/log.ts`: trace < debug < info < warn < error
|
|
628
|
+
- runtime: `localStorage.logLevel` / `localStorage.logFilter`
|
|
629
|
+
- build-time: `VITE_LOG_LEVEL` + `VITE_LOG_FILTER`; vitest sets `VITE_LOG_LEVEL=error`
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
### e2e test files
|
|
634
|
+
|
|
635
|
+
| file | tests | tags | notes |
|
|
636
|
+
| --------------------------------- | ----- | --------- | ------------------------------------- |
|
|
637
|
+
| `e2e/sharing.spec.ts` | - | @p2p | two-browser, real iroh |
|
|
638
|
+
| `e2e/sharing-access.spec.ts` | - | - | access control, single-browser |
|
|
639
|
+
| `e2e/share-link-autoplay.spec.ts` | - | - | synthetic share fragment |
|
|
640
|
+
| `e2e/all-playlists.spec.ts` | 10 | - | hamburger panel |
|
|
641
|
+
| `e2e/song-cache-state.spec.ts` | 17 | - | blob cache states |
|
|
642
|
+
| `e2e/zip-bundle.spec.ts` | ~20 | - | zip download + standalone roundtrip |
|
|
643
|
+
| `e2e/audio-player.spec.ts` | 7 | 2x @mock | playback + mock blob |
|
|
644
|
+
| `e2e/p2p-states.spec.ts` | 6 | all @mock | mocked p2p transport scenarios |
|
|
645
|
+
| `e2e/playlist-crud.spec.ts` | 5 | - | create, edit, cover image persistence |
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## notes
|
|
650
|
+
|
|
651
|
+
### e2e selectors and test patterns
|
|
652
|
+
|
|
653
|
+
- `data-testid="song-duration"` - one per song row; also has `data-sha256` and
|
|
654
|
+
`data-download-state` attrs. always use this to select song row durations
|
|
655
|
+
(the playlist total-duration badge also shows time strings).
|
|
656
|
+
- `data-testid="btn-play-playlist"` - AudioPlayer button;
|
|
657
|
+
`aria-pressed="true"/"false"` (string); `aria-busy` while fetching/buffering
|
|
658
|
+
- `data-testid="now-playing-title"` - shows current song title (only when playing)
|
|
659
|
+
- `data-testid="app-ready"` - app boot sentinel (visually hidden `<h1>`)
|
|
660
|
+
- `data-testid="edit-panel"` - playlist edit panel root (for scoping child locators)
|
|
661
|
+
- clicking song rows: if the action-buttons overlay intercepts pointer events
|
|
662
|
+
(visible on hover/play state), use `page.evaluate` / `dispatchEvent("click")`
|
|
663
|
+
rather than `.click()` or `{ force: true }` - force clicks hit the wrong target
|
|
664
|
+
- `fetchBlobBySha(page, sha256)` programmatic retry (bypasses overlay entirely)
|
|
665
|
+
|
|
666
|
+
### subagent dispatch notes
|
|
667
|
+
|
|
668
|
+
tasks in "modularization: concrete next steps" are designed to be independently
|
|
669
|
+
dispatched. when dispatching, provide:
|
|
670
|
+
|
|
671
|
+
1. the task section from this doc as context
|
|
672
|
+
2. the relevant source files to read first
|
|
673
|
+
3. the constraint "all existing tests must pass after changes"
|
|
674
|
+
|
|
675
|
+
tasks that touch `tomb/` require the tomb workspace context too:
|
|
676
|
+
|
|
677
|
+
- `tomb/client-codegen/freqhole-api-client/` is the shared package directory
|
|
678
|
+
- `tomb/client/spume/` is the spume frontend
|
|
679
|
+
|
|
680
|
+
for linting tasks, the full list of errors from `eslint --max-warnings 0` is
|
|
681
|
+
useful starting context so the subagent doesn't need to discover violations first.
|
|
682
|
+
|
|
683
|
+
### general
|
|
684
|
+
|
|
685
|
+
- iroh relay/nat is known flaky in two-browser tests: sometimes 8 min, sometimes
|
|
686
|
+
13s. not fixable in test code alone.
|
|
687
|
+
- `fileProcessingService.ts` parses filename only (no ID3 tags). cover art via
|
|
688
|
+
`imageService.extractAlbumArt` (ID3v2 APIC parser).
|
|
689
|
+
- `getBlobObjectURL` caches URLs internally; do not revoke them.
|
|
690
|
+
- `PREFETCH_CONCURRENCY = 3`; `BLOB_FETCH_TIMEOUT_MS = 30_000` (let, configurable
|
|
691
|
+
via `_devSetBlobFetchTimeout`). timeout applies to both dev-override and real p2p paths.
|
|
692
|
+
- `songReactivity.ts`: `triggerSpecificSongUpdate` is a workaround for `SongRow`
|
|
693
|
+
not subscribing directly to the doc handle. the proper fix is deriving from a
|
|
694
|
+
`playlistSongs()` signal - but this touches reactivity deeply, defer until
|
|
695
|
+
`@freqhole/solid` work begins.
|