Merge pull request #2271 from PR0M3TH3AN/docs-codebase-alignment-1921547855180611109

📘 Align development docs with codebase behavior
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-09 14:42:24 -05:00
commit cf3d021ec8
11 files changed
+130 -36

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ For any nostr related work, please review the nip documentation located in /docs
* **Goal:** Always deliver smooth playback while keeping hosting costs low.
* **Primary transport:** A hosted video URL (MP4/WebM/HLS/DASH) that the `<video>` element can stream directly.
* **Fallback transport:** A WebTorrent magnet that includes browser‑safe trackers plus optional HTTP hints.
* **Runtime behavior:** Call `bitvidApp.playVideoWithFallback({ url, magnet })` in `js/app.js`. This function orchestrates URL probing and magnet fallback, delegating low-level stream handling to `js/services/playbackService.js`. `js/playbackUtils.js` now provides magnet/session helper utilities rather than the playback entry point itself.
* **Runtime behavior:** Call `bitvidApp.playVideoWithFallback({ url, magnet })` (implemented in `js/app/playbackCoordinator.js` and bound to the app instance in `js/app.js`). This function orchestrates URL probing and magnet fallback, delegating low-level stream handling to `js/services/playbackService.js`. `js/playbackUtils.js` now provides magnet/session helper utilities rather than the playback entry point itself.
* **Content contract:** bitvid posts (Nostr kind `30078`) must include a `title` and at least one of `url` or `magnet`. Prefer to publish both along with optional `thumbnail`, `description`, and `mode` fields. When mirroring to NIP‑94 (`kind 1063`), copy the hosted URL and (optionally) the magnet so other clients can discover the same asset.
## Styling & Theming Rules (token-first)
+3 -2
View File
@@ -397,13 +397,14 @@ call the facade to keep API surfaces consistent.
### Nostr video fetch limits
`nostrClient.fetchVideos(options)` now accepts an optional `options.limit` to
control how many video events are requested per relay. The request size defaults
The `nostrClient` uses `subscribeVideos(callback, options)` for the main feed to support buffering and streaming. It accepts an optional `options.limit` to control how many video events are requested per relay. The request size defaults
to `DEFAULT_VIDEO_REQUEST_LIMIT` (150) and is clamped to
`MAX_VIDEO_REQUEST_LIMIT` (500) to avoid accidental firehose requests as the
network grows. Use a smaller limit for lightweight surfaces or a larger value
when running batch jobs that can tolerate additional load.
`nostrClient.fetchVideos(options)` is a legacy wrapper around subscription and is deprecated.
The build command compiles Tailwind with `tailwind.config.cjs`, runs it through
the PostCSS pipeline defined in `postcss.config.cjs` (for autoprefixing), and
emits the purged, minified stylesheet at `css/tailwind.generated.css`. That
+1 -1
View File
@@ -182,7 +182,7 @@ Video posts should treat `videoRootId` as the stable series identifier that rema
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
| DM relay hints (`NOTE_TYPES.DM_RELAY_LIST`) | `10050` | Repeating `['relay', <relay url>]` tags to advertise delivery relays for NIP-17 gift-wrapped DMs | Empty content |
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', <eventId>]` or `['a', <address>]`), and a stable dedupe tag `['d', <view identifier>]`, with optional `['session','true']` when a session actor signs; schema overrides may append extra tags. `['video', ...]` is supported for legacy overrides only. | Optional plaintext message |
| Watch history month (`NOTE_TYPES.WATCH_HISTORY`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', `${WATCH_HISTORY_LIST_IDENTIFIER}:<YYYY-MM>`]` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
| Watch history month (`NOTE_TYPES.WATCH_HISTORY`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', '<YYYY-MM>']` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30000` | `['d', 'subscriptions']`, `['encrypted', 'nip44_v2']` (updated to the negotiated scheme at publish time) | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
| Subscription list backup (`NOTE_TYPES.SUBSCRIPTION_BACKUP`) | `30000` | `['d', <identifier>]`, `['encrypted', 'nip44_v2']` | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
| User block list (`NOTE_TYPES.USER_BLOCK_LIST`) | `10000` | `['d', 'user-blocks']` | **LEGACY**. NIP-04/NIP-44 encrypted JSON array of [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) tag tuples. Migrated to standard Mute List (kind `10000`) which uses public tags for mutes and encrypted content for private blocks. |
+3 -3
View File
@@ -1,10 +1,10 @@
# URL-first playback fallback orchestration
This document explains how `bitvidApp.playVideoWithFallback` in `js/app.js` coordinates hosted URL playback first and falls back to WebTorrent when needed. It also covers the supporting helpers in `js/services/playbackService.js`, `js/playbackUtils.js`, and `js/magnetUtils.js`, including how tracker lists and `ws=`/`xs=` hints are applied.
This document explains how `bitvidApp.playVideoWithFallback` (implemented via `PlaybackCoordinator` in `js/app/playbackCoordinator.js` and bound to the application instance in `js/app.js`) coordinates hosted URL playback first and falls back to WebTorrent when needed. It also covers the supporting helpers in `js/services/playbackService.js`, `js/playbackUtils.js`, and `js/magnetUtils.js`, including how tracker lists and `ws=`/`xs=` hints are applied.
## High-level flow
1. `bitvidApp.playVideoWithFallback({ url, magnet })` trims inputs, ensures the video modal is ready, and constructs a playback session via `playbackService.createSession(...)`.
1. `bitvidApp.playVideoWithFallback({ url, magnet })` (via `PlaybackCoordinator`) trims inputs, ensures the video modal is ready, and constructs a playback session via `playbackService.createSession(...)`.
2. The session (from `js/services/playbackService.js`) derives a canonical magnet payload using `deriveTorrentPlaybackConfig` (from `js/playbackUtils.js`).
3. Playback starts with the hosted URL when `URL_FIRST_ENABLED` is true and a URL is available; otherwise, it goes directly to torrent playback.
4. If URL playback stalls or errors, the session triggers `playViaWebTorrent` with the normalized magnet and optional web seed hints.
@@ -59,7 +59,7 @@ The fallback hand-off happens in these places inside `PlaybackSession.execute()`
```mermaid
sequenceDiagram
participant App as bitvidApp (js/app.js)
participant App as App (Coordinator)
participant Service as PlaybackService (js/services/playbackService.js)
participant Session as PlaybackSession
participant Video as HTMLVideoElement
+14 -2
View File
@@ -927,6 +927,18 @@ export class NostrClient {
return this.connectionManager.makeCountUnsupportedError(relayUrl);
}
resolveEventDTag(event, fallbackEvent = null) {
if (event && event.tags) {
const dTag = getDTagValueFromTags(event.tags);
if (dTag) return dTag;
}
if (fallbackEvent && fallbackEvent.tags) {
const dTag = getDTagValueFromTags(fallbackEvent.tags);
if (dTag) return dTag;
}
return "";
}
applyRootCreatedAt(video) {
if (!video || typeof video !== "object") return;
const rootId = video.videoRootId;
@@ -3068,8 +3080,8 @@ export class NostrClient {
return this.signerManager.disconnectRemoteSigner(options);
}
async ensureSessionActor() {
return this.signerManager.ensureSessionActor();
async ensureSessionActor(...args) {
return this.signerManager.ensureSessionActor(...args);
}
clearStoredSessionActor() {
+39 -2
View File
@@ -325,7 +325,7 @@ export class SignerManager {
return resolveActiveSigner(pubkey);
}
async ensureSessionActor() {
async ensureSessionActor(createIfMissing = false) {
if (this.sessionActor) {
return this.sessionActor;
}
@@ -341,6 +341,40 @@ export class SignerManager {
return storedEntry;
}
if (createIfMissing) {
const tools = (await ensureNostrTools()) || getCachedNostrTools();
if (tools) {
let privateKey;
let pubkey;
if (typeof tools.generatePrivateKey === "function") {
const raw = tools.generatePrivateKey();
if (typeof raw === "string") {
privateKey = raw;
pubkey = tools.getPublicKey(privateKey);
} else {
pubkey = tools.getPublicKey(raw);
privateKey = Array.from(raw)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
} else if (typeof tools.generateSecretKey === "function") {
const secret = tools.generateSecretKey();
pubkey = tools.getPublicKey(secret);
privateKey = Array.from(secret)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
if (privateKey && pubkey) {
const newActor = { privateKey, pubkey };
this.sessionActor = newActor;
persistSessionActorEntry(newActor);
return newActor;
}
}
}
return null;
}
@@ -452,7 +486,10 @@ export class SignerManager {
return { ok: false, error: "extension-missing" };
}
extension = typeof window !== "undefined" ? window.nostr : null;
if (!extension && typeof window !== "undefined") {
extension = window.nostr;
}
const message = resolvePermissionStatusMessage(missing, context);
try {
+15 -1
View File
@@ -140,7 +140,21 @@ export function signEventWithPrivateKey(event, privateKey) {
throw new Error("Missing signing primitives");
}
const finalized = finalizeFn(prepared, normalizedKey);
let secretKey = normalizedKey;
if (typeof Uint8Array !== "undefined") {
try {
const len = normalizedKey.length;
const bytes = new Uint8Array(len / 2);
for (let i = 0; i < len; i += 2) {
bytes[i / 2] = parseInt(normalizedKey.substring(i, i + 2), 16);
}
secretKey = bytes;
} catch (error) {
// Ignore conversion error, secretKey remains string
}
}
const finalized = finalizeFn(prepared, secretKey);
if (
!finalized ||
typeof finalized.id !== "string" ||
+11 -1
View File
@@ -484,7 +484,17 @@ export function clearStoredSessionActor() {
export function isSessionActor(nostrClient) {
const sa = nostrClient?.sessionActor;
return !!sa && sa.source !== "nsec";
if (!sa || sa.source === "nsec") {
return false;
}
const clientPubkey =
typeof nostrClient?.pubkey === "string" ? nostrClient.pubkey : null;
const sessionPubkey = typeof sa.pubkey === "string" ? sa.pubkey : null;
if (clientPubkey && sessionPubkey && clientPubkey !== sessionPubkey) {
return false;
}
return true;
}
function _closeSessionActorDb() {
+3 -1
View File
@@ -700,8 +700,10 @@ export async function publishViewEvent(
return { ok: false, error: "invalid-pointer" };
}
const actorPubkey = await client.ensureSessionActor();
const actorObj = await client.ensureSessionActor(true);
// actorPubkey derived via client.ensureSessionActor()
const actorPubkey = actorObj?.pubkey || (typeof actorObj === "string" ? actorObj : null);
if (!actorPubkey) {
return { ok: false, error: "missing-actor" };
}
+39 -22
View File
@@ -1,29 +1,46 @@
→ Running tests/nostr/integration-remote-flow.test.mjs
VIEW RESULT ERROR: { ok: false, error: 'missing-actor' }
TAP version 13
# Subtest: WebTorrent Regression Tests
# Subtest: probePeers should report healthy if webseed is present even with 0 peers
ok 1 - probePeers should report healthy if webseed is present even with 0 peers
---
duration_ms: 204.415485
type: 'test'
...
# Subtest: probePeers should report unhealthy if no webseed and 0 peers
ok 2 - probePeers should report unhealthy if no webseed and 0 peers
---
duration_ms: 52.544489
type: 'test'
...
1..2
ok 1 - WebTorrent Regression Tests
# Subtest: nostr login + remote signing + publish + watch history integration
not ok 1 - nostr login + remote signing + publish + watch history integration
---
duration_ms: 259.856073
type: 'suite'
duration_ms: 60.800483
type: 'test'
location: '/app/tests/nostr/integration-remote-flow.test.mjs:52:1'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
false !== true
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: true
actual: false
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (file:///app/tests/nostr/integration-remote-flow.test.mjs:272:83)
async Test.run (node:internal/test_runner/test:1054:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
...
1..1
# tests 2
# suites 1
# pass 2
# fail 0
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 269.477515
# duration_ms 72.354406
file:///app/scripts/run-targeted-tests.mjs:44
reject(new Error(`${relativePath} failed with exit code ${code}`));
^
Error: tests/nostr/integration-remote-flow.test.mjs failed with exit code 1
at ChildProcess.<anonymous> (file:///app/scripts/run-targeted-tests.mjs:44:16)
at ChildProcess.emit (node:events:519:28)
at maybeClose (node:internal/child_process:1101:16)
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
Node.js v22.22.0
@@ -130,6 +130,7 @@ test("nostr login + remote signing + publish + watch history integration", async
const toolkit = {
finalizeEvent,
generateSecretKey,
getEventHash,
getPublicKey,
nip04,