@siafoundation/sia-storage 0.1.2 → 0.1.4
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/README.md +41 -3
- package/dist/{chunk-QYQQIWFH.js → chunk-YOMDUMYW.js} +7 -3
- package/dist/chunk-YOMDUMYW.js.map +1 -0
- package/dist/index.d.ts +59 -6
- package/dist/index.js +120 -29
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +40 -15
- package/dist/next.js +1 -1
- package/dist/sia-storage-sw.js +2 -2
- package/dist/stream-worker.js +111 -14
- package/dist/stream-worker.js.map +1 -1
- package/package.json +8 -7
- package/wasm/sia_storage_wasm.d.ts +61 -38
- package/wasm/sia_storage_wasm.js +141 -40
- package/wasm/sia_storage_wasm_bg.wasm +0 -0
- package/wasm/sia_storage_wasm_bg.wasm.d.ts +10 -4
- package/dist/chunk-QYQQIWFH.js.map +0 -1
package/dist/stream-worker.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
parseStreamPath,
|
|
4
4
|
requestReply,
|
|
5
5
|
streamErrorMessage
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-YOMDUMYW.js";
|
|
7
7
|
|
|
8
8
|
// src/stream-worker.ts
|
|
9
9
|
import wasmInit, {
|
|
@@ -88,6 +88,7 @@ function errorResponse(status) {
|
|
|
88
88
|
// src/stream/worker.ts
|
|
89
89
|
var SOURCE_TIMEOUT = 5e3;
|
|
90
90
|
var IDLE_TIMEOUT = 5 * 6e4;
|
|
91
|
+
var REPORT_INTERVAL = 100;
|
|
91
92
|
function serveStreams(scope, connect, supportsDownloads) {
|
|
92
93
|
const sources = /* @__PURE__ */ new Map();
|
|
93
94
|
const sessions = /* @__PURE__ */ new Map();
|
|
@@ -178,6 +179,9 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
178
179
|
const session = sessionFor(client.id, reply);
|
|
179
180
|
source.session = session;
|
|
180
181
|
session.sources.add(source);
|
|
182
|
+
if (reply.events?.length) {
|
|
183
|
+
source.reporter = createReporter(client, token, reply.events);
|
|
184
|
+
}
|
|
181
185
|
idle(session);
|
|
182
186
|
const { objectId, sealed, name, mime, size } = reply;
|
|
183
187
|
return { objectId, sealed, name, mime, size };
|
|
@@ -188,7 +192,8 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
188
192
|
closed: false,
|
|
189
193
|
requests: /* @__PURE__ */ new Set(),
|
|
190
194
|
ready,
|
|
191
|
-
setup
|
|
195
|
+
setup,
|
|
196
|
+
reading: 0
|
|
192
197
|
};
|
|
193
198
|
sources.set(sourceKey, source);
|
|
194
199
|
return source;
|
|
@@ -197,6 +202,7 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
197
202
|
if (source.closed) return;
|
|
198
203
|
source.closed = true;
|
|
199
204
|
sources.delete(source.key);
|
|
205
|
+
source.reporter?.stop();
|
|
200
206
|
source.setup.abort(new DOMException("Released", "AbortError"));
|
|
201
207
|
for (const cancel of source.requests) cancel();
|
|
202
208
|
source.session?.sources.delete(source);
|
|
@@ -219,7 +225,11 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
219
225
|
}) : void 0;
|
|
220
226
|
void Promise.resolve(cancellation).finally(() => {
|
|
221
227
|
reader?.releaseLock();
|
|
222
|
-
|
|
228
|
+
const { source } = job;
|
|
229
|
+
source?.requests.delete(job.abort);
|
|
230
|
+
if (source && job.reading && --source.reading === 0) {
|
|
231
|
+
source.reporter?.status("idle");
|
|
232
|
+
}
|
|
223
233
|
job.session?.jobs.delete(lifetime);
|
|
224
234
|
if (job.session) idle(job.session);
|
|
225
235
|
complete();
|
|
@@ -235,7 +245,7 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
235
245
|
const message = streamErrorMessage(error);
|
|
236
246
|
const aborted = error instanceof Error && error.name === "AbortError";
|
|
237
247
|
const { source, range } = job;
|
|
238
|
-
if (!aborted && source && !source.closed) {
|
|
248
|
+
if (!aborted && !job.quiet && source && !source.closed) {
|
|
239
249
|
try {
|
|
240
250
|
source.client.postMessage({
|
|
241
251
|
type: "sia-stream-error",
|
|
@@ -252,12 +262,13 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
252
262
|
};
|
|
253
263
|
return job;
|
|
254
264
|
}
|
|
255
|
-
async function serve(request, clientId, token, download) {
|
|
265
|
+
async function serve(request, clientId, token, { download, warm }) {
|
|
256
266
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
257
267
|
return errorResponse(405);
|
|
258
268
|
}
|
|
259
269
|
if (!supportsDownloads) return errorResponse(503);
|
|
260
270
|
const job = startJob(request, token);
|
|
271
|
+
job.quiet = warm;
|
|
261
272
|
try {
|
|
262
273
|
const client = await scope.clients.get(clientId);
|
|
263
274
|
if (!client || request.signal.aborted) return errorResponse(404);
|
|
@@ -277,7 +288,14 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
277
288
|
);
|
|
278
289
|
job.range = range;
|
|
279
290
|
const headers = responseHeaders(metadata, range, download);
|
|
280
|
-
if (request.method === "HEAD" || range.status === 416
|
|
291
|
+
if (request.method === "HEAD" || range.status === 416) {
|
|
292
|
+
job.finish();
|
|
293
|
+
return new Response(null, { status: range.status, headers });
|
|
294
|
+
}
|
|
295
|
+
const reporter = warm ? void 0 : source.reporter;
|
|
296
|
+
job.reading = !warm;
|
|
297
|
+
if (!warm && ++source.reading === 1) reporter?.status("connecting");
|
|
298
|
+
if (range.length === 0) {
|
|
281
299
|
job.finish();
|
|
282
300
|
return new Response(null, { status: range.status, headers });
|
|
283
301
|
}
|
|
@@ -285,7 +303,13 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
285
303
|
if (job.closed) return errorResponse(502);
|
|
286
304
|
const object = await objectFor(session, sdk, metadata);
|
|
287
305
|
if (job.closed) return errorResponse(502);
|
|
288
|
-
const chunks = sdk.download(object, {
|
|
306
|
+
const chunks = sdk.download(object, {
|
|
307
|
+
offset: range.offset,
|
|
308
|
+
length: range.length,
|
|
309
|
+
...reporter?.wants.shards && {
|
|
310
|
+
onShardDownloaded: (progress) => reporter.shard(progress)
|
|
311
|
+
}
|
|
312
|
+
}).getReader();
|
|
289
313
|
job.reader = chunks;
|
|
290
314
|
return new Response(rangeBody(job, chunks, range.length), {
|
|
291
315
|
status: range.status,
|
|
@@ -330,8 +354,9 @@ function serveStreams(scope, connect, supportsDownloads) {
|
|
|
330
354
|
const stream = parseStreamPath(url.pathname);
|
|
331
355
|
if (!stream) return;
|
|
332
356
|
const download = url.searchParams.get("download") === "1";
|
|
357
|
+
const warm = url.searchParams.get("warm") === "1";
|
|
333
358
|
event.respondWith(
|
|
334
|
-
serve(event.request, stream.clientId, stream.token, download)
|
|
359
|
+
serve(event.request, stream.clientId, stream.token, { download, warm })
|
|
335
360
|
);
|
|
336
361
|
});
|
|
337
362
|
}
|
|
@@ -361,6 +386,9 @@ function rangeBody(job, chunks, length) {
|
|
|
361
386
|
throw new Error("Shared file stream exceeded the requested length.");
|
|
362
387
|
}
|
|
363
388
|
controller.enqueue(value);
|
|
389
|
+
const reporter = job.quiet ? void 0 : job.source?.reporter;
|
|
390
|
+
reporter?.status("downloading");
|
|
391
|
+
reporter?.bytes(value.byteLength);
|
|
364
392
|
if (remaining === 0) {
|
|
365
393
|
controller.close();
|
|
366
394
|
job.finish(true);
|
|
@@ -377,6 +405,61 @@ function rangeBody(job, chunks, length) {
|
|
|
377
405
|
{ highWaterMark: 0 }
|
|
378
406
|
);
|
|
379
407
|
}
|
|
408
|
+
function createReporter(client, token, events) {
|
|
409
|
+
const wants = {
|
|
410
|
+
status: events.includes("status"),
|
|
411
|
+
progress: events.includes("progress"),
|
|
412
|
+
shards: events.includes("shards")
|
|
413
|
+
};
|
|
414
|
+
let stopped = false;
|
|
415
|
+
let current;
|
|
416
|
+
let bytes = 0;
|
|
417
|
+
let shards = [];
|
|
418
|
+
let timer;
|
|
419
|
+
function post(message) {
|
|
420
|
+
try {
|
|
421
|
+
client.postMessage(message);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function flush() {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
timer = void 0;
|
|
428
|
+
if (stopped || bytes === 0 && shards.length === 0) return;
|
|
429
|
+
post({ type: "sia-stream-progress", token, bytes, shards });
|
|
430
|
+
bytes = 0;
|
|
431
|
+
shards = [];
|
|
432
|
+
}
|
|
433
|
+
function schedule() {
|
|
434
|
+
timer ??= setTimeout(flush, REPORT_INTERVAL);
|
|
435
|
+
}
|
|
436
|
+
function status(next) {
|
|
437
|
+
if (stopped || next === current) return;
|
|
438
|
+
flush();
|
|
439
|
+
current = next;
|
|
440
|
+
if (wants.status) post({ type: "sia-stream-status", token, status: next });
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
wants,
|
|
444
|
+
status,
|
|
445
|
+
bytes(count) {
|
|
446
|
+
if (stopped || !wants.progress) return;
|
|
447
|
+
bytes += count;
|
|
448
|
+
schedule();
|
|
449
|
+
},
|
|
450
|
+
shard({ hostKey, shardSize, shardIndex, slabIndex, elapsedMs }) {
|
|
451
|
+
if (stopped) return;
|
|
452
|
+
shards.push({ hostKey, shardSize, shardIndex, slabIndex, elapsedMs });
|
|
453
|
+
schedule();
|
|
454
|
+
},
|
|
455
|
+
stop() {
|
|
456
|
+
stopped = true;
|
|
457
|
+
clearTimeout(timer);
|
|
458
|
+
timer = void 0;
|
|
459
|
+
shards = [];
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
380
463
|
|
|
381
464
|
// src/stream-worker.ts
|
|
382
465
|
function installStreams(scope, options = {}) {
|
|
@@ -397,13 +480,27 @@ function installStreams(scope, options = {}) {
|
|
|
397
480
|
}
|
|
398
481
|
async function connectShared({
|
|
399
482
|
indexerUrl,
|
|
400
|
-
seed
|
|
483
|
+
seed,
|
|
484
|
+
sealKey
|
|
401
485
|
}) {
|
|
402
|
-
const
|
|
486
|
+
const key = sealKey === void 0 ? void 0 : new AppKey(fromHex(sealKey));
|
|
487
|
+
let sdk;
|
|
488
|
+
try {
|
|
489
|
+
sdk = await SharedSdk.connect(indexerUrl, seed);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
key?.free();
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
403
494
|
return {
|
|
404
|
-
object
|
|
405
|
-
|
|
406
|
-
|
|
495
|
+
// The page sealed the object it already had with the share's random key.
|
|
496
|
+
// Opening that saves a round trip to the indexer before every stream's
|
|
497
|
+
// first byte.
|
|
498
|
+
object: async ({ objectId, sealed }) => key && sealed ? PinnedObject.open(key, sealed) : sdk.object(objectId),
|
|
499
|
+
download: (object, options) => sdk.download(object, options),
|
|
500
|
+
free: () => {
|
|
501
|
+
sdk.free();
|
|
502
|
+
key?.free();
|
|
503
|
+
}
|
|
407
504
|
};
|
|
408
505
|
}
|
|
409
506
|
async function connectApp({
|
|
@@ -419,7 +516,7 @@ async function connectApp({
|
|
|
419
516
|
return {
|
|
420
517
|
// The page sent the object sealed with this key, so no lookup is needed.
|
|
421
518
|
object: async ({ sealed }) => PinnedObject.open(key, sealed),
|
|
422
|
-
download: (object,
|
|
519
|
+
download: (object, options) => sdk.download(object, options),
|
|
423
520
|
free: () => {
|
|
424
521
|
sdk.free();
|
|
425
522
|
key.free();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stream-worker.ts","../src/stream/response.ts","../src/stream/worker.ts"],"sourcesContent":["/**\n * The worker side of streaming, for sites that already run a service worker.\n *\n * A page has only one service worker, so a site with its own calls\n * `installStreams(self)` in it and passes that worker's URL to\n * `enableStreaming`. Every other site serves the prebuilt `sia-storage-sw.js`,\n * which is this function with the WebAssembly built in.\n */\nimport wasmInit, {\n AppKey,\n Builder,\n type InitInput,\n PinnedObject,\n SharedSdk,\n} from '../wasm/sia_storage_wasm.js'\nimport type { Connection } from './stream/protocol'\nimport { serveStreams, type StreamScope, type StreamSdk } from './stream/worker'\n\nexport type { StreamScope }\n\nexport type InstallStreamsOptions = {\n /**\n * The SDK's WebAssembly, as a URL, bytes or compiled module. Only needed\n * when the worker's bundler does not emit `sia_storage_wasm_bg.wasm`.\n */\n wasm?: InitInput\n}\n\nexport function installStreams(\n scope: StreamScope,\n options: InstallStreamsOptions = {},\n) {\n let ready: Promise<unknown> | undefined\n serveStreams(\n scope,\n async (connection) => {\n ready ??= wasmInit(\n options.wasm === undefined ? undefined : { module_or_path: options.wasm },\n )\n await ready\n return connection.kind === 'shared'\n ? connectShared(connection)\n : connectApp(connection)\n },\n // Downloads from hosts need WebTransport, which some browsers leave out\n // of service workers. The page then reads objects itself.\n typeof WebTransport === 'function',\n )\n}\n\nasync function connectShared({\n indexerUrl,\n seed,\n}: Extract<Connection, { kind: 'shared' }>): Promise<StreamSdk> {\n const sdk = await SharedSdk.connect(indexerUrl, seed)\n return {\n object: ({ objectId }) => sdk.object(objectId),\n download: (object, range) => sdk.download(object, range),\n free: () => sdk.free(),\n }\n}\n\nasync function connectApp({\n indexerUrl,\n appKey,\n appMeta,\n}: Extract<Connection, { kind: 'app' }>): Promise<StreamSdk> {\n const key = new AppKey(fromHex(appKey))\n const builder = new Builder(indexerUrl, appMeta)\n try {\n const sdk = await builder.connected(key)\n if (!sdk) throw new Error('The app key is not registered with the indexer.')\n return {\n // The page sent the object sealed with this key, so no lookup is needed.\n object: async ({ sealed }) => PinnedObject.open(key, sealed!),\n download: (object, range) => sdk.download(object, range),\n free: () => {\n sdk.free()\n key.free()\n },\n }\n } catch (error) {\n key.free()\n throw error\n } finally {\n builder.free()\n }\n}\n\n// Uint8Array.fromHex is too new for some browsers that run service workers.\nfunction fromHex(hex: string) {\n return Uint8Array.from(hex.match(/../g) ?? [], (byte) => parseInt(byte, 16))\n}\n","// File bytes come from whoever owns the share, so the browser must never cache\n// them or run them as this origin's content.\nconst PROTECTIVE_HEADERS = {\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n 'Content-Security-Policy': \"sandbox; default-src 'none'\",\n 'Referrer-Policy': 'no-referrer',\n}\n\nexport type ByteRange = {\n status: 200 | 206 | 416\n offset: number\n length: number\n}\n\nexport function parseRange(range: string | null, size: number): ByteRange {\n if (!Number.isSafeInteger(size) || size < 0) {\n throw new RangeError('Invalid file size')\n }\n if (range === null) return { status: 200, offset: 0, length: size }\n\n const invalid: ByteRange = { status: 416, offset: 0, length: 0 }\n const match = /^bytes=(\\d*)-(\\d*)$/i.exec(range.trim())\n if (!match || (!match[1] && !match[2]) || size === 0) return invalid\n // Both groups always participate in a match, possibly as empty strings.\n const start = match[1]!\n const end = match[2]!\n\n // Range numerals are not limited to JS's safe integers. Compare before converting.\n const total = BigInt(size)\n let first: bigint\n let last: bigint\n if (!start) {\n const suffix = BigInt(end)\n if (suffix === 0n) return invalid\n first = suffix >= total ? 0n : total - suffix\n last = total - 1n\n } else {\n first = BigInt(start)\n last = end ? BigInt(end) : total - 1n\n if (first >= total || last < first) return invalid\n if (last >= total) last = total - 1n\n }\n return {\n status: 206,\n offset: Number(first),\n length: Number(last - first + 1n),\n }\n}\n\nexport function contentDisposition(name: string, download: boolean) {\n // TextEncoder replaces lone surrogates without requiring toWellFormed(),\n // which older browsers with module service workers do not implement.\n const wellFormed = new TextDecoder().decode(new TextEncoder().encode(name))\n const filename =\n wellFormed\n // Filenames must not contain header controls or path separators.\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f\\u007f/\\\\]/g, '_')\n .trim() || 'download'\n const fallback = filename.replace(/[^\\u0020-\\u007e]|[\";]/g, '_')\n const encoded = encodeURIComponent(filename).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n )\n return `${download ? 'attachment' : 'inline'}; filename=\"${fallback}\"; filename*=UTF-8''${encoded}`\n}\n\nexport function responseHeaders(\n metadata: { name: string; mime: string; size: number },\n range: ByteRange,\n download: boolean,\n) {\n // Only use a media type, never arbitrary header syntax from shared metadata.\n const mime = (metadata.mime.split(';', 1)[0] ?? '').trim()\n const headers = new Headers({\n 'Accept-Ranges': 'bytes',\n 'Content-Length': String(range.length),\n 'Content-Type': /^[\\w!#$&^.+-]+\\/[\\w!#$&^.+-]+$/.test(mime)\n ? mime\n : 'application/octet-stream',\n 'Content-Disposition': contentDisposition(metadata.name, download),\n ...PROTECTIVE_HEADERS,\n })\n if (range.status === 206) {\n headers.set(\n 'Content-Range',\n `bytes ${range.offset}-${range.offset + range.length - 1}/${metadata.size}`,\n )\n } else if (range.status === 416) {\n headers.set('Content-Range', `bytes */${metadata.size}`)\n }\n return headers\n}\n\nexport function errorResponse(status: number) {\n const headers = new Headers(PROTECTIVE_HEADERS)\n if (status === 405) headers.set('Allow', 'GET, HEAD')\n return new Response(null, { status, headers })\n}\n","import type { PinnedObject } from '../../wasm/sia_storage_wasm.js'\nimport { streamErrorMessage } from './errors'\nimport {\n type ClientReply,\n isSourceReply,\n type PageMessage,\n parseStreamPath,\n requestReply,\n type Connection,\n type SourceDetails,\n type WorkerMessage,\n} from './protocol'\nimport {\n type ByteRange,\n errorResponse,\n parseRange,\n responseHeaders,\n} from './response'\n\nconst SOURCE_TIMEOUT = 5_000\nconst IDLE_TIMEOUT = 5 * 60_000\n\n/** An object as the page describes it. See `SourceDetails`. */\nexport type ObjectRef = Pick<SourceDetails, 'objectId' | 'sealed'>\n\n/** A connected SDK of either kind, as the worker uses it. */\nexport type StreamSdk = {\n object(ref: ObjectRef): Promise<PinnedObject>\n download(\n object: PinnedObject,\n range: { offset: number; length: number },\n ): ReadableStream\n free(): void\n}\n\n// The parts of ServiceWorkerGlobalScope this module uses, written out so it\n// type-checks alongside page code, whose DOM types conflict with the service\n// worker ones. A real ServiceWorkerGlobalScope satisfies them.\ntype StreamClient = { id: string; postMessage(message: unknown, transfer?: Transferable[]): void }\ntype WaitUntil = { waitUntil(promise: Promise<unknown>): void }\ntype StreamFetchEvent = { request: Request; respondWith(response: Promise<Response>): void }\ntype StreamMessageEvent = WaitUntil & {\n data: unknown\n source: unknown\n ports: readonly MessagePort[]\n}\nexport type StreamScope = {\n location: { origin: string }\n skipWaiting(): Promise<void>\n clients: {\n get(id: string): Promise<StreamClient | undefined>\n claim(): Promise<void>\n }\n addEventListener(type: 'install' | 'activate', listener: (event: WaitUntil) => void): void\n addEventListener(type: 'message', listener: (event: StreamMessageEvent) => void): void\n addEventListener(type: 'fetch', listener: (event: StreamFetchEvent) => void): void\n}\ntype Metadata = Pick<\n SourceDetails,\n 'objectId' | 'sealed' | 'name' | 'mime' | 'size'\n>\ntype Chunks = ReadableStreamDefaultReader<Uint8Array>\n\n/** One page's open share, and the SDK the worker connects for it on first use. */\ntype Session = {\n key: string\n connection: Connection\n sources: Set<Source>\n objects: Map<string, Promise<PinnedObject>>\n // One `Job.lifetime` per request in flight.\n jobs: Set<Promise<void>>\n closed: boolean\n timer?: ReturnType<typeof setTimeout>\n sdk?: Promise<StreamSdk>\n}\n\n/** One stream token, resolved to its file by asking the page that made it. */\ntype Source = {\n key: string\n client: StreamClient\n closed: boolean\n // The `abort` of each job serving this token.\n requests: Set<() => void>\n ready: Promise<Metadata>\n setup: AbortController\n session?: Session\n}\n\n/** One HTTP request for a stream URL, from the first await to its last byte. */\ntype Job = {\n closed: boolean\n lifetime: Promise<void>\n source?: Source\n session?: Session\n range?: ByteRange\n reader?: Chunks\n controller?: ReadableStreamDefaultController<Uint8Array>\n finish(cancel?: boolean): void\n abort(): void\n fail(error: unknown): void\n}\n\n// The control port carries credentials and metadata only. Bytes never leave\n// this worker, since each response consumes the SDK's own stream with native\n// backpressure.\nexport function serveStreams(\n scope: StreamScope,\n connect: (connection: Connection) => Promise<StreamSdk>,\n supportsDownloads: boolean,\n) {\n const sources = new Map<string, Source>()\n const sessions = new Map<string, Session>()\n // Released session keys. A page reply already in flight when its share is\n // released must not bring the session back.\n const released = new Set<string>()\n const key = (clientId: string, id: string) => JSON.stringify([clientId, id])\n\n function idle(session: Session) {\n clearTimeout(session.timer)\n if (!session.closed && session.jobs.size === 0) {\n session.timer = setTimeout(\n () => void disposeSession(session),\n IDLE_TIMEOUT,\n )\n }\n }\n\n function sessionFor(clientId: string, details: SourceDetails) {\n const sessionKey = key(clientId, details.session)\n let session = sessions.get(sessionKey)\n if (!session) {\n session = {\n key: sessionKey,\n connection: details.connection,\n sources: new Set(),\n objects: new Map(),\n jobs: new Set(),\n closed: false,\n }\n sessions.set(sessionKey, session)\n }\n return session\n }\n\n async function disposeSession(session: Session) {\n if (session.closed) return\n session.closed = true\n sessions.delete(session.key)\n clearTimeout(session.timer)\n for (const source of session.sources) dropSource(source)\n // Do not free wasm handles while a connect, lookup or cancellation uses them.\n await Promise.allSettled(session.jobs)\n await Promise.allSettled(\n [...session.objects.values()].map(async (object) =>\n (await object).free(),\n ),\n )\n try {\n ;(await session.sdk)?.free()\n } catch {\n // Connection failures are reported by the request, not during disposal.\n }\n }\n\n // A failed connect or lookup is forgotten, so the next request retries it\n // instead of failing with the cached error.\n function sdkFor(session: Session) {\n if (session.sdk) return session.sdk\n const { connection } = session\n const connecting = Promise.resolve().then(() => connect(connection))\n session.sdk = connecting\n connecting.catch(() => {\n if (session.sdk === connecting) delete session.sdk\n })\n return connecting\n }\n\n function objectFor(session: Session, sdk: StreamSdk, ref: ObjectRef) {\n const { objectId } = ref\n const cached = session.objects.get(objectId)\n if (cached) return cached\n const lookup = Promise.resolve().then(() => sdk.object(ref))\n session.objects.set(objectId, lookup)\n lookup.catch(() => {\n if (session.objects.get(objectId) === lookup) {\n session.objects.delete(objectId)\n }\n })\n return lookup\n }\n\n function getSource(client: StreamClient, token: string) {\n const sourceKey = key(client.id, token)\n const cached = sources.get(sourceKey)\n if (cached) return cached\n const setup = new AbortController()\n const request = { type: 'sia-source', token } satisfies WorkerMessage\n const ready = requestReply(client, request, {\n timeout: SOURCE_TIMEOUT,\n signal: setup.signal,\n }).then((reply) => {\n if (!isSourceReply(reply)) {\n throw new Error('Shared file source unavailable.')\n }\n if (source.closed || released.has(key(client.id, reply.session))) {\n throw new DOMException('Released', 'AbortError')\n }\n const session = sessionFor(client.id, reply)\n source.session = session\n session.sources.add(source)\n idle(session)\n // Keep credentials in the session record, never in response metadata.\n const { objectId, sealed, name, mime, size } = reply\n return { objectId, sealed, name, mime, size }\n })\n const source: Source = {\n key: sourceKey,\n client,\n closed: false,\n requests: new Set(),\n ready,\n setup,\n }\n sources.set(sourceKey, source)\n return source\n }\n\n function dropSource(source: Source) {\n if (source.closed) return\n source.closed = true\n sources.delete(source.key)\n source.setup.abort(new DOMException('Released', 'AbortError'))\n for (const cancel of source.requests) cancel()\n source.session?.sources.delete(source)\n }\n\n /**\n * Tracks one request until it settles, whichever way that happens: its last\n * byte, the page aborting it, its source being released, or an SDK failure.\n */\n function startJob(request: Request, token: string): Job {\n let complete = () => {}\n // Kept out of event.waitUntil. Chromium kills a worker whose event runs\n // past 5 minutes, which would end any download still running at that mark.\n const lifetime = new Promise<void>((resolve) => {\n complete = resolve\n })\n const job: Job = {\n closed: false,\n lifetime,\n finish(cancel = false) {\n if (job.closed) return\n job.closed = true\n request.signal.removeEventListener('abort', job.abort)\n const { reader } = job\n const cancellation =\n cancel && reader ? reader.cancel().catch(() => {}) : undefined\n void Promise.resolve(cancellation).finally(() => {\n reader?.releaseLock()\n job.source?.requests.delete(job.abort)\n job.session?.jobs.delete(lifetime)\n if (job.session) idle(job.session)\n complete()\n })\n },\n abort() {\n if (job.closed) return\n job.controller?.error(new DOMException('Aborted', 'AbortError'))\n job.finish(true)\n },\n fail(error) {\n if (job.closed) return\n const message = streamErrorMessage(error)\n const aborted = error instanceof Error && error.name === 'AbortError'\n const { source, range } = job\n if (!aborted && source && !source.closed) {\n // Diagnostics go only to the owning page. Never log SDK objects here.\n try {\n source.client.postMessage({\n type: 'sia-stream-error',\n token,\n message,\n ...(range && { offset: range.offset, length: range.length }),\n } satisfies WorkerMessage)\n } catch {\n // The owning tab may already be gone.\n }\n }\n job.controller?.error(new Error(message))\n job.finish(true)\n },\n }\n return job\n }\n\n async function serve(\n request: Request,\n clientId: string,\n token: string,\n download: boolean,\n ): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return errorResponse(405)\n }\n if (!supportsDownloads) return errorResponse(503)\n const job = startJob(request, token)\n try {\n const client = await scope.clients.get(clientId)\n if (!client || request.signal.aborted) return errorResponse(404)\n const source = getSource(client, token)\n job.source = source\n source.requests.add(job.abort)\n request.signal.addEventListener('abort', job.abort, { once: true })\n const metadata = await source.ready\n if (job.closed) return errorResponse(502)\n // Set by `ready` before it resolves.\n const session = source.session!\n job.session = session\n session.jobs.add(job.lifetime)\n clearTimeout(session.timer)\n const range = parseRange(\n request.method === 'HEAD' ? null : request.headers.get('range'),\n metadata.size,\n )\n job.range = range\n const headers = responseHeaders(metadata, range, download)\n if (\n request.method === 'HEAD' ||\n range.status === 416 ||\n range.length === 0\n ) {\n job.finish()\n return new Response(null, { status: range.status, headers })\n }\n const sdk = await sdkFor(session)\n if (job.closed) return errorResponse(502)\n const object = await objectFor(session, sdk, metadata)\n if (job.closed) return errorResponse(502)\n const chunks: Chunks = sdk\n .download(object, { offset: range.offset, length: range.length })\n .getReader()\n job.reader = chunks\n return new Response(rangeBody(job, chunks, range.length), {\n status: range.status,\n headers,\n })\n } catch (error) {\n job.fail(error)\n if (job.source && !job.source.session) dropSource(job.source)\n return errorResponse(502)\n }\n }\n\n scope.addEventListener('install', (event) =>\n event.waitUntil(scope.skipWaiting()),\n )\n scope.addEventListener('activate', (event) =>\n event.waitUntil(scope.clients.claim()),\n )\n scope.addEventListener('message', (event) => {\n const source = event.source as Partial<StreamClient> | null\n const clientId = typeof source?.id === 'string' ? source.id : undefined\n if (!clientId) return\n const data = event.data as PageMessage | undefined\n if (data?.type === 'sia-client') {\n const reply: ClientReply = { clientId, supported: supportsDownloads }\n event.ports[0]?.postMessage(reply)\n event.ports[0]?.close()\n } else if (data?.type === 'sia-release' && typeof data.token === 'string') {\n // The session outlives its last source until IDLE_TIMEOUT, so moving to\n // the next file reuses the connected SDK.\n const source = sources.get(key(clientId, data.token))\n if (source) dropSource(source)\n } else if (\n data?.type === 'sia-release-share' &&\n typeof data.session === 'string'\n ) {\n const sessionKey = key(clientId, data.session)\n released.add(sessionKey)\n const session = sessions.get(sessionKey)\n if (session) event.waitUntil(disposeSession(session))\n }\n })\n scope.addEventListener('fetch', (event) => {\n const url = new URL(event.request.url)\n if (url.origin !== scope.location.origin) return\n const stream = parseStreamPath(url.pathname)\n if (!stream) return\n const download = url.searchParams.get('download') === '1'\n event.respondWith(\n serve(event.request, stream.clientId, stream.token, download),\n )\n })\n}\n\n/**\n * Exactly `length` bytes from `chunks`, or an error if the SDK stream ends\n * early or runs over. Each pull reads one chunk, so backpressure reaches the SDK.\n */\nfunction rangeBody(job: Job, chunks: Chunks, length: number) {\n let remaining = length\n return new ReadableStream<Uint8Array>(\n {\n start(controller) {\n job.controller = controller\n },\n async pull(controller) {\n try {\n const { done, value } = await chunks.read()\n if (job.closed) return\n if (done) {\n if (remaining !== 0) {\n throw new Error(\n 'Shared file stream ended before all bytes arrived.',\n )\n }\n controller.close()\n job.finish()\n return\n }\n remaining -= value.byteLength\n if (remaining < 0) {\n throw new Error('Shared file stream exceeded the requested length.')\n }\n controller.enqueue(value)\n if (remaining === 0) {\n controller.close()\n job.finish(true)\n }\n } catch (error) {\n job.fail(error)\n }\n },\n cancel() {\n job.finish(true)\n return job.lifetime\n },\n },\n { highWaterMark: 0 },\n )\n}\n"],"mappings":"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","names":["source"]}
|
|
1
|
+
{"version":3,"sources":["../src/stream-worker.ts","../src/stream/response.ts","../src/stream/worker.ts"],"sourcesContent":["/**\n * The worker side of streaming, for sites that already run a service worker.\n *\n * A page has only one service worker, so a site with its own calls\n * `installStreams(self)` in it and passes that worker's URL to\n * `enableStreaming`. Every other site serves the prebuilt `sia-storage-sw.js`,\n * which is this function with the WebAssembly built in.\n */\nimport wasmInit, {\n AppKey,\n Builder,\n type InitInput,\n PinnedObject,\n SharedSdk,\n} from '../wasm/sia_storage_wasm.js'\nimport type { Connection } from './stream/protocol'\nimport { serveStreams, type StreamScope, type StreamSdk } from './stream/worker'\n\nexport type { StreamScope }\n\nexport type InstallStreamsOptions = {\n /**\n * The SDK's WebAssembly, as a URL, bytes or compiled module. Only needed\n * when the worker's bundler does not emit `sia_storage_wasm_bg.wasm`.\n */\n wasm?: InitInput\n}\n\nexport function installStreams(\n scope: StreamScope,\n options: InstallStreamsOptions = {},\n) {\n let ready: Promise<unknown> | undefined\n serveStreams(\n scope,\n async (connection) => {\n ready ??= wasmInit(\n options.wasm === undefined ? undefined : { module_or_path: options.wasm },\n )\n await ready\n return connection.kind === 'shared'\n ? connectShared(connection)\n : connectApp(connection)\n },\n // Downloads from hosts need WebTransport, which some browsers leave out\n // of service workers. The page then reads objects itself.\n typeof WebTransport === 'function',\n )\n}\n\nasync function connectShared({\n indexerUrl,\n seed,\n sealKey,\n}: Extract<Connection, { kind: 'shared' }>): Promise<StreamSdk> {\n // Built before connecting, so a key that fails to load cannot leave a\n // connected SDK behind with nothing to free it.\n const key = sealKey === undefined ? undefined : new AppKey(fromHex(sealKey))\n let sdk: SharedSdk\n try {\n sdk = await SharedSdk.connect(indexerUrl, seed)\n } catch (error) {\n key?.free()\n throw error\n }\n return {\n // The page sealed the object it already had with the share's random key.\n // Opening that saves a round trip to the indexer before every stream's\n // first byte.\n object: async ({ objectId, sealed }) =>\n key && sealed ? PinnedObject.open(key, sealed) : sdk.object(objectId),\n download: (object, options) => sdk.download(object, options),\n free: () => {\n sdk.free()\n key?.free()\n },\n }\n}\n\nasync function connectApp({\n indexerUrl,\n appKey,\n appMeta,\n}: Extract<Connection, { kind: 'app' }>): Promise<StreamSdk> {\n const key = new AppKey(fromHex(appKey))\n const builder = new Builder(indexerUrl, appMeta)\n try {\n const sdk = await builder.connected(key)\n if (!sdk) throw new Error('The app key is not registered with the indexer.')\n return {\n // The page sent the object sealed with this key, so no lookup is needed.\n object: async ({ sealed }) => PinnedObject.open(key, sealed!),\n download: (object, options) => sdk.download(object, options),\n free: () => {\n sdk.free()\n key.free()\n },\n }\n } catch (error) {\n key.free()\n throw error\n } finally {\n builder.free()\n }\n}\n\n// Uint8Array.fromHex is too new for some browsers that run service workers.\nfunction fromHex(hex: string) {\n return Uint8Array.from(hex.match(/../g) ?? [], (byte) => parseInt(byte, 16))\n}\n","// File bytes come from whoever owns the share, so the browser must never cache\n// them or run them as this origin's content.\nconst PROTECTIVE_HEADERS = {\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n 'Content-Security-Policy': \"sandbox; default-src 'none'\",\n 'Referrer-Policy': 'no-referrer',\n}\n\nexport type ByteRange = {\n status: 200 | 206 | 416\n offset: number\n length: number\n}\n\nexport function parseRange(range: string | null, size: number): ByteRange {\n if (!Number.isSafeInteger(size) || size < 0) {\n throw new RangeError('Invalid file size')\n }\n if (range === null) return { status: 200, offset: 0, length: size }\n\n const invalid: ByteRange = { status: 416, offset: 0, length: 0 }\n const match = /^bytes=(\\d*)-(\\d*)$/i.exec(range.trim())\n if (!match || (!match[1] && !match[2]) || size === 0) return invalid\n // Both groups always participate in a match, possibly as empty strings.\n const start = match[1]!\n const end = match[2]!\n\n // Range numerals are not limited to JS's safe integers. Compare before converting.\n const total = BigInt(size)\n let first: bigint\n let last: bigint\n if (!start) {\n const suffix = BigInt(end)\n if (suffix === 0n) return invalid\n first = suffix >= total ? 0n : total - suffix\n last = total - 1n\n } else {\n first = BigInt(start)\n last = end ? BigInt(end) : total - 1n\n if (first >= total || last < first) return invalid\n if (last >= total) last = total - 1n\n }\n return {\n status: 206,\n offset: Number(first),\n length: Number(last - first + 1n),\n }\n}\n\nexport function contentDisposition(name: string, download: boolean) {\n // TextEncoder replaces lone surrogates without requiring toWellFormed(),\n // which older browsers with module service workers do not implement.\n const wellFormed = new TextDecoder().decode(new TextEncoder().encode(name))\n const filename =\n wellFormed\n // Filenames must not contain header controls or path separators.\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f\\u007f/\\\\]/g, '_')\n .trim() || 'download'\n const fallback = filename.replace(/[^\\u0020-\\u007e]|[\";]/g, '_')\n const encoded = encodeURIComponent(filename).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n )\n return `${download ? 'attachment' : 'inline'}; filename=\"${fallback}\"; filename*=UTF-8''${encoded}`\n}\n\nexport function responseHeaders(\n metadata: { name: string; mime: string; size: number },\n range: ByteRange,\n download: boolean,\n) {\n // Only use a media type, never arbitrary header syntax from shared metadata.\n const mime = (metadata.mime.split(';', 1)[0] ?? '').trim()\n const headers = new Headers({\n 'Accept-Ranges': 'bytes',\n 'Content-Length': String(range.length),\n 'Content-Type': /^[\\w!#$&^.+-]+\\/[\\w!#$&^.+-]+$/.test(mime)\n ? mime\n : 'application/octet-stream',\n 'Content-Disposition': contentDisposition(metadata.name, download),\n ...PROTECTIVE_HEADERS,\n })\n if (range.status === 206) {\n headers.set(\n 'Content-Range',\n `bytes ${range.offset}-${range.offset + range.length - 1}/${metadata.size}`,\n )\n } else if (range.status === 416) {\n headers.set('Content-Range', `bytes */${metadata.size}`)\n }\n return headers\n}\n\nexport function errorResponse(status: number) {\n const headers = new Headers(PROTECTIVE_HEADERS)\n if (status === 405) headers.set('Allow', 'GET, HEAD')\n return new Response(null, { status, headers })\n}\n","import type {\n PinnedObject,\n ShardProgress,\n} from '../../wasm/sia_storage_wasm.js'\nimport { streamErrorMessage } from './errors'\nimport {\n type ClientReply,\n isSourceReply,\n type PageMessage,\n parseStreamPath,\n requestReply,\n type Connection,\n type SourceDetails,\n type StreamEvent,\n type StreamStatus,\n type WorkerMessage,\n} from './protocol'\nimport {\n type ByteRange,\n errorResponse,\n parseRange,\n responseHeaders,\n} from './response'\n\nconst SOURCE_TIMEOUT = 5_000\nconst IDLE_TIMEOUT = 5 * 60_000\n// How long bytes and shard reports for one URL collect before they are sent,\n// so a stream costs a few messages a second rather than one per chunk.\nconst REPORT_INTERVAL = 100\n\n/** An object as the page describes it. See `SourceDetails`. */\nexport type ObjectRef = Pick<SourceDetails, 'objectId' | 'sealed'>\n\n/** A connected SDK of either kind, as the worker uses it. */\nexport type StreamSdk = {\n object(ref: ObjectRef): Promise<PinnedObject>\n download(\n object: PinnedObject,\n options: {\n offset: number\n length: number\n onShardDownloaded?: (progress: ShardProgress) => void\n },\n ): ReadableStream\n free(): void\n}\n\n// The parts of ServiceWorkerGlobalScope this module uses, written out so it\n// type-checks alongside page code, whose DOM types conflict with the service\n// worker ones. A real ServiceWorkerGlobalScope satisfies them.\ntype StreamClient = { id: string; postMessage(message: unknown, transfer?: Transferable[]): void }\ntype WaitUntil = { waitUntil(promise: Promise<unknown>): void }\ntype StreamFetchEvent = { request: Request; respondWith(response: Promise<Response>): void }\ntype StreamMessageEvent = WaitUntil & {\n data: unknown\n source: unknown\n ports: readonly MessagePort[]\n}\nexport type StreamScope = {\n location: { origin: string }\n skipWaiting(): Promise<void>\n clients: {\n get(id: string): Promise<StreamClient | undefined>\n claim(): Promise<void>\n }\n addEventListener(type: 'install' | 'activate', listener: (event: WaitUntil) => void): void\n addEventListener(type: 'message', listener: (event: StreamMessageEvent) => void): void\n addEventListener(type: 'fetch', listener: (event: StreamFetchEvent) => void): void\n}\ntype Metadata = Pick<\n SourceDetails,\n 'objectId' | 'sealed' | 'name' | 'mime' | 'size'\n>\ntype Chunks = ReadableStreamDefaultReader<Uint8Array>\n\n/** One page's open share, and the SDK the worker connects for it on first use. */\ntype Session = {\n key: string\n connection: Connection\n sources: Set<Source>\n objects: Map<string, Promise<PinnedObject>>\n // One `Job.lifetime` per request in flight.\n jobs: Set<Promise<void>>\n closed: boolean\n timer?: ReturnType<typeof setTimeout>\n sdk?: Promise<StreamSdk>\n}\n\n/** One stream token, resolved to its file by asking the page that made it. */\ntype Source = {\n key: string\n client: StreamClient\n closed: boolean\n // The `abort` of each job serving this token.\n requests: Set<() => void>\n ready: Promise<Metadata>\n setup: AbortController\n session?: Session\n // Set when the page asked for status and progress messages.\n reporter?: Reporter\n // Requests for this URL past their HEAD and range checks and not yet\n // settled. Its status is `idle` while this is zero.\n reading: number\n}\n\n/** One HTTP request for a stream URL, from the first await to its last byte. */\ntype Job = {\n closed: boolean\n lifetime: Promise<void>\n source?: Source\n session?: Session\n range?: ByteRange\n // Counted in `source.reading` until the job settles.\n reading?: boolean\n // A warm-up read, which reports nothing to the page.\n quiet?: boolean\n reader?: Chunks\n controller?: ReadableStreamDefaultController<Uint8Array>\n finish(cancel?: boolean): void\n abort(): void\n fail(error: unknown): void\n}\n\n// The control port carries credentials and metadata only. Bytes never leave\n// this worker, since each response consumes the SDK's own stream with native\n// backpressure.\nexport function serveStreams(\n scope: StreamScope,\n connect: (connection: Connection) => Promise<StreamSdk>,\n supportsDownloads: boolean,\n) {\n const sources = new Map<string, Source>()\n const sessions = new Map<string, Session>()\n // Released session keys. A page reply already in flight when its share is\n // released must not bring the session back.\n const released = new Set<string>()\n const key = (clientId: string, id: string) => JSON.stringify([clientId, id])\n\n function idle(session: Session) {\n clearTimeout(session.timer)\n if (!session.closed && session.jobs.size === 0) {\n session.timer = setTimeout(\n () => void disposeSession(session),\n IDLE_TIMEOUT,\n )\n }\n }\n\n function sessionFor(clientId: string, details: SourceDetails) {\n const sessionKey = key(clientId, details.session)\n let session = sessions.get(sessionKey)\n if (!session) {\n session = {\n key: sessionKey,\n connection: details.connection,\n sources: new Set(),\n objects: new Map(),\n jobs: new Set(),\n closed: false,\n }\n sessions.set(sessionKey, session)\n }\n return session\n }\n\n async function disposeSession(session: Session) {\n if (session.closed) return\n session.closed = true\n sessions.delete(session.key)\n clearTimeout(session.timer)\n for (const source of session.sources) dropSource(source)\n // Do not free wasm handles while a connect, lookup or cancellation uses them.\n await Promise.allSettled(session.jobs)\n await Promise.allSettled(\n [...session.objects.values()].map(async (object) =>\n (await object).free(),\n ),\n )\n try {\n ;(await session.sdk)?.free()\n } catch {\n // Connection failures are reported by the request, not during disposal.\n }\n }\n\n // A failed connect or lookup is forgotten, so the next request retries it\n // instead of failing with the cached error.\n function sdkFor(session: Session) {\n if (session.sdk) return session.sdk\n const { connection } = session\n const connecting = Promise.resolve().then(() => connect(connection))\n session.sdk = connecting\n connecting.catch(() => {\n if (session.sdk === connecting) delete session.sdk\n })\n return connecting\n }\n\n function objectFor(session: Session, sdk: StreamSdk, ref: ObjectRef) {\n const { objectId } = ref\n const cached = session.objects.get(objectId)\n if (cached) return cached\n const lookup = Promise.resolve().then(() => sdk.object(ref))\n session.objects.set(objectId, lookup)\n lookup.catch(() => {\n if (session.objects.get(objectId) === lookup) {\n session.objects.delete(objectId)\n }\n })\n return lookup\n }\n\n function getSource(client: StreamClient, token: string) {\n const sourceKey = key(client.id, token)\n const cached = sources.get(sourceKey)\n if (cached) return cached\n const setup = new AbortController()\n const request = { type: 'sia-source', token } satisfies WorkerMessage\n const ready = requestReply(client, request, {\n timeout: SOURCE_TIMEOUT,\n signal: setup.signal,\n }).then((reply) => {\n if (!isSourceReply(reply)) {\n throw new Error('Shared file source unavailable.')\n }\n if (source.closed || released.has(key(client.id, reply.session))) {\n throw new DOMException('Released', 'AbortError')\n }\n const session = sessionFor(client.id, reply)\n source.session = session\n session.sources.add(source)\n if (reply.events?.length) {\n source.reporter = createReporter(client, token, reply.events)\n }\n idle(session)\n // Keep credentials in the session record, never in response metadata.\n const { objectId, sealed, name, mime, size } = reply\n return { objectId, sealed, name, mime, size }\n })\n const source: Source = {\n key: sourceKey,\n client,\n closed: false,\n requests: new Set(),\n ready,\n setup,\n reading: 0,\n }\n sources.set(sourceKey, source)\n return source\n }\n\n function dropSource(source: Source) {\n if (source.closed) return\n source.closed = true\n sources.delete(source.key)\n source.reporter?.stop()\n source.setup.abort(new DOMException('Released', 'AbortError'))\n for (const cancel of source.requests) cancel()\n source.session?.sources.delete(source)\n }\n\n /**\n * Tracks one request until it settles, whichever way that happens: its last\n * byte, the page aborting it, its source being released, or an SDK failure.\n */\n function startJob(request: Request, token: string): Job {\n let complete = () => {}\n // Kept out of event.waitUntil. Chromium kills a worker whose event runs\n // past 5 minutes, which would end any download still running at that mark.\n const lifetime = new Promise<void>((resolve) => {\n complete = resolve\n })\n const job: Job = {\n closed: false,\n lifetime,\n finish(cancel = false) {\n if (job.closed) return\n job.closed = true\n request.signal.removeEventListener('abort', job.abort)\n const { reader } = job\n const cancellation =\n cancel && reader ? reader.cancel().catch(() => {}) : undefined\n void Promise.resolve(cancellation).finally(() => {\n reader?.releaseLock()\n const { source } = job\n source?.requests.delete(job.abort)\n if (source && job.reading && --source.reading === 0) {\n source.reporter?.status('idle')\n }\n job.session?.jobs.delete(lifetime)\n if (job.session) idle(job.session)\n complete()\n })\n },\n abort() {\n if (job.closed) return\n job.controller?.error(new DOMException('Aborted', 'AbortError'))\n job.finish(true)\n },\n fail(error) {\n if (job.closed) return\n const message = streamErrorMessage(error)\n const aborted = error instanceof Error && error.name === 'AbortError'\n const { source, range } = job\n // A warm-up is invisible to the page, failures included. The play\n // that follows connects again and reports its own failure.\n if (!aborted && !job.quiet && source && !source.closed) {\n // Diagnostics go only to the owning page. Never log SDK objects here.\n try {\n source.client.postMessage({\n type: 'sia-stream-error',\n token,\n message,\n ...(range && { offset: range.offset, length: range.length }),\n } satisfies WorkerMessage)\n } catch {\n // The owning tab may already be gone.\n }\n }\n job.controller?.error(new Error(message))\n job.finish(true)\n },\n }\n return job\n }\n\n async function serve(\n request: Request,\n clientId: string,\n token: string,\n { download, warm }: { download: boolean; warm: boolean },\n ): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return errorResponse(405)\n }\n if (!supportsDownloads) return errorResponse(503)\n const job = startJob(request, token)\n job.quiet = warm\n try {\n const client = await scope.clients.get(clientId)\n if (!client || request.signal.aborted) return errorResponse(404)\n const source = getSource(client, token)\n job.source = source\n source.requests.add(job.abort)\n request.signal.addEventListener('abort', job.abort, { once: true })\n const metadata = await source.ready\n if (job.closed) return errorResponse(502)\n // Set by `ready` before it resolves.\n const session = source.session!\n job.session = session\n session.jobs.add(job.lifetime)\n clearTimeout(session.timer)\n const range = parseRange(\n request.method === 'HEAD' ? null : request.headers.get('range'),\n metadata.size,\n )\n job.range = range\n const headers = responseHeaders(metadata, range, download)\n if (request.method === 'HEAD' || range.status === 416) {\n job.finish()\n return new Response(null, { status: range.status, headers })\n }\n // A warm-up is invisible to the page, so a player that warms a file\n // does not see it go connecting and downloading before Play.\n const reporter = warm ? undefined : source.reporter\n job.reading = !warm\n // A read that starts while another is already downloading leaves the\n // status at `downloading`.\n if (!warm && ++source.reading === 1) reporter?.status('connecting')\n // A zero-byte file reports connecting then idle, as a read in the page\n // does, without starting an SDK download.\n if (range.length === 0) {\n job.finish()\n return new Response(null, { status: range.status, headers })\n }\n const sdk = await sdkFor(session)\n if (job.closed) return errorResponse(502)\n const object = await objectFor(session, sdk, metadata)\n if (job.closed) return errorResponse(502)\n const chunks: Chunks = sdk\n .download(object, {\n offset: range.offset,\n length: range.length,\n ...(reporter?.wants.shards && {\n onShardDownloaded: (progress) => reporter.shard(progress),\n }),\n })\n .getReader()\n job.reader = chunks\n return new Response(rangeBody(job, chunks, range.length), {\n status: range.status,\n headers,\n })\n } catch (error) {\n job.fail(error)\n if (job.source && !job.source.session) dropSource(job.source)\n return errorResponse(502)\n }\n }\n\n scope.addEventListener('install', (event) =>\n event.waitUntil(scope.skipWaiting()),\n )\n scope.addEventListener('activate', (event) =>\n event.waitUntil(scope.clients.claim()),\n )\n scope.addEventListener('message', (event) => {\n const source = event.source as Partial<StreamClient> | null\n const clientId = typeof source?.id === 'string' ? source.id : undefined\n if (!clientId) return\n const data = event.data as PageMessage | undefined\n if (data?.type === 'sia-client') {\n const reply: ClientReply = { clientId, supported: supportsDownloads }\n event.ports[0]?.postMessage(reply)\n event.ports[0]?.close()\n } else if (data?.type === 'sia-release' && typeof data.token === 'string') {\n // The session outlives its last source until IDLE_TIMEOUT, so moving to\n // the next file reuses the connected SDK.\n const source = sources.get(key(clientId, data.token))\n if (source) dropSource(source)\n } else if (\n data?.type === 'sia-release-share' &&\n typeof data.session === 'string'\n ) {\n const sessionKey = key(clientId, data.session)\n released.add(sessionKey)\n const session = sessions.get(sessionKey)\n if (session) event.waitUntil(disposeSession(session))\n }\n })\n scope.addEventListener('fetch', (event) => {\n const url = new URL(event.request.url)\n if (url.origin !== scope.location.origin) return\n const stream = parseStreamPath(url.pathname)\n if (!stream) return\n const download = url.searchParams.get('download') === '1'\n const warm = url.searchParams.get('warm') === '1'\n event.respondWith(\n serve(event.request, stream.clientId, stream.token, { download, warm }),\n )\n })\n}\n\n/**\n * Exactly `length` bytes from `chunks`, or an error if the SDK stream ends\n * early or runs over. Each pull reads one chunk, so backpressure reaches the SDK.\n */\nfunction rangeBody(job: Job, chunks: Chunks, length: number) {\n let remaining = length\n return new ReadableStream<Uint8Array>(\n {\n start(controller) {\n job.controller = controller\n },\n async pull(controller) {\n try {\n const { done, value } = await chunks.read()\n if (job.closed) return\n if (done) {\n if (remaining !== 0) {\n throw new Error(\n 'Shared file stream ended before all bytes arrived.',\n )\n }\n controller.close()\n job.finish()\n return\n }\n remaining -= value.byteLength\n if (remaining < 0) {\n throw new Error('Shared file stream exceeded the requested length.')\n }\n controller.enqueue(value)\n const reporter = job.quiet ? undefined : job.source?.reporter\n reporter?.status('downloading')\n reporter?.bytes(value.byteLength)\n if (remaining === 0) {\n controller.close()\n job.finish(true)\n }\n } catch (error) {\n job.fail(error)\n }\n },\n cancel() {\n job.finish(true)\n return job.lifetime\n },\n },\n { highWaterMark: 0 },\n )\n}\n\ntype Reporter = {\n wants: { shards: boolean }\n status(status: StreamStatus): void\n bytes(count: number): void\n shard(progress: ShardProgress): void\n stop(): void\n}\n\n/**\n * Status and progress messages for one stream URL, sent to the page that made\n * it. Statuses go at once and only when they change. Bytes and shards collect\n * for REPORT_INTERVAL. After `stop`, every call does nothing, since the SDK\n * can still report a shard from a download being cancelled.\n */\nfunction createReporter(\n client: StreamClient,\n token: string,\n events: StreamEvent[],\n): Reporter {\n const wants = {\n status: events.includes('status'),\n progress: events.includes('progress'),\n shards: events.includes('shards'),\n }\n let stopped = false\n let current: StreamStatus | undefined\n let bytes = 0\n let shards: ShardProgress[] = []\n let timer: ReturnType<typeof setTimeout> | undefined\n\n function post(message: WorkerMessage) {\n try {\n client.postMessage(message)\n } catch {\n // The owning tab may already be gone.\n }\n }\n function flush() {\n clearTimeout(timer)\n timer = undefined\n if (stopped || (bytes === 0 && shards.length === 0)) return\n post({ type: 'sia-stream-progress', token, bytes, shards })\n bytes = 0\n shards = []\n }\n function schedule() {\n timer ??= setTimeout(flush, REPORT_INTERVAL)\n }\n function status(next: StreamStatus) {\n if (stopped || next === current) return\n flush()\n current = next\n if (wants.status) post({ type: 'sia-stream-status', token, status: next })\n }\n\n return {\n wants,\n status,\n bytes(count) {\n if (stopped || !wants.progress) return\n bytes += count\n schedule()\n },\n shard({ hostKey, shardSize, shardIndex, slabIndex, elapsedMs }) {\n if (stopped) return\n // Copied field by field, so only plain data crosses to the page.\n shards.push({ hostKey, shardSize, shardIndex, slabIndex, elapsedMs })\n schedule()\n },\n stop() {\n stopped = true\n clearTimeout(timer)\n timer = undefined\n shards = []\n },\n }\n}\n"],"mappings":"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","names":["source"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siafoundation/sia-storage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,10 +56,11 @@
|
|
|
56
56
|
"build-wasm": "bun run scripts/build-wasm.ts",
|
|
57
57
|
"setup": "bun run scripts/setup-rust.ts",
|
|
58
58
|
"setup-napi-test": "bun run scripts/setup-napi-test.ts",
|
|
59
|
-
"typecheck": "tsc --noEmit",
|
|
59
|
+
"typecheck": "tsc --noEmit && tsc -p bench",
|
|
60
60
|
"test": "bun test src/tests/install-integration.test.ts",
|
|
61
61
|
"test:browser": "bun test src/tests/vite-integration.test.ts src/tests/esbuild-integration.test.ts src/tests/webpack-integration.test.ts src/tests/rollup-integration.test.ts src/tests/nextjs-integration.test.ts src/tests/streaming.test.ts src/tests/stream-prebuilt-worker.test.ts",
|
|
62
62
|
"test:all": "bun test",
|
|
63
|
+
"bench": "bun run bench/run.ts",
|
|
63
64
|
"prepublishOnly": "bun run build && bun test",
|
|
64
65
|
"publish-npm": "bun run scripts/publish.ts"
|
|
65
66
|
},
|
|
@@ -71,10 +72,10 @@
|
|
|
71
72
|
"typescript": "5.9.3"
|
|
72
73
|
},
|
|
73
74
|
"optionalDependencies": {
|
|
74
|
-
"@siafoundation/sia-storage-darwin-arm64": "0.1.
|
|
75
|
-
"@siafoundation/sia-storage-darwin-x64": "0.1.
|
|
76
|
-
"@siafoundation/sia-storage-linux-x64-gnu": "0.1.
|
|
77
|
-
"@siafoundation/sia-storage-linux-arm64-gnu": "0.1.
|
|
78
|
-
"@siafoundation/sia-storage-win32-x64-msvc": "0.1.
|
|
75
|
+
"@siafoundation/sia-storage-darwin-arm64": "0.1.4",
|
|
76
|
+
"@siafoundation/sia-storage-darwin-x64": "0.1.4",
|
|
77
|
+
"@siafoundation/sia-storage-linux-x64-gnu": "0.1.4",
|
|
78
|
+
"@siafoundation/sia-storage-linux-arm64-gnu": "0.1.4",
|
|
79
|
+
"@siafoundation/sia-storage-win32-x64-msvc": "0.1.4"
|
|
79
80
|
}
|
|
80
81
|
}
|