npaw-plugin-nwf
Version:
NPAW's Plugin
200 lines (199 loc) • 9.03 kB
TypeScript
import { SegmentState, SegmentLeaderRole } from '../Utils/Constants';
import { TrackTypeValue } from '../Loaders/P2PSegmentIdResolver';
export type SegmentStateValue = (typeof SegmentState)[keyof typeof SegmentState];
export type SegmentLeaderRoleValue = (typeof SegmentLeaderRole)[keyof typeof SegmentLeaderRole];
/**
* Persisted metadata for a stored segment. Kept separate from the bytes so it
* can be loaded cheaply at startup to rebuild the in-memory index without
* touching the (potentially large) payload blobs.
*/
export type StoredSegmentMetadata = {
id: string;
url: string;
trackType: TrackTypeValue;
rendition?: string;
expectedSize: number;
state: SegmentStateValue;
leaderRole: SegmentLeaderRoleValue;
createdAt: number;
lastUsedAt: number;
writtenBytes: number;
failureReason?: string;
};
/**
* Persistence backend interface. The MVP ships with `IndexedDBBackend` for
* browsers (parity with iOS `FileManager` + Android filesystem) and
* `InMemoryBackend` as an automatic fallback for Node / jsdom / SSR.
* All methods are async: callers must await them.
*/
export interface DiskSegmentStoreBackend {
/** Load the index of all persisted segment metadata. */
loadAllMetadata(): Promise<StoredSegmentMetadata[]>;
/** Persist the metadata for a segment (upsert). */
putMetadata(meta: StoredSegmentMetadata): Promise<void>;
/** Persist the final byte payload for a segment (once READY). */
putBytes(id: string, bytes: Uint8Array): Promise<void>;
/** Fetch the persisted byte payload; returns undefined if not stored. */
getBytes(id: string): Promise<Uint8Array | undefined>;
/** Delete both metadata and payload for a segment. */
delete(id: string): Promise<void>;
/** Drop the entire store (metadata + payloads). */
clearAll(): Promise<void>;
}
/**
* Persistence backed by the browser's IndexedDB. Origin-scoped, quota-bounded,
* survives reload. Mirrors the persistence semantics of iOS `FileManager` and
* Android filesystem used by the native `DiskSegmentStore`.
*/
export declare class IndexedDBBackend implements DiskSegmentStoreBackend {
private dbPromise?;
static isSupported(): boolean;
private openDb;
loadAllMetadata(): Promise<StoredSegmentMetadata[]>;
putMetadata(meta: StoredSegmentMetadata): Promise<void>;
putBytes(id: string, bytes: Uint8Array): Promise<void>;
getBytes(id: string): Promise<Uint8Array | undefined>;
delete(id: string): Promise<void>;
clearAll(): Promise<void>;
}
/** Fallback used when IndexedDB is unavailable (Node, jsdom, SSR). */
export declare class InMemoryBackend implements DiskSegmentStoreBackend {
private meta;
private bytes;
loadAllMetadata(): Promise<StoredSegmentMetadata[]>;
putMetadata(meta: StoredSegmentMetadata): Promise<void>;
putBytes(id: string, data: Uint8Array): Promise<void>;
getBytes(id: string): Promise<Uint8Array | undefined>;
delete(id: string): Promise<void>;
clearAll(): Promise<void>;
}
/** Picks the best available backend. */
export declare function createDefaultBackend(): DiskSegmentStoreBackend;
/**
* A segment in the local cache. Mirrors iOS `StoredSegment` and Android
* `DiskSegmentStore.Entry`. Metadata is always authoritative in memory for the
* state machine (ACQUIRING / READY / FAILED / EVICTED); bytes live in memory
* during acquisition (for streaming reads) and are flushed through to
* IndexedDB on `markReady` so they survive across sessions.
*/
export declare class StoredSegment {
readonly id: string;
readonly url: string;
readonly trackType: TrackTypeValue;
readonly rendition?: string;
expectedSize: number;
state: SegmentStateValue;
leaderRole: SegmentLeaderRoleValue;
readonly createdAt: number;
lastUsedAt: number;
writtenBytes: number;
failureReason?: string;
/** Per-chunk buffer used while ACQUIRING, discarded once flattened. */
private _chunks;
/** Flat payload held in RAM during the session after `markReady`. */
private _finalData?;
/** True if the bytes live only in the backend (rebuilt from hydrate()). */
private _bytesHydratedFromBackend;
private _waiters;
constructor(meta: StoredSegmentMetadata);
toMetadata(): StoredSegmentMetadata;
touch(): void;
isVisibleForRendition(activeVideoRendition: string | undefined): boolean;
/** Fast sync read from RAM (returns undefined if bytes live only in backend). */
readDataSync(): Uint8Array | undefined;
/** Internal helpers used by the store. */
appendBytes(bytes: Uint8Array): void;
finalizeReady(): Uint8Array;
adoptFinalData(bytes: Uint8Array): void;
needsBytesFromBackend(): boolean;
markHydratedWithoutBytes(): void;
discard(): void;
/** Read in-memory bytes at `offset` (used by GrowingSegmentReader). */
readBytesAt(offset: number, maxBytes: number): Uint8Array | undefined;
/** Register a one-shot waiter notified on next state change or append. */
subscribeWaiter(waiter: (state: SegmentStateValue) => void): () => void;
private _notifyWaiters;
}
/**
* Segment store with the iOS/Android public API, backed by IndexedDB by
* default. The in-memory mirror keeps the state-machine synchronous (so
* P2PLoader/CDNLoader flow works like on native) while byte I/O is async
* against the backend.
*
* Typical lifecycle of a captured segment:
* 1. `createOrGetAcquiring` → an in-memory `StoredSegment` with state=ACQUIRING.
* Metadata is written-through to the backend immediately (for crash recovery).
* 2. `append(id, bytes)` while the CDN stream flows. Bytes stay in RAM; any
* open `GrowingSegmentReader` is notified so peers can stream the in-flight
* segment (early announcement / ACQUIRING serve).
* 3. `markReady(id)` flattens the chunks, writes them to the backend (so they
* survive a reload), keeps a copy in RAM for the current session, and
* returns the finalized segment.
* 4. Eviction via `evictExpiredAndOverflow` or explicit `evict(id)` removes
* both RAM and backend copies.
*/
export default class DiskSegmentStore {
private segments;
private backend;
private hydrated;
/**
* Serialized write chain for backend mutations. Metadata and byte writes
* submitted via `queueBackendWrite` run in submission order. Prevents
* races where a later-submitted READY write lands on disk before the
* earlier ACQUIRING write, leaving a stale state in IndexedDB.
*/
private writeChain;
constructor(backend?: DiskSegmentStoreBackend);
/**
* Allows tests to await all pending write-through flushes. Returns a
* promise that resolves once the internal write chain is quiescent.
*/
flushPendingWrites(): Promise<void>;
private queueBackendWrite;
/**
* Loads the persisted index from the backend. Does NOT read the byte
* payloads — those are fetched lazily on `readDataAsync` / `openReader`
* first access. Call once at startup.
*/
hydrate(): Promise<void>;
createOrGetAcquiring(id: string, url: string, trackType: TrackTypeValue, rendition: string | undefined, expectedSize: number, leaderRole: SegmentLeaderRoleValue): StoredSegment;
append(id: string, bytes: Uint8Array): void;
markReady(id: string): StoredSegment | undefined;
markFailed(id: string, reason?: string): StoredSegment | undefined;
evict(id: string): StoredSegment | undefined;
getReady(id: string): StoredSegment | undefined;
get(id: string): StoredSegment | undefined;
readyIds(activeVideoRendition?: string): Set<string>;
totalReadyBytes(): number;
readyCount(): number;
evictExpiredAndOverflow(retentionMs: number, maxBytes: number): StoredSegment[];
openReader(id: string): GrowingSegmentReader | undefined;
clear(): Promise<void>;
/**
* Async payload read. Prefers RAM (`readDataSync`); if the bytes were
* persisted in a previous session, fetches them from the backend on demand
* and caches the result for subsequent reads.
*/
readDataAsync(id: string): Promise<Uint8Array | undefined>;
/** Internal: used by GrowingSegmentReader to pull bytes for READY, ejected-from-RAM entries. */
_backfillBytes(seg: StoredSegment): Promise<void>;
private persistMetadata;
}
/**
* Streaming reader over a `StoredSegment`. Returns chunks as they are
* appended; awaits up to `waitTimeoutMs` on each call when the segment is
* still ACQUIRING. Parity with iOS `GrowingSegmentReader` (sync-in-Swift,
* Promise-based in JS).
*/
export declare class GrowingSegmentReader {
private segment;
private store?;
private offset;
private closed;
constructor(segment: StoredSegment, store?: DiskSegmentStore);
expectedSize(): number;
currentState(): SegmentStateValue;
hasPendingBytes(): boolean;
readNextChunk(maxBytes: number, waitTimeoutMs: number): Promise<Uint8Array | undefined>;
close(): void;
}