mkvpeek 0.1.0

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/browser.d.ts +12 -0
  4. package/dist/browser.js +3 -0
  5. package/dist/ebml.d.ts +168 -0
  6. package/dist/ebml.js +224 -0
  7. package/dist/entry/browser.d.ts +44 -0
  8. package/dist/entry/browser.js +9 -0
  9. package/dist/entry/node.d.ts +37 -0
  10. package/dist/entry/node.js +38 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.js +3 -0
  13. package/dist/io/file.d.ts +2 -0
  14. package/dist/io/file.js +47 -0
  15. package/dist/io/lanes.d.ts +17 -0
  16. package/dist/io/lanes.js +124 -0
  17. package/dist/io/memory.d.ts +2 -0
  18. package/dist/io/memory.js +16 -0
  19. package/dist/io/range.d.ts +22 -0
  20. package/dist/io/range.js +190 -0
  21. package/dist/io/source.d.ts +102 -0
  22. package/dist/io/source.js +31 -0
  23. package/dist/io/url-node.d.ts +10 -0
  24. package/dist/io/url-node.js +99 -0
  25. package/dist/io/url.d.ts +21 -0
  26. package/dist/io/url.js +80 -0
  27. package/dist/io/windows.d.ts +37 -0
  28. package/dist/io/windows.js +105 -0
  29. package/dist/matroska/block.d.ts +47 -0
  30. package/dist/matroska/block.js +114 -0
  31. package/dist/matroska/chain.d.ts +55 -0
  32. package/dist/matroska/chain.js +47 -0
  33. package/dist/matroska/clusters.d.ts +4 -0
  34. package/dist/matroska/clusters.js +232 -0
  35. package/dist/matroska/cues.d.ts +23 -0
  36. package/dist/matroska/cues.js +93 -0
  37. package/dist/matroska/header.d.ts +52 -0
  38. package/dist/matroska/header.js +531 -0
  39. package/dist/peek/contract.d.ts +57 -0
  40. package/dist/peek/contract.js +24 -0
  41. package/dist/peek/engine.d.ts +19 -0
  42. package/dist/peek/engine.js +32 -0
  43. package/dist/peek/target.d.ts +11 -0
  44. package/dist/peek/target.js +56 -0
  45. package/dist/subtitle/assemble.d.ts +21 -0
  46. package/dist/subtitle/assemble.js +114 -0
  47. package/dist/subtitle/conclude.d.ts +20 -0
  48. package/dist/subtitle/conclude.js +57 -0
  49. package/dist/subtitle/indexed.d.ts +5 -0
  50. package/dist/subtitle/indexed.js +172 -0
  51. package/dist/subtitle/options.d.ts +76 -0
  52. package/dist/subtitle/options.js +1 -0
  53. package/dist/subtitle/peek.d.ts +5 -0
  54. package/dist/subtitle/peek.js +107 -0
  55. package/dist/subtitle/tuning.d.ts +4 -0
  56. package/dist/subtitle/tuning.js +76 -0
  57. package/dist/subtitle/walked.d.ts +5 -0
  58. package/dist/subtitle/walked.js +28 -0
  59. package/dist/tracks/core.d.ts +25 -0
  60. package/dist/tracks/core.js +24 -0
  61. package/dist/tracks/list.d.ts +8 -0
  62. package/dist/tracks/list.js +6 -0
  63. package/dist/tracks/subtitle.d.ts +45 -0
  64. package/dist/tracks/subtitle.js +38 -0
  65. package/dist/vocabulary.d.ts +88 -0
  66. package/dist/vocabulary.js +10 -0
  67. package/package.json +68 -0
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The Node.js entry point.
3
+ *
4
+ * It includes the whole browser surface.
5
+ */
6
+ export type { Attachment, ContainerInfo, ListedTrack, PeekCode, PeekOptions, PeekOutcome, RefusalCode, Source, SubtitleFinder, SubtitleOptions, SubtitlePreset, SubtitleProgress, SubtitleTrack, SubtitleTuning, SubtitleVia, Target, Track, TrackFlags, TrackTag, TrackType, Trip, UrlOptions, } from "./browser.js";
7
+ export { isRefusalCode, worthFallback } from "./browser.js";
8
+ export { peekSubtitles, peekTracks } from "./entry/node.js";
9
+ export { urlSource } from "./io/url-node.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { isRefusalCode, worthFallback } from "./browser.js";
2
+ export { peekSubtitles, peekTracks } from "./entry/node.js";
3
+ export { urlSource } from "./io/url-node.js";
@@ -0,0 +1,2 @@
1
+ import { type Source } from "./source.js";
2
+ export declare function fileSource(path: string): Promise<Source>;
@@ -0,0 +1,47 @@
1
+ import { open, statfs } from "node:fs/promises";
2
+ import { available, LARGE_READ_BYTES, lend } from "./source.js";
3
+ export async function fileSource(path) {
4
+ const handle = await open(path, "r");
5
+ const local = await isLocalDisk(path);
6
+ let cached = null;
7
+ const size = async () => {
8
+ cached ??= (await handle.stat()).size;
9
+ return cached;
10
+ };
11
+ const read = async (at, length, into) => {
12
+ const bounded = available(at, length, await size());
13
+ const unzeroed = bounded >= LARGE_READ_BYTES;
14
+ const lent = unzeroed ? lend(into, bounded) : null;
15
+ const buffer = lent ?? (unzeroed ? Buffer.allocUnsafeSlow(bounded) : new Uint8Array(bounded));
16
+ let filled = 0;
17
+ while (filled < bounded) {
18
+ const { bytesRead } = await handle.read(buffer, filled, bounded - filled, at + filled);
19
+ if (bytesRead === 0)
20
+ break;
21
+ filled += bytesRead;
22
+ }
23
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, filled);
24
+ };
25
+ const close = () => handle.close();
26
+ const trip = local ? "local" : "mount";
27
+ return { size, read, close, trip };
28
+ }
29
+ const NETWORK_MAGIC = new Set([
30
+ 0x6969,
31
+ 0xff534d42,
32
+ 0xfe534d42,
33
+ 0x517b,
34
+ 0x01021997,
35
+ 0x00c36400,
36
+ 0x7461636f,
37
+ 0x5346414f,
38
+ 0x6b414653,
39
+ ]);
40
+ async function isLocalDisk(path) {
41
+ try {
42
+ return !NETWORK_MAGIC.has((await statfs(path)).type);
43
+ }
44
+ catch {
45
+ return true;
46
+ }
47
+ }
@@ -0,0 +1,17 @@
1
+ type Gathered<T extends readonly (() => Promise<unknown>)[]> = {
2
+ [K in keyof T]: Awaited<ReturnType<T[K]>>;
3
+ };
4
+ /** A queue whose halt is shared. */
5
+ export declare class HaltableQueue<T> {
6
+ #private;
7
+ get halted(): boolean;
8
+ push(item: T): void;
9
+ close(): void;
10
+ halt(): void;
11
+ drain(concurrency: number, each: (item: T) => Promise<boolean>, signal?: AbortSignal): Promise<void>;
12
+ }
13
+ export declare function inParallel<T, R>(items: readonly T[], concurrency: number, each: (item: T, index: number) => Promise<R>, signal?: AbortSignal): Promise<R[]>;
14
+ /** A heterogeneous batch, each slot answering in its own type. */
15
+ export declare function gathered<T extends readonly (() => Promise<unknown>)[]>(tasks: T, concurrency: number, signal?: AbortSignal): Promise<Gathered<T>>;
16
+ export declare function joinOrAbort(lanes: ReadonlyArray<Promise<unknown>>, signal: AbortSignal | undefined): Promise<void>;
17
+ export {};
@@ -0,0 +1,124 @@
1
+ import { abortReason, onAbort, stopIfAborted } from "./source.js";
2
+ export class HaltableQueue {
3
+ #items = [];
4
+ #next = 0;
5
+ #closed = false;
6
+ #halted = false;
7
+ #parked = [];
8
+ get halted() {
9
+ return this.#halted;
10
+ }
11
+ push(item) {
12
+ this.#items.push(item);
13
+ this.#parked.shift()?.();
14
+ }
15
+ close() {
16
+ this.#closed = true;
17
+ this.#wake();
18
+ }
19
+ halt() {
20
+ this.#halted = true;
21
+ this.#closed = true;
22
+ this.#wake();
23
+ }
24
+ async drain(concurrency, each, signal) {
25
+ const take = () => this.#take();
26
+ const halt = () => this.halt();
27
+ await runWorkers(take, halt, concurrency, each, signal);
28
+ }
29
+ async #take() {
30
+ while (!this.#halted) {
31
+ if (this.#next < this.#items.length) {
32
+ const item = this.#items[this.#next];
33
+ this.#next += 1;
34
+ return item;
35
+ }
36
+ if (this.#closed)
37
+ return null;
38
+ await new Promise((resolve) => {
39
+ this.#parked.push(resolve);
40
+ });
41
+ }
42
+ return null;
43
+ }
44
+ #wake() {
45
+ for (const wake of this.#parked.splice(0))
46
+ wake();
47
+ }
48
+ }
49
+ export async function inParallel(items, concurrency, each, signal) {
50
+ const out = new Array(items.length);
51
+ let next = 0;
52
+ const take = () => {
53
+ const index = next;
54
+ next += 1;
55
+ return Promise.resolve(index >= items.length ? null : [items[index], index]);
56
+ };
57
+ const halt = () => { };
58
+ const store = async ([item, index]) => {
59
+ out[index] = await each(item, index);
60
+ return true;
61
+ };
62
+ await runWorkers(take, halt, Math.min(concurrency, items.length), store, signal);
63
+ return out;
64
+ }
65
+ export async function gathered(tasks, concurrency, signal) {
66
+ const ran = (task) => task();
67
+ const out = await inParallel(tasks, concurrency, ran, signal);
68
+ return out;
69
+ }
70
+ export async function joinOrAbort(lanes, signal) {
71
+ const join = Promise.allSettled(lanes);
72
+ let settled = join;
73
+ let unhook = () => { };
74
+ if (signal !== undefined) {
75
+ const abortPromise = new Promise((_, reject) => {
76
+ unhook = onAbort(signal, () => {
77
+ reject(abortReason(signal));
78
+ });
79
+ });
80
+ settled = Promise.race([join, abortPromise]);
81
+ }
82
+ try {
83
+ for (const one of await settled) {
84
+ if (one.status === "rejected")
85
+ throw one.reason;
86
+ }
87
+ }
88
+ finally {
89
+ unhook();
90
+ }
91
+ }
92
+ async function runWorkers(take, halt, concurrency, each, signal) {
93
+ let stopped = false;
94
+ const stop = () => {
95
+ stopped = true;
96
+ halt();
97
+ };
98
+ const unhook = onAbort(signal, stop);
99
+ const worker = async () => {
100
+ while (!stopped) {
101
+ const item = await take();
102
+ if (item === null)
103
+ return;
104
+ try {
105
+ stopIfAborted(signal);
106
+ if (!(await each(item))) {
107
+ stop();
108
+ return;
109
+ }
110
+ }
111
+ catch (error) {
112
+ stop();
113
+ throw error;
114
+ }
115
+ }
116
+ };
117
+ try {
118
+ const lanes = Array.from({ length: Math.max(1, concurrency) }, worker);
119
+ await joinOrAbort(lanes, signal);
120
+ }
121
+ finally {
122
+ unhook();
123
+ }
124
+ }
@@ -0,0 +1,2 @@
1
+ import { type Source } from "./source.js";
2
+ export declare function memorySource(bytes: ArrayBufferView | ArrayBuffer): Source;
@@ -0,0 +1,16 @@
1
+ import { available, lend } from "./source.js";
2
+ export function memorySource(bytes) {
3
+ const held = ArrayBuffer.isView(bytes)
4
+ ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
5
+ : new Uint8Array(bytes);
6
+ const read = async (at, length, into) => {
7
+ const bounded = available(at, length, held.byteLength);
8
+ const lent = lend(into, bounded);
9
+ if (lent !== null) {
10
+ lent.set(held.subarray(at, at + bounded));
11
+ return lent;
12
+ }
13
+ return new Uint8Array(held.subarray(at, at + bounded));
14
+ };
15
+ return { size: () => Promise.resolve(held.byteLength), read, trip: "local" };
16
+ }
@@ -0,0 +1,22 @@
1
+ import { type Source } from "./source.js";
2
+ export interface RangeReply {
3
+ status: number;
4
+ header(name: string): string | null;
5
+ body: AsyncIterable<Uint8Array>;
6
+ cancel(): Promise<void>;
7
+ }
8
+ export type RangeTransport = (from: number, to: number, signal: AbortSignal | undefined) => Promise<RangeReply>;
9
+ export type OwnedTransport = RangeTransport & {
10
+ dispose?: () => void;
11
+ };
12
+ export interface RangeOptions {
13
+ /**
14
+ * How long the source may deliver nothing before the read is refused.
15
+ *
16
+ * Unset, a socket that has gone quiet waits until {@linkcode signal} cuts it.
17
+ */
18
+ stallMs?: number | null;
19
+ signal?: AbortSignal | undefined;
20
+ }
21
+ export declare const bytesRange: (from: number, to: number) => string;
22
+ export declare function rangeSource(url: string, transport: OwnedTransport, { stallMs, signal }: RangeOptions): Promise<Source>;
@@ -0,0 +1,190 @@
1
+ import { abortReason, available, HEAD_BYTES, lend, onAbort, stopIfAborted, } from "./source.js";
2
+ export const bytesRange = (from, to) => `bytes=${String(from)}-${String(to)}`;
3
+ export async function rangeSource(url, transport, { stallMs, signal }) {
4
+ const stall = stallMs !== null && stallMs !== undefined && Number.isFinite(stallMs) && stallMs > 0
5
+ ? stallMs
6
+ : null;
7
+ const invariants = {
8
+ url,
9
+ transport,
10
+ stallMs: stall,
11
+ signal: signal ?? undefined,
12
+ };
13
+ const { dispose } = transport;
14
+ let headRead;
15
+ try {
16
+ stopIfAborted(signal);
17
+ headRead = await readHead(invariants);
18
+ }
19
+ catch (error) {
20
+ try {
21
+ dispose?.();
22
+ }
23
+ catch { }
24
+ throw error;
25
+ }
26
+ const { total, head } = headRead;
27
+ const size = () => Promise.resolve(total);
28
+ const read = async (at, length, into) => {
29
+ const bounded = available(at, length, total);
30
+ if (bounded === 0)
31
+ return new Uint8Array(0);
32
+ const borrowed = lend(into, bounded);
33
+ if (at + bounded <= head.length) {
34
+ if (borrowed === null)
35
+ return new Uint8Array(head.subarray(at, at + bounded));
36
+ borrowed.set(head.subarray(at, at + bounded));
37
+ return borrowed;
38
+ }
39
+ if (at < head.length) {
40
+ const fromHead = head.length - at;
41
+ const out = borrowed ?? new Uint8Array(bounded);
42
+ out.set(head.subarray(at, head.length));
43
+ const room = out.subarray(fromHead);
44
+ const tail = await readOverRanges(invariants, head.length, bounded - fromHead, room);
45
+ return out.subarray(0, fromHead + tail.length);
46
+ }
47
+ return readOverRanges(invariants, at, bounded, borrowed);
48
+ };
49
+ return {
50
+ size,
51
+ read,
52
+ ...(dispose === undefined ? {} : { close: () => Promise.resolve(dispose()) }),
53
+ trip: "remote",
54
+ };
55
+ }
56
+ const IDLE_CHUNK_LIMIT = 1000;
57
+ const MAX_RESPONSES_PER_READ = 64;
58
+ async function requestRange(invariants, dog, from, to) {
59
+ const reply = await invariants.transport(from, to, dog.signal);
60
+ const dropped = async (why) => {
61
+ await reply.cancel();
62
+ return new Error(why);
63
+ };
64
+ if (reply.status !== 206) {
65
+ throw await dropped(`${invariants.url} answered HTTP ${String(reply.status)} to a Range request; this reader needs a server that serves ranges`);
66
+ }
67
+ const encoding = reply.header("content-encoding");
68
+ const recoded = encoding !== null && encoding.trim().toLowerCase() !== "identity";
69
+ if (recoded) {
70
+ throw await dropped(`${invariants.url} answered a range encoded as ${encoding}, and this reader asked for identity`);
71
+ }
72
+ const contentRange = /^bytes\s+(\d+)-\d+\/(\d+)\s*$/.exec(reply.header("content-range") ?? "");
73
+ const total = Number(contentRange?.[2]);
74
+ if (contentRange === null || !Number.isSafeInteger(total)) {
75
+ throw await dropped(`${invariants.url} did not state a countable total length in Content-Range`);
76
+ }
77
+ const start = Number(contentRange[1]);
78
+ if (start !== from) {
79
+ throw await dropped(`${invariants.url} answered a range from ${String(start)} for one asked from ${String(from)}; this reader needs the bytes it asked for`);
80
+ }
81
+ const body = watched(invariants.url, reply.body, dog);
82
+ return { ...reply, total, body };
83
+ }
84
+ async function readHead(invariants) {
85
+ const dog = watchdog(invariants.url, invariants.stallMs, invariants.signal);
86
+ try {
87
+ const first = await requestRange(invariants, dog, 0, HEAD_BYTES - 1);
88
+ const into = new Uint8Array(available(0, HEAD_BYTES, first.total));
89
+ const landed = await readBodyInto(first, into, 0);
90
+ return { total: first.total, head: into.subarray(0, landed) };
91
+ }
92
+ finally {
93
+ dog.off();
94
+ }
95
+ }
96
+ async function readOverRanges(invariants, at, bounded, into = null) {
97
+ const out = into ?? new Uint8Array(bounded);
98
+ let written = 0;
99
+ let rounds = 0;
100
+ const dog = watchdog(invariants.url, invariants.stallMs, invariants.signal);
101
+ try {
102
+ while (written < bounded) {
103
+ rounds += 1;
104
+ if (rounds > MAX_RESPONSES_PER_READ) {
105
+ throw new Error(`${invariants.url} dribbled ${String(written)} bytes over ${String(MAX_RESPONSES_PER_READ)} responses for one read; this reader does not pay a request per byte`);
106
+ }
107
+ const from = at + written;
108
+ const response = await requestRange(invariants, dog, from, at + bounded - 1);
109
+ const landed = await readBodyInto(response, out, written);
110
+ if (landed === 0) {
111
+ throw new Error(`${invariants.url} answered a range from ${String(from)} with no bytes in it`);
112
+ }
113
+ written += landed;
114
+ }
115
+ return out.subarray(0, written);
116
+ }
117
+ finally {
118
+ dog.off();
119
+ }
120
+ }
121
+ async function readBodyInto(reply, into, at) {
122
+ let written = 0;
123
+ try {
124
+ for await (const value of reply.body) {
125
+ const room = into.length - at - written;
126
+ if (room <= 0)
127
+ break;
128
+ const kept = Math.min(value.length, room);
129
+ const taken = value.subarray(0, kept);
130
+ into.set(taken, at + written);
131
+ written += kept;
132
+ }
133
+ }
134
+ finally {
135
+ await reply.cancel().catch(() => { });
136
+ }
137
+ return written;
138
+ }
139
+ async function* watched(url, body, dog) {
140
+ let idle = 0;
141
+ let raisedHere = false;
142
+ try {
143
+ for await (const chunk of body) {
144
+ if (chunk.length > 0) {
145
+ idle = 0;
146
+ dog.alive();
147
+ yield chunk;
148
+ continue;
149
+ }
150
+ await new Promise((resolve) => setTimeout(resolve, 0));
151
+ idle += 1;
152
+ if (idle > IDLE_CHUNK_LIMIT) {
153
+ raisedHere = true;
154
+ throw new Error(`${url} sent ${String(idle)} empty chunks in a row and no bytes`);
155
+ }
156
+ const stop = abortReason(dog.signal);
157
+ if (stop !== null) {
158
+ raisedHere = true;
159
+ throw stop;
160
+ }
161
+ }
162
+ }
163
+ catch (error) {
164
+ if (raisedHere)
165
+ throw error;
166
+ throw (abortReason(dog.signal) ?? new Error(`${url} stopped sending mid-body (${String(error)})`));
167
+ }
168
+ }
169
+ function watchdog(url, stallMs, caller) {
170
+ if (stallMs === null)
171
+ return { signal: caller, alive: () => { }, off: () => { } };
172
+ const controller = new AbortController();
173
+ let timer;
174
+ const alive = () => {
175
+ clearTimeout(timer);
176
+ const stall = () => {
177
+ const silent = new Error(`${url} sent nothing for ${String(stallMs)}ms`);
178
+ controller.abort(silent);
179
+ };
180
+ timer = setTimeout(stall, stallMs);
181
+ };
182
+ alive();
183
+ const forward = () => controller.abort(caller?.reason);
184
+ const unhook = onAbort(caller, forward);
185
+ const off = () => {
186
+ clearTimeout(timer);
187
+ unhook();
188
+ };
189
+ return { signal: controller.signal, alive, off };
190
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Anything addressable by offset and length can be a source.
3
+ *
4
+ * - a file
5
+ * - an HTTP server that honours Range requests
6
+ * - an object store
7
+ * - a buffer already in memory
8
+ */
9
+ export interface Source {
10
+ size(): Promise<number>;
11
+ /**
12
+ * Returns as much as was asked for.
13
+ *
14
+ * A short answer anywhere but at the end of the file is read as the source having stopped.
15
+ *
16
+ * @param into A reuse hint.
17
+ * A cooperating source writes there and returns that subarray, and may ignore it;
18
+ * either way the returned view is the truth.
19
+ * Touching it again after resolve breaks the contract,
20
+ * and a late write changes the bytes of whoever holds the borrowed window.
21
+ */
22
+ read(at: number, length: number, into?: Uint8Array): Promise<Uint8Array>;
23
+ close?(): Promise<void>;
24
+ trip?: Trip;
25
+ }
26
+ /** What kind of round trip a read to this source is. */
27
+ export type Trip = "local" | "mount" | "remote";
28
+ /** A source whose size has been asked. */
29
+ export interface SizedSource {
30
+ source: Source;
31
+ fileSize: number;
32
+ }
33
+ export interface Range {
34
+ at: number;
35
+ length: number;
36
+ }
37
+ /** The index path's knobs, the shape of a planned read of scattered ranges. */
38
+ export interface IndexFetch {
39
+ /**
40
+ * Two ranges closer than this are fetched as one, gap included.
41
+ *
42
+ * Raising it trades bytes for round trips; at 0 only ranges that already touch are merged.
43
+ */
44
+ gapBytes: number;
45
+ /**
46
+ * How far past its stated size to look at each point the plan names.
47
+ *
48
+ * Raising it makes more points finish in one read at the cost of bytes,
49
+ * and a window this wide sits on the heap per point.
50
+ */
51
+ cueAheadBytes: number;
52
+ /**
53
+ * Concurrent range requests.
54
+ *
55
+ * WARN: on a network path this is the number of open connections,
56
+ * so **mind the target server's socket limit.**
57
+ */
58
+ concurrency: number;
59
+ }
60
+ /** The walk path's knobs, the shape of a scan that starts at the front. */
61
+ export interface WalkFetch {
62
+ /**
63
+ * How far past a block the walk reads ahead when it reads that block.
64
+ *
65
+ * Raising it trades bytes for round trips; at 0 it reads only what was asked for.
66
+ */
67
+ blockAheadBytes: number;
68
+ /**
69
+ * Walk lanes.
70
+ *
71
+ * On a network path it means the same as {@linkcode IndexFetch.concurrency}.
72
+ */
73
+ concurrency: number;
74
+ }
75
+ export type ReportBytes = (done: number, total: number) => void;
76
+ export interface Watch {
77
+ onProgress: ReportBytes;
78
+ signal: AbortSignal | undefined;
79
+ }
80
+ /**
81
+ * How much the opening read fetches.
82
+ *
83
+ * A number that usually finishes a typical header in one round trip.
84
+ */
85
+ export declare const HEAD_BYTES: number;
86
+ /**
87
+ * From this size a file Source stops zero-filling, and the pool makes no slot or loan below it.
88
+ *
89
+ * On a large read the zero-fill costs as much as the read.
90
+ */
91
+ export declare const LARGE_READ_BYTES: number;
92
+ export declare const MAX_SPAN_BYTES: number;
93
+ /**
94
+ * Lends the front of the hint when it can hold the clamped answer.
95
+ *
96
+ * Otherwise null, and the caller allocates.
97
+ */
98
+ export declare const lend: (into: Uint8Array | undefined, bounded: number) => Uint8Array | null;
99
+ export declare function abortReason(signal: AbortSignal | undefined): Error | null;
100
+ export declare function stopIfAborted(signal: AbortSignal | undefined): void;
101
+ export declare function available(at: number, length: number, size: number): number;
102
+ export declare function onAbort(signal: AbortSignal | undefined, run: () => void): () => void;
@@ -0,0 +1,31 @@
1
+ export const HEAD_BYTES = 128 * 1024;
2
+ export const LARGE_READ_BYTES = 16 * 1024;
3
+ export const MAX_SPAN_BYTES = 4 * 1024 ** 2;
4
+ export const lend = (into, bounded) => into !== undefined && into.length >= bounded ? into.subarray(0, bounded) : null;
5
+ export function abortReason(signal) {
6
+ if (signal?.aborted !== true)
7
+ return null;
8
+ return signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
9
+ }
10
+ export function stopIfAborted(signal) {
11
+ const stopped = abortReason(signal);
12
+ if (stopped !== null)
13
+ throw stopped;
14
+ }
15
+ export function available(at, length, size) {
16
+ const addressable = Number.isInteger(at) && at >= 0 && Number.isInteger(length) && length >= 0;
17
+ if (!addressable) {
18
+ throw new RangeError(`a read asked for ${String(length)} bytes at ${String(at)}`);
19
+ }
20
+ return Math.max(0, Math.min(length, size - at));
21
+ }
22
+ export function onAbort(signal, run) {
23
+ if (signal === undefined)
24
+ return () => { };
25
+ if (signal.aborted) {
26
+ run();
27
+ return () => { };
28
+ }
29
+ signal.addEventListener("abort", run, { once: true });
30
+ return () => signal.removeEventListener("abort", run);
31
+ }
@@ -0,0 +1,10 @@
1
+ /** URL policy over a node:http transport. */
2
+ import type { Source } from "./source.js";
3
+ import { type UrlOptions } from "./url.js";
4
+ /**
5
+ * An HTTP source over `node:http`, or over `fetch` when one is passed.
6
+ *
7
+ * A server answering a Range request with anything but 206 is not read;
8
+ * the read throws, which the envelope reports as `source-failed`.
9
+ */
10
+ export declare function urlSource(url: string, options?: UrlOptions): Promise<Source>;
@@ -0,0 +1,99 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import { bytesRange, rangeSource, } from "./range.js";
4
+ import { abortReason, onAbort } from "./source.js";
5
+ import { httpTarget, TRANSPORT_OWNED_HEADERS, urlSource as viaFetch, whyInadmissible, withoutHeaders, } from "./url.js";
6
+ export async function urlSource(url, options = {}) {
7
+ if (options.fetch !== undefined)
8
+ return viaFetch(url, options);
9
+ const transport = nodeTransport(url, options);
10
+ return rangeSource(url, transport, options);
11
+ }
12
+ const MAX_SOCKETS = 64;
13
+ const MAX_REDIRECTS = 20;
14
+ const CROSS_ORIGIN_STRIPPED = new Set(["authorization", "cookie", "proxy-authorization"]);
15
+ const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
16
+ function nodeTransport(url, options) {
17
+ const startUrl = httpTarget(url);
18
+ const agent = {
19
+ "http:": new http.Agent({ keepAlive: true, maxSockets: MAX_SOCKETS }),
20
+ "https:": new https.Agent({ keepAlive: true, maxSockets: MAX_SOCKETS }),
21
+ };
22
+ const headers = withoutHeaders(options.headers ?? {}, TRANSPORT_OWNED_HEADERS);
23
+ const sendRange = (parsed, carrying, from, to, signal) => new Promise((resolve, reject) => {
24
+ const secure = parsed.protocol === "https:";
25
+ const lib = secure ? https : http;
26
+ const host = parsed.hostname;
27
+ const requestOptions = {
28
+ hostname: host.startsWith("[") ? host.slice(1, -1) : host,
29
+ port: parsed.port,
30
+ path: parsed.pathname + parsed.search,
31
+ agent: agent[secure ? "https:" : "http:"],
32
+ headers: {
33
+ ...carrying,
34
+ "accept-encoding": "identity",
35
+ range: bytesRange(from, to),
36
+ },
37
+ };
38
+ const settle = (response) => {
39
+ resolve(replyOver(response));
40
+ };
41
+ const request = lib.request(requestOptions, settle);
42
+ const stop = () => {
43
+ request.destroy(abortReason(signal) ?? undefined);
44
+ };
45
+ const unhook = onAbort(signal, stop);
46
+ request.on("close", unhook);
47
+ request.on("error", (error) => {
48
+ unhook();
49
+ reject(error);
50
+ });
51
+ request.end();
52
+ });
53
+ const transport = async (from, to, signal) => {
54
+ let current = startUrl;
55
+ let carrying = headers;
56
+ for (let left = MAX_REDIRECTS;; left -= 1) {
57
+ const reply = await sendRange(current, carrying, from, to, signal);
58
+ const location = reply.header("location");
59
+ if (!REDIRECT_STATUS.has(reply.status) || location === null)
60
+ return reply;
61
+ await reply.cancel();
62
+ if (left === 0) {
63
+ throw new Error(`${url} redirected more than ${String(MAX_REDIRECTS)} times`);
64
+ }
65
+ const next = URL.parse(location, current.href);
66
+ if (next === null) {
67
+ throw new Error(`${url} redirected to ${location}, which is not a URL`);
68
+ }
69
+ const why = whyInadmissible(next);
70
+ if (why !== null)
71
+ throw new Error(`${url} redirected to one that ${why}`);
72
+ if (next.origin !== current.origin) {
73
+ carrying = withoutHeaders(carrying, CROSS_ORIGIN_STRIPPED);
74
+ }
75
+ current = next;
76
+ }
77
+ };
78
+ const dispose = () => {
79
+ for (const key of ["http:", "https:"]) {
80
+ try {
81
+ agent[key].destroy();
82
+ }
83
+ catch { }
84
+ }
85
+ };
86
+ const owned = { dispose };
87
+ return Object.assign(transport, owned);
88
+ }
89
+ function replyOver(response) {
90
+ const header = (name) => {
91
+ const value = response.headers[name.toLowerCase()];
92
+ return typeof value === "string" ? value : null;
93
+ };
94
+ const cancel = () => {
95
+ response.destroy();
96
+ return Promise.resolve();
97
+ };
98
+ return { status: response.statusCode ?? 0, header, body: response, cancel };
99
+ }