@siafoundation/sia-storage 0.1.1 → 0.1.3
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 +20 -0
- package/dist/{chunk-QYQQIWFH.js → chunk-QICJ5CIY.js} +3 -2
- package/dist/chunk-QICJ5CIY.js.map +1 -0
- package/dist/index.d.ts +48 -5
- package/dist/index.js +73 -22
- package/dist/index.js.map +1 -1
- package/dist/index.node.cjs +31 -12
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.d.ts +14 -2
- package/dist/next.js +1 -1
- package/dist/sia-storage-sw.js +2 -2
- package/dist/stream-worker.js +88 -7
- package/dist/stream-worker.js.map +1 -1
- package/package.json +8 -8
- package/wasm/sia_storage_wasm.d.ts +7 -7
- package/wasm/sia_storage_wasm.js +23 -23
- package/wasm/sia_storage_wasm_bg.wasm +0 -0
- package/wasm/sia_storage_wasm_bg.wasm.d.ts +7 -7
- package/dist/chunk-QYQQIWFH.js.map +0 -1
package/README.md
CHANGED
|
@@ -96,6 +96,26 @@ button.onclick = () => streams.download(object, { name: 'clip.mp4' })
|
|
|
96
96
|
streams.close()
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
### Status and progress
|
|
100
|
+
|
|
101
|
+
`url()` and `download()` take three optional callbacks, for a loading state, a progress readout or a map of the hosts serving the file. They work the same whether the worker streams the file or the page reads it itself.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const video = await streams.url(object, {
|
|
105
|
+
name: 'clip.mp4',
|
|
106
|
+
type: 'video/mp4',
|
|
107
|
+
onStatus: (status) => setStatus(status),
|
|
108
|
+
onProgress: (bytes) => setReceived(bytes),
|
|
109
|
+
onShard: (shard) => markHost(shard.hostKey, shard.slabIndex),
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **`onStatus`** goes `connecting`, `downloading`, `idle`, and around again for each new read, such as a seek. `connecting` lasts until the first bytes arrive, and on the URL's first read it covers the worker connecting to the indexer. `idle` means no read is in flight, after the last one finished, was cancelled or failed.
|
|
114
|
+
- **`onProgress`** is the total bytes the URL has received. It counts transfer, not coverage: a range read again after a seek counts again, so it can pass the file size, and a new URL for the same file starts from zero. For how much of a video is buffered, use the media element's `buffered`. A `download()` reads the file once, so its total ends at the file size.
|
|
115
|
+
- **`onShard`** is called for each piece the SDK reads from a host: the host key, slab, shard index, bytes read and time taken. A slab is read in chunks, so the same shard comes up once per chunk.
|
|
116
|
+
|
|
117
|
+
A URL without these callbacks costs nothing, and one with only some of them gets only those reports. The worker sends bytes and shards in batches, at most every 100 ms and just before each status change, and stops when the URL is released. A callback that throws is reported to the console and does not stop the stream or the other callbacks.
|
|
118
|
+
|
|
99
119
|
### Serve the worker
|
|
100
120
|
|
|
101
121
|
The worker is one file, `sia-storage-sw.js`, served from your site's root. Use the line for your setup.
|
|
@@ -14,6 +14,7 @@ function reportStreamError(error, name, range) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// src/stream/protocol.ts
|
|
17
|
+
var STREAM_EVENTS = ["status", "progress", "shards"];
|
|
17
18
|
var STREAM_PATH = /\/__sia_stream__\/([^/]+)\/([^/]+)\/[^/]+$/;
|
|
18
19
|
function streamPath(scope, clientId, token) {
|
|
19
20
|
return `${scope}__sia_stream__/${clientId}/${token}/file`;
|
|
@@ -33,7 +34,7 @@ function isConnection(value) {
|
|
|
33
34
|
}
|
|
34
35
|
function isSourceReply(reply) {
|
|
35
36
|
const data = reply;
|
|
36
|
-
return data?.type === "source" && strings(data, ["session", "objectId", "name", "mime"]) && isConnection(data.connection) && (data.connection.kind === "shared" || typeof data.sealed === "object" && data.sealed !== null) && Number.isSafeInteger(data.size) && data.size >= 0;
|
|
37
|
+
return data?.type === "source" && strings(data, ["session", "objectId", "name", "mime"]) && isConnection(data.connection) && (data.connection.kind === "shared" || typeof data.sealed === "object" && data.sealed !== null) && Number.isSafeInteger(data.size) && data.size >= 0 && (data.events === void 0 || Array.isArray(data.events) && data.events.every((event) => STREAM_EVENTS.includes(event)));
|
|
37
38
|
}
|
|
38
39
|
function requestReply(target, message, { timeout, signal }) {
|
|
39
40
|
return new Promise((resolve, reject) => {
|
|
@@ -71,4 +72,4 @@ export {
|
|
|
71
72
|
isSourceReply,
|
|
72
73
|
requestReply
|
|
73
74
|
};
|
|
74
|
-
//# sourceMappingURL=chunk-
|
|
75
|
+
//# sourceMappingURL=chunk-QICJ5CIY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stream/errors.ts","../src/stream/protocol.ts"],"sourcesContent":["/** Only sanitized strings cross the diagnostic boundary, never raw SDK objects. */\nexport function streamErrorMessage(error: unknown): string {\n const message =\n error instanceof Error\n ? error.message\n : typeof error === 'string'\n ? error\n : 'Unknown streaming failure'\n return message\n .replace(/\\b(?:https?|wss?|sia):\\/\\/[^\\s\"'<>]+/gi, '[URL redacted]')\n .replace(/(?:bearer\\s+)[^\\s,;]+/gi, 'Bearer [redacted]')\n .replace(\n /((?:seed|share|token|key|authorization)\\s*[=:]\\s*)[^\\s,;]+/gi,\n '$1[redacted]',\n )\n .replace(/[a-f0-9]{64,}/gi, '[redacted]')\n .replace(/[\\r\\n\\t]/g, ' ')\n .slice(0, 1000)\n}\n\nexport function reportStreamError(\n error: unknown,\n name: string,\n range: { offset: number; length: number } | undefined,\n) {\n const message = streamErrorMessage(error)\n // One string rather than an object, so it reads the same in every console.\n const bytes = range\n ? ` bytes ${range.offset}-${range.offset + range.length - 1}`\n : ''\n console.error(`[Sia stream] ${streamErrorMessage(name)}${bytes}: ${message}`)\n return message\n}\n","/**\n * Messages between a page and its streaming service worker.\n *\n * Stream URLs carry only the page's client id and a random token. When the\n * worker meets a token it does not know, it asks the page that made it for the\n * file and the share's credentials. The page and a worker can come from\n * different deployed versions, so the worker checks the page's reply before\n * using it.\n */\nimport type {\n AppMetadata,\n SealedObject,\n ShardProgress,\n} from '../../wasm/sia_storage_wasm.js'\n\n/**\n * What a stream URL is doing. `connecting` is a request waiting for its first\n * bytes, which on the URL's first request includes the worker connecting its\n * SDK and finding the object. `downloading` means bytes are arriving. `idle`\n * means no request for the URL is in flight, after the last one finished, was\n * cancelled or failed.\n */\nexport type StreamStatus = 'connecting' | 'downloading' | 'idle'\n\n/** The reports a page can ask for, one per callback it was given. */\nexport type StreamEvent = 'status' | 'progress' | 'shards'\nconst STREAM_EVENTS: readonly unknown[] = ['status', 'progress', 'shards']\n\n/**\n * What the worker connects its own SDK with. A `SharedSdk` reconnects from its\n * sharing key's seed, and an `Sdk` from its app key, as hex, and app metadata.\n */\nexport type Connection =\n | { kind: 'shared'; indexerUrl: string; seed: string }\n | { kind: 'app'; indexerUrl: string; appKey: string; appMeta: AppMetadata }\n\nexport type SourceDetails = {\n // One per opened SDK, so the worker can release it as a whole.\n session: string\n connection: Connection\n objectId: string\n // An `Sdk`'s object, sealed with its app key, so the worker can open it\n // without a lookup. A `SharedSdk`'s object is looked up by `objectId`.\n sealed?: SealedObject\n name: string\n mime: string\n size: number\n // The reports the page wants for this URL. The worker sends no others, and\n // asks the SDK for shard reports only when `shards` is here.\n events?: StreamEvent[]\n}\n\n/** Page to worker. */\nexport type PageMessage =\n | { type: 'sia-client' }\n | { type: 'sia-release'; token: string }\n | { type: 'sia-release-share'; session: string }\n\n/** Worker to page. */\nexport type WorkerMessage =\n | { type: 'sia-source'; token: string }\n | {\n type: 'sia-stream-error'\n token: string\n message: string\n offset?: number\n length?: number\n }\n | { type: 'sia-stream-status'; token: string; status: StreamStatus }\n | {\n type: 'sia-stream-progress'\n token: string\n // Bytes served since the previous progress message.\n bytes: number\n shards: ShardProgress[]\n }\n\n/** The worker's answer to `sia-client`. */\nexport type ClientReply = { clientId: string; supported: boolean }\n\n/** The page's answer to `sia-source`. */\nexport type SourceReply =\n | ({ type: 'source' } & SourceDetails)\n | { type: 'error' }\n\n// Stream URLs sit under the worker's scope, so a download's navigation, which\n// only a worker whose scope covers the URL can answer, reaches it on sites\n// served from a sub-path too.\nconst STREAM_PATH = /\\/__sia_stream__\\/([^/]+)\\/([^/]+)\\/[^/]+$/\n\n/** `scope` is the worker registration's scope URL, which ends in a slash. */\nexport function streamPath(scope: string, clientId: string, token: string) {\n return `${scope}__sia_stream__/${clientId}/${token}/file`\n}\n\nexport function parseStreamPath(pathname: string) {\n const match = STREAM_PATH.exec(pathname)\n return match ? { clientId: match[1]!, token: match[2]! } : undefined\n}\n\ntype Fields = Record<string, unknown> | null | undefined\n\nconst strings = (data: Fields, fields: string[]) =>\n fields.every((field) => typeof data?.[field] === 'string')\n\n/** Checked on both sides, so bad metadata fails in `openStreams`, not per request. */\nexport function isAppMetadata(value: unknown): value is AppMetadata {\n return strings(value as Fields, ['appId', 'name', 'description', 'serviceUrl'])\n}\n\nfunction isConnection(value: unknown): value is Connection {\n const data = value as Fields\n if (data?.kind === 'shared') return strings(data, ['indexerUrl', 'seed'])\n return (\n data?.kind === 'app' &&\n strings(data, ['indexerUrl', 'appKey']) &&\n isAppMetadata(data.appMeta)\n )\n}\n\nexport function isSourceReply(\n reply: unknown,\n): reply is { type: 'source' } & SourceDetails {\n const data = reply as Fields\n return (\n data?.type === 'source' &&\n strings(data, ['session', 'objectId', 'name', 'mime']) &&\n isConnection(data.connection) &&\n (data.connection.kind === 'shared' ||\n (typeof data.sealed === 'object' && data.sealed !== null)) &&\n Number.isSafeInteger(data.size) &&\n (data.size as number) >= 0 &&\n (data.events === undefined ||\n (Array.isArray(data.events) &&\n data.events.every((event) => STREAM_EVENTS.includes(event))))\n )\n}\n\n/**\n * Posts `message` with a reply port and resolves with the first reply. Rejects\n * after `timeout` ms, or when `signal` aborts.\n */\nexport function requestReply(\n target: { postMessage(message: unknown, transfer: Transferable[]): void },\n message: unknown,\n { timeout, signal }: { timeout?: number; signal?: AbortSignal },\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const { port1, port2 } = new MessageChannel()\n const timer =\n timeout === undefined\n ? undefined\n : setTimeout(\n () => settle(() => reject(new Error('No reply in time.'))),\n timeout,\n )\n const abort = () => settle(() => reject(signal?.reason))\n function settle(outcome: () => void) {\n clearTimeout(timer)\n signal?.removeEventListener('abort', abort)\n port1.close()\n port2.close()\n outcome()\n }\n if (signal?.aborted) return abort()\n signal?.addEventListener('abort', abort, { once: true })\n port1.onmessage = ({ data }) => settle(() => resolve(data))\n port1.onmessageerror = () =>\n settle(() => reject(new Error('Unreadable reply.')))\n try {\n target.postMessage(message, [port2])\n } catch (error) {\n settle(() => reject(error))\n }\n })\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,53 @@
|
|
|
1
|
-
import { AppMetadata, Sdk, SharedSdk, PinnedObject } from '../wasm/sia_storage_wasm.js';
|
|
1
|
+
import { ShardProgress, AppMetadata, Sdk, SharedSdk, PinnedObject } from '../wasm/sia_storage_wasm.js';
|
|
2
2
|
export { Account, AppKey, AppMetadata, Builder, DownloadOptions, Host, HostQuery, KeyRecord, KeyStats, ObjectEvent, PackedUpload, PinnedObject, PinnedSlab, Sdk, SealedObject, Sector, ShardProgress, SharedSdk, SharingKey, Slab, UploadOptions, encodedSize, generateRecoveryPhrase, setLogger, validateRecoveryPhrase } from '../wasm/sia_storage_wasm.js';
|
|
3
3
|
|
|
4
4
|
declare function detectPrivateRelay(): Promise<boolean>;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Messages between a page and its streaming service worker.
|
|
8
|
+
*
|
|
9
|
+
* Stream URLs carry only the page's client id and a random token. When the
|
|
10
|
+
* worker meets a token it does not know, it asks the page that made it for the
|
|
11
|
+
* file and the share's credentials. The page and a worker can come from
|
|
12
|
+
* different deployed versions, so the worker checks the page's reply before
|
|
13
|
+
* using it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What a stream URL is doing. `connecting` is a request waiting for its first
|
|
18
|
+
* bytes, which on the URL's first request includes the worker connecting its
|
|
19
|
+
* SDK and finding the object. `downloading` means bytes are arriving. `idle`
|
|
20
|
+
* means no request for the URL is in flight, after the last one finished, was
|
|
21
|
+
* cancelled or failed.
|
|
22
|
+
*/
|
|
23
|
+
type StreamStatus = 'connecting' | 'downloading' | 'idle';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Previews and downloads for pages that cannot stream through the service
|
|
27
|
+
* worker, where the page reads the object itself. A preview holds the whole
|
|
28
|
+
* file in memory as a Blob. A download writes straight to disk through the
|
|
29
|
+
* save picker where the browser has one, and buffers a Blob where it does not.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** What a file reports while it downloads, on the worker or in the page. */
|
|
33
|
+
type FileEvents = {
|
|
34
|
+
/**
|
|
35
|
+
* Total bytes this URL has received. It is a transfer count, not how much of
|
|
36
|
+
* the file is held: a range read again after a seek counts again, so it can
|
|
37
|
+
* pass the file size, and a new URL for the same file starts from zero. For
|
|
38
|
+
* how much of a video is buffered, read the media element's `buffered`.
|
|
39
|
+
*/
|
|
40
|
+
onProgress?: (bytesDownloaded: number) => void;
|
|
41
|
+
/**
|
|
42
|
+
* One call per piece the SDK read from a host. A download reads each slab in
|
|
43
|
+
* chunks, so the same slab and shard come again for every chunk, and only
|
|
44
|
+
* the first `minShards` hosts to answer for a chunk are reported.
|
|
45
|
+
*/
|
|
46
|
+
onShard?: (progress: ShardProgress) => void;
|
|
47
|
+
/** Each change of what the URL is doing. See `StreamStatus`. */
|
|
48
|
+
onStatus?: (status: StreamStatus) => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
6
51
|
/**
|
|
7
52
|
* The page side of streaming.
|
|
8
53
|
*
|
|
@@ -34,15 +79,13 @@ type AppCredentials = {
|
|
|
34
79
|
};
|
|
35
80
|
/** The credentials `openStreams` takes for each kind of SDK. */
|
|
36
81
|
type CredentialsFor<S extends Sdk | SharedSdk> = S extends SharedSdk ? SharedCredentials : AppCredentials;
|
|
37
|
-
type FileOptions = {
|
|
82
|
+
type FileOptions = FileEvents & {
|
|
38
83
|
/** Used for downloads and for the browser's media handling. */
|
|
39
84
|
name: string;
|
|
40
85
|
/** The MIME type. Defaults to `application/octet-stream`. */
|
|
41
86
|
type?: string;
|
|
42
87
|
/** A failure after streaming started. Without it, failures are logged. */
|
|
43
88
|
onError?: (message: string) => void;
|
|
44
|
-
/** Bytes read so far, when the page reads the object itself. */
|
|
45
|
-
onProgress?: (bytesDownloaded: number) => void;
|
|
46
89
|
};
|
|
47
90
|
/** A URL for an `<img>`, `<video>`, `<audio>`, `<iframe>` or `fetch`. */
|
|
48
91
|
type StreamedFile = {
|
|
@@ -91,4 +134,4 @@ declare function openStreams<S extends Sdk | SharedSdk>(sdk: S, credentials: Cre
|
|
|
91
134
|
/** Initialize the WASM module. Safe to call multiple times. */
|
|
92
135
|
declare function initSia(): Promise<void>;
|
|
93
136
|
|
|
94
|
-
export { type AppCredentials, type CredentialsFor, type FileOptions, type SharedCredentials, type StreamedFile, type StreamingOptions, type Streams, detectPrivateRelay, enableStreaming, initSia, openStreams };
|
|
137
|
+
export { type AppCredentials, type CredentialsFor, type FileEvents, type FileOptions, type SharedCredentials, type StreamStatus, type StreamedFile, type StreamingOptions, type Streams, detectPrivateRelay, enableStreaming, initSia, openStreams };
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
requestReply,
|
|
5
5
|
streamErrorMessage,
|
|
6
6
|
streamPath
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QICJ5CIY.js";
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
10
|
import wasmInit from "../wasm/sia_storage_wasm.js";
|
|
@@ -35,22 +35,54 @@ async function checkPrivateRelay(userAgent, request) {
|
|
|
35
35
|
function chunks(stream) {
|
|
36
36
|
return stream.getReader();
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
function notify(callback, value) {
|
|
39
|
+
if (!callback) return;
|
|
40
|
+
try {
|
|
41
|
+
callback(value);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
reportError(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function download(sdk, object, events) {
|
|
47
|
+
const { onShard } = events;
|
|
48
|
+
return sdk.download(
|
|
49
|
+
object,
|
|
50
|
+
onShard ? { onShardDownloaded: (shard) => notify(onShard, shard) } : {}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function track({ onProgress, onStatus }) {
|
|
54
|
+
let received = 0;
|
|
55
|
+
let started = false;
|
|
56
|
+
notify(onStatus, "connecting");
|
|
57
|
+
return {
|
|
58
|
+
chunk(length) {
|
|
59
|
+
if (!started) notify(onStatus, "downloading");
|
|
60
|
+
started = true;
|
|
61
|
+
received += length;
|
|
62
|
+
notify(onProgress, received);
|
|
63
|
+
},
|
|
64
|
+
end() {
|
|
65
|
+
notify(onStatus, "idle");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function readBlob(sdk, object, type, events, signal) {
|
|
70
|
+
const progress = track(events);
|
|
71
|
+
let reader;
|
|
72
|
+
const onAbort = () => void reader?.cancel();
|
|
41
73
|
signal.addEventListener("abort", onAbort);
|
|
42
74
|
const parts = [];
|
|
43
|
-
let bytesDownloaded = 0;
|
|
44
75
|
try {
|
|
76
|
+
reader = chunks(download(sdk, object, events));
|
|
45
77
|
while (true) {
|
|
46
78
|
const { done, value } = await reader.read();
|
|
47
79
|
if (done) break;
|
|
48
80
|
parts.push(value);
|
|
49
|
-
|
|
50
|
-
onProgress(bytesDownloaded);
|
|
81
|
+
progress.chunk(value.length);
|
|
51
82
|
}
|
|
52
83
|
} finally {
|
|
53
84
|
signal.removeEventListener("abort", onAbort);
|
|
85
|
+
progress.end();
|
|
54
86
|
}
|
|
55
87
|
signal.throwIfAborted();
|
|
56
88
|
return new Blob(parts, type ? { type } : {});
|
|
@@ -63,14 +95,14 @@ function saveBlob(blob, name) {
|
|
|
63
95
|
a.click();
|
|
64
96
|
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
65
97
|
}
|
|
66
|
-
async function saveToDisk(sdk, object, name, type,
|
|
98
|
+
async function saveToDisk(sdk, object, name, type, events) {
|
|
67
99
|
const picker = window.showSaveFilePicker;
|
|
68
100
|
if (!picker) {
|
|
69
101
|
const blob = await readBlob(
|
|
70
102
|
sdk,
|
|
71
103
|
object,
|
|
72
104
|
type,
|
|
73
|
-
|
|
105
|
+
events,
|
|
74
106
|
new AbortController().signal
|
|
75
107
|
);
|
|
76
108
|
saveBlob(blob, name);
|
|
@@ -83,16 +115,19 @@ async function saveToDisk(sdk, object, name, type, onProgress) {
|
|
|
83
115
|
if (e instanceof DOMException && e.name === "AbortError") return false;
|
|
84
116
|
throw e;
|
|
85
117
|
}
|
|
86
|
-
|
|
87
|
-
const progress =
|
|
118
|
+
const file = await handle.createWritable();
|
|
119
|
+
const progress = track(events);
|
|
120
|
+
const counter = new TransformStream({
|
|
88
121
|
transform(chunk, controller) {
|
|
89
|
-
|
|
90
|
-
onProgress(bytesDownloaded);
|
|
122
|
+
progress.chunk(chunk.length);
|
|
91
123
|
controller.enqueue(chunk);
|
|
92
124
|
}
|
|
93
125
|
});
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
try {
|
|
127
|
+
await download(sdk, object, events).pipeThrough(counter).pipeTo(file);
|
|
128
|
+
} finally {
|
|
129
|
+
progress.end();
|
|
130
|
+
}
|
|
96
131
|
return true;
|
|
97
132
|
}
|
|
98
133
|
|
|
@@ -181,8 +216,7 @@ function openStreams(sdk, credentials) {
|
|
|
181
216
|
sdk,
|
|
182
217
|
object,
|
|
183
218
|
options.type,
|
|
184
|
-
options
|
|
185
|
-
}),
|
|
219
|
+
options,
|
|
186
220
|
options.signal ?? new AbortController().signal
|
|
187
221
|
);
|
|
188
222
|
const url = URL.createObjectURL(blob);
|
|
@@ -202,8 +236,7 @@ function openStreams(sdk, credentials) {
|
|
|
202
236
|
object,
|
|
203
237
|
options.name,
|
|
204
238
|
options.type,
|
|
205
|
-
options
|
|
206
|
-
})
|
|
239
|
+
options
|
|
207
240
|
);
|
|
208
241
|
return saved ? "saved" : "cancelled";
|
|
209
242
|
},
|
|
@@ -236,7 +269,7 @@ function openedFor(sdk, credentials) {
|
|
|
236
269
|
function toHex(bytes) {
|
|
237
270
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
238
271
|
}
|
|
239
|
-
function streamUrl(handle, object, { name, type, onError }) {
|
|
272
|
+
function streamUrl(handle, object, { name, type, onError, onProgress, onShard, onStatus }) {
|
|
240
273
|
if (!worker || !navigator.serviceWorker.controller || !opened.has(handle)) {
|
|
241
274
|
return void 0;
|
|
242
275
|
}
|
|
@@ -248,7 +281,9 @@ function streamUrl(handle, object, { name, type, onError }) {
|
|
|
248
281
|
size: object.size(),
|
|
249
282
|
name,
|
|
250
283
|
mime: type || "application/octet-stream",
|
|
251
|
-
onError
|
|
284
|
+
onError,
|
|
285
|
+
events: { onProgress, onShard, onStatus },
|
|
286
|
+
received: 0
|
|
252
287
|
});
|
|
253
288
|
return {
|
|
254
289
|
url: streamPath(worker.scope, worker.clientId, token),
|
|
@@ -280,10 +315,25 @@ function onWorkerMessage(event) {
|
|
|
280
315
|
const range = offset !== void 0 && length !== void 0 ? { offset, length } : void 0;
|
|
281
316
|
if (entry.onError) entry.onError(streamErrorMessage(data.message));
|
|
282
317
|
else reportStreamError(data.message, entry.name, range);
|
|
318
|
+
} else if (data?.type === "sia-stream-status") {
|
|
319
|
+
notify(entries.get(data.token)?.events.onStatus, data.status);
|
|
320
|
+
} else if (data?.type === "sia-stream-progress") {
|
|
321
|
+
const entry = entries.get(data.token);
|
|
322
|
+
if (!entry || !Array.isArray(data.shards)) return;
|
|
323
|
+
const { onProgress, onShard } = entry.events;
|
|
324
|
+
for (const shard of data.shards) notify(onShard, shard);
|
|
325
|
+
if (Number.isSafeInteger(data.bytes) && data.bytes > 0) {
|
|
326
|
+
entry.received += data.bytes;
|
|
327
|
+
notify(onProgress, entry.received);
|
|
328
|
+
}
|
|
283
329
|
} else if (data?.type === "sia-source") {
|
|
284
330
|
const port = event.ports[0];
|
|
285
331
|
if (!port) return;
|
|
286
332
|
const entry = entries.get(data.token);
|
|
333
|
+
const events = [];
|
|
334
|
+
if (entry?.events.onStatus) events.push("status");
|
|
335
|
+
if (entry?.events.onProgress) events.push("progress");
|
|
336
|
+
if (entry?.events.onShard) events.push("shards");
|
|
287
337
|
const reply = entry ? {
|
|
288
338
|
type: "source",
|
|
289
339
|
session: entry.handle.session,
|
|
@@ -292,7 +342,8 @@ function onWorkerMessage(event) {
|
|
|
292
342
|
...entry.sealed && { sealed: entry.sealed },
|
|
293
343
|
size: entry.size,
|
|
294
344
|
mime: entry.mime,
|
|
295
|
-
name: entry.name
|
|
345
|
+
name: entry.name,
|
|
346
|
+
...events.length > 0 && { events }
|
|
296
347
|
} : { type: "error" };
|
|
297
348
|
port.postMessage(reply);
|
|
298
349
|
port.close();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/relay.ts","../src/stream/fallback.ts","../src/stream/page.ts"],"sourcesContent":["// Browser entry; lazily initializes WASM on first use.\n\nimport wasmInit from '../wasm/sia_storage_wasm.js'\n\nexport { detectPrivateRelay } from './relay'\nexport { enableStreaming, openStreams } from './stream/page'\nexport type {\n AppCredentials,\n CredentialsFor,\n FileOptions,\n SharedCredentials,\n StreamedFile,\n Streams,\n StreamingOptions,\n} from './stream/page'\n\nlet initPromise: Promise<unknown> | null = null\n\n/** Initialize the WASM module. Safe to call multiple times. */\nexport async function initSia(): Promise<void> {\n if (!initPromise) initPromise = wasmInit()\n await initPromise\n}\n\nexport {\n AppKey,\n Builder,\n KeyRecord,\n ObjectEvent,\n PackedUpload,\n PinnedObject,\n Sdk,\n SharedSdk,\n SharingKey,\n encodedSize,\n generateRecoveryPhrase,\n setLogger,\n validateRecoveryPhrase,\n} from '../wasm/sia_storage_wasm.js'\nexport type {\n Account,\n AppMetadata,\n DownloadOptions,\n Host,\n HostQuery,\n KeyStats,\n PinnedSlab,\n Sector,\n SealedObject,\n ShardProgress,\n Slab,\n UploadOptions,\n} from '../wasm/sia_storage_wasm.js'\n","/**\n * Whether the visitor is on iCloud Private Relay, which the SDK does not work\n * through. Show them a way to turn it off in iCloud settings.\n *\n * Asks check-relay.sia.storage, a Sia Foundation service that compares the\n * caller's address with Apple's published list of Private Relay addresses.\n * Only Safari's traffic goes through Private Relay, so other browsers resolve\n * false without a request. The answer is kept for the page's lifetime, and a\n * failure to get one resolves false.\n */\nconst RELAY_CHECK_URL = 'https://check-relay.sia.storage'\nconst TIMEOUT = 5000\n\nlet answer: Promise<boolean> | undefined\n\nexport function detectPrivateRelay(): Promise<boolean> {\n answer ??=\n typeof navigator === 'undefined'\n ? Promise.resolve(false)\n : checkPrivateRelay(navigator.userAgent, fetch)\n return answer\n}\n\n/** `detectPrivateRelay` without the caching, for tests. */\nexport async function checkPrivateRelay(\n userAgent: string,\n request: typeof fetch,\n): Promise<boolean> {\n const safari =\n /Version\\/\\S+.*Safari\\//.test(userAgent) &&\n !/Chrome|Chromium|CriOS|FxiOS|Edg|OPR|OPiOS|Ddg\\/|Android/.test(userAgent)\n if (!safari) return false\n try {\n const response = await request(RELAY_CHECK_URL, {\n signal: AbortSignal.timeout(TIMEOUT),\n })\n const body = (await response.json()) as { relay?: unknown }\n return body.relay === true\n } catch {\n return false\n }\n}\n","/**\n * Previews and downloads for pages that cannot stream through the service\n * worker, where the page reads the object itself. A preview holds the whole\n * file in memory as a Blob. A download writes straight to disk through the\n * save picker where the browser has one, and buffers a Blob where it does not.\n */\nimport type { PinnedObject, Sdk, SharedSdk } from '../../wasm/sia_storage_wasm.js'\n\ntype AnySdk = Pick<Sdk | SharedSdk, 'download'>\n\ntype Chunk = Uint8Array<ArrayBuffer>\n\n// The File System Access save picker, in Chromium only and not yet in\n// TypeScript's DOM types.\ntype SavePicker = (options: {\n suggestedName: string\n}) => Promise<FileSystemFileHandle & { createWritable(): Promise<WritableStream<Chunk>> }>\n\nfunction chunks(stream: ReadableStream): ReadableStreamDefaultReader<Chunk> {\n return stream.getReader() as ReadableStreamDefaultReader<Chunk>\n}\n\n/** The whole object as a Blob, stopping early when `signal` aborts. */\nexport async function readBlob(\n sdk: AnySdk,\n object: PinnedObject,\n type: string | undefined,\n onProgress: (bytesDownloaded: number) => void,\n signal: AbortSignal,\n) {\n const reader = chunks(sdk.download(object))\n const onAbort = () => void reader.cancel()\n signal.addEventListener('abort', onAbort)\n const parts: Chunk[] = []\n let bytesDownloaded = 0\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n parts.push(value)\n bytesDownloaded += value.length\n onProgress(bytesDownloaded)\n }\n } finally {\n signal.removeEventListener('abort', onAbort)\n }\n signal.throwIfAborted()\n return new Blob(parts, type ? { type } : {})\n}\n\nexport function saveBlob(blob: Blob, name: string) {\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = name\n a.click()\n // Some browsers cancel the download if the URL goes away in the same task.\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/** False if the visitor closes the save picker. */\nexport async function saveToDisk(\n sdk: AnySdk,\n object: PinnedObject,\n name: string,\n type: string | undefined,\n onProgress: (bytesDownloaded: number) => void,\n) {\n const picker = (window as { showSaveFilePicker?: SavePicker })\n .showSaveFilePicker\n if (!picker) {\n const blob = await readBlob(\n sdk,\n object,\n type,\n onProgress,\n new AbortController().signal,\n )\n saveBlob(blob, name)\n return true\n }\n\n // The picker must open inside the click that asked for it, so nothing is\n // awaited before this call.\n let handle: Awaited<ReturnType<SavePicker>>\n try {\n handle = await picker.call(window, { suggestedName: name })\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') return false\n throw e\n }\n let bytesDownloaded = 0\n const progress = new TransformStream<Chunk, Chunk>({\n transform(chunk, controller) {\n bytesDownloaded += chunk.length\n onProgress(bytesDownloaded)\n controller.enqueue(chunk)\n },\n })\n const file = await handle.createWritable()\n await (sdk.download(object) as ReadableStream<Chunk>)\n .pipeThrough(progress)\n .pipeTo(file)\n return true\n}\n","/**\n * The page side of streaming.\n *\n * `openStreams` hands out same-origin URLs that the library's service worker\n * answers with ranged reads of an object, so a `<video>` seeks without loading\n * the whole file and a download goes to the browser's download manager instead\n * of the page's memory. The worker connects its own SDK of the same kind, with\n * credentials this page hands it over a message port and it keeps in memory.\n * Where the worker is unavailable, the same calls read the object in the page.\n */\nimport type {\n AppKey,\n AppMetadata,\n PinnedObject,\n SealedObject,\n Sdk,\n SharedSdk,\n} from '../../wasm/sia_storage_wasm.js'\nimport { reportStreamError, streamErrorMessage } from './errors'\nimport { readBlob, saveToDisk } from './fallback'\nimport {\n type ClientReply,\n type Connection,\n isAppMetadata,\n type PageMessage,\n requestReply,\n type SourceReply,\n streamPath,\n type WorkerMessage,\n} from './protocol'\n\nexport type StreamingOptions = {\n /** Where the site serves the worker file. Defaults to `/sia-storage-sw.js`. */\n workerUrl?: string\n /** The worker's scope. Defaults to the directory `workerUrl` is in. */\n scope?: string\n /** `'module'` when `workerUrl` is an ES module worker of your own. */\n type?: WorkerType\n}\n\n/** What a `SharedSdk` was connected with. */\nexport type SharedCredentials = { indexerUrl: string; seed: string }\n\n/** What an `Sdk`'s `Builder` was made with. The key comes from `sdk.appKey()`. */\nexport type AppCredentials = { indexerUrl: string; appMeta: AppMetadata }\n\n/** The credentials `openStreams` takes for each kind of SDK. */\nexport type CredentialsFor<S extends Sdk | SharedSdk> = S extends SharedSdk\n ? SharedCredentials\n : AppCredentials\n\nexport type FileOptions = {\n /** Used for downloads and for the browser's media handling. */\n name: string\n /** The MIME type. Defaults to `application/octet-stream`. */\n type?: string\n /** A failure after streaming started. Without it, failures are logged. */\n onError?: (message: string) => void\n /** Bytes read so far, when the page reads the object itself. */\n onProgress?: (bytesDownloaded: number) => void\n}\n\n/** A URL for an `<img>`, `<video>`, `<audio>`, `<iframe>` or `fetch`. */\nexport type StreamedFile = {\n url: string\n /** The whole file, when the page read it itself instead of streaming. */\n blob?: Blob\n /** Frees the URL. Call it when the element showing it goes away. */\n release(): void\n}\n\nexport type Streams = {\n /** A URL serving the object. Streams where it can, else reads it whole. */\n url(\n object: PinnedObject,\n options: FileOptions & { signal?: AbortSignal },\n ): Promise<StreamedFile>\n /**\n * Saves the object, from a click handler. Streams to the browser's download\n * manager where it can, else opens the save picker, else saves a Blob.\n * Awaits nothing before the picker opens, so the browser still counts it as\n * part of the click.\n */\n download(\n object: PinnedObject,\n options: FileOptions,\n ): Promise<'streaming' | 'saved' | 'cancelled'>\n /**\n * Cancels the streams this handle made. Other handles on the same SDK keep\n * theirs. Call it when you stop using the handle.\n */\n close(): void\n}\n\nconst DEFAULT_WORKER_URL = '/sia-storage-sw.js'\n// Covers registering the worker, its first activation, and the handshake.\nconst SETUP_TIMEOUT = 5000\n\n/** A stream URL this page handed out, and what the worker needs to serve it. */\ntype StreamEntry = {\n handle: Opened\n objectId: string\n sealed: SealedObject | undefined\n size: number\n name: string\n mime: string\n onError: ((message: string) => void) | undefined\n}\n\n/**\n * One `openStreams` call, and how the worker reconnects its SDK. Each call gets\n * its own session, so closing one handle leaves another on the same SDK open.\n */\ntype Opened = { session: string; connection: Connection; appKey?: AppKey }\n\nconst entries = new Map<string, StreamEntry>()\nconst opened = new Set<Opened>()\nlet worker: { clientId: string; scope: string } | undefined\nlet initialization: Promise<boolean> | undefined\nlet initOptions: StreamingOptions = {}\n// The page's controller when setup last failed. Undefined unless it failed.\nlet failedController: ServiceWorker | null | undefined\n\n/**\n * Registers the streaming worker and resolves whether this page can stream.\n * `openStreams` calls it with the defaults, so call it yourself only to pass\n * options, and before the first `openStreams`. Later calls return the first\n * call's answer, except when setup failed or timed out and a different worker\n * has since taken control of the page. Then the next call tries again, with\n * the first call's options.\n */\nexport function enableStreaming(\n options: StreamingOptions = {},\n): Promise<boolean> {\n if (!initialization) {\n initOptions = options\n } else if (failedController === undefined) {\n return initialization\n } else {\n // Only a new controller is worth a retry. Retrying against the same one,\n // or none, would wait out SETUP_TIMEOUT again on every call.\n const controller = navigator.serviceWorker.controller\n if (!controller || controller === failedController) return initialization\n }\n failedController = undefined\n initialization = initialize(initOptions).catch(() => {\n failedController = navigator.serviceWorker.controller\n return false\n })\n return initialization\n}\n\nasync function initialize({\n workerUrl = DEFAULT_WORKER_URL,\n scope,\n type,\n}: StreamingOptions) {\n if (\n typeof window === 'undefined' ||\n !('serviceWorker' in navigator) ||\n !window.isSecureContext\n ) {\n return false\n }\n const { serviceWorker } = navigator\n serviceWorker.addEventListener('message', onWorkerMessage)\n const deadline = AbortSignal.timeout(SETUP_TIMEOUT)\n // A hard reload (Shift+Reload) loads the page without its worker, and an\n // active worker only claims pages when it first activates. Waiting would\n // run out the whole deadline, so fall back at once.\n const existing = await within(serviceWorker.getRegistration(), deadline)\n if (existing?.active && !serviceWorker.controller) return false\n const registration = await within(\n serviceWorker\n .register(workerUrl, { ...(scope && { scope }), ...(type && { type }) })\n .then(() => serviceWorker.ready),\n deadline,\n )\n // On a first visit, the page is controlled once the new worker claims it.\n if (!serviceWorker.controller) {\n await within(\n new Promise((resolve) =>\n serviceWorker.addEventListener('controllerchange', resolve, {\n once: true,\n }),\n ),\n deadline,\n )\n }\n const controller = serviceWorker.controller\n if (!controller) return false\n const request = { type: 'sia-client' } satisfies PageMessage\n const reply = (await requestReply(controller, request, {\n signal: deadline,\n })) as ClientReply\n if (!reply.supported) return false\n worker = { clientId: reply.clientId, scope: registration.scope }\n return true\n}\n\n/** `promise`, or a rejection if `signal` aborts first. */\nfunction within<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {\n return new Promise((resolve, reject) => {\n signal.throwIfAborted()\n signal.addEventListener('abort', () => reject(signal.reason), {\n once: true,\n })\n promise.then(resolve, reject)\n })\n}\n\n/**\n * Previews and downloads for one SDK's objects, from an `Sdk` or a `SharedSdk`.\n *\n * The worker cannot use this page's SDK, so it connects its own with\n * `credentials`: the indexer URL and seed a `SharedSdk` was connected with, or\n * the indexer URL and app metadata an `Sdk`'s `Builder` was made with.\n */\nexport function openStreams<S extends Sdk | SharedSdk>(\n sdk: S,\n credentials: CredentialsFor<S>,\n): Streams {\n const handle = openedFor(sdk, credentials)\n opened.add(handle)\n // Started now, so it has usually finished by the first download click,\n // which cannot wait for it.\n void enableStreaming()\n return {\n async url(object, options) {\n await enableStreaming()\n options.signal?.throwIfAborted()\n const stream = streamUrl(handle, object, options)\n if (stream) return stream\n const blob = await readBlob(\n sdk,\n object,\n options.type,\n options.onProgress ?? (() => {}),\n options.signal ?? new AbortController().signal,\n )\n const url = URL.createObjectURL(blob)\n return { url, blob, release: () => URL.revokeObjectURL(url) }\n },\n async download(object, options) {\n const stream = streamUrl(handle, object, options)\n if (stream) {\n // A hidden frame, so a worker error loads there instead of replacing\n // the page. Nothing signals when the download has started, so it stays.\n const frame = document.createElement('iframe')\n frame.hidden = true\n frame.src = `${stream.url}?download=1`\n document.body.append(frame)\n // Never released here. The URL stays valid until `close()`, so\n // delayed starts and browser retries still work.\n return 'streaming'\n }\n const saved = await saveToDisk(\n sdk,\n object,\n options.name,\n options.type,\n options.onProgress ?? (() => {}),\n )\n return saved ? 'saved' : 'cancelled'\n },\n close() {\n releaseStreams(handle)\n },\n }\n}\n\n/**\n * Checked at runtime too, for callers without types. An `Sdk` is told apart\n * by its `appKey()`, which a `SharedSdk` does not have.\n */\nfunction openedFor(sdk: Sdk | SharedSdk, credentials: unknown): Opened {\n const { indexerUrl, seed, appMeta } = (credentials ?? {}) as {\n indexerUrl?: unknown\n seed?: unknown\n appMeta?: unknown\n }\n const session = crypto.randomUUID()\n if (typeof (sdk as Partial<Sdk>).appKey === 'function') {\n if (typeof indexerUrl !== 'string' || !isAppMetadata(appMeta)) {\n throw new TypeError('openStreams(Sdk) needs { indexerUrl, appMeta }.')\n }\n const appKey = (sdk as Sdk).appKey()\n const connection: Connection = {\n kind: 'app',\n indexerUrl,\n appKey: toHex(appKey.export()),\n appMeta,\n }\n return { session, connection, appKey }\n }\n if (typeof indexerUrl !== 'string' || typeof seed !== 'string') {\n throw new TypeError('openStreams(SharedSdk) needs { indexerUrl, seed }.')\n }\n return { session, connection: { kind: 'shared', indexerUrl, seed } }\n}\n\nfunction toHex(bytes: Uint8Array) {\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n\nfunction streamUrl(\n handle: Opened,\n object: PinnedObject,\n { name, type, onError }: FileOptions,\n): StreamedFile | undefined {\n if (!worker || !navigator.serviceWorker.controller || !opened.has(handle)) {\n return undefined\n }\n const token = crypto.randomUUID()\n entries.set(token, {\n handle,\n objectId: object.id(),\n sealed: handle.appKey && object.seal(handle.appKey),\n size: object.size(),\n name,\n mime: type || 'application/octet-stream',\n onError,\n })\n return {\n url: streamPath(worker.scope, worker.clientId, token),\n release() {\n entries.delete(token)\n const message = { type: 'sia-release', token } satisfies PageMessage\n navigator.serviceWorker.controller?.postMessage(message)\n },\n }\n}\n\nfunction releaseStreams(handle: Opened) {\n for (const [token, entry] of entries) {\n if (entry.handle === handle) entries.delete(token)\n }\n if (!opened.delete(handle)) return\n const message = {\n type: 'sia-release-share',\n session: handle.session,\n } satisfies PageMessage\n navigator.serviceWorker.controller?.postMessage(message)\n}\n\nfunction onWorkerMessage(event: MessageEvent) {\n if (event.source !== navigator.serviceWorker.controller) return\n const data = event.data as WorkerMessage | undefined\n if (data?.type === 'sia-stream-error') {\n const entry = entries.get(data.token)\n if (!entry) return\n const { offset, length } = data\n const range =\n offset !== undefined && length !== undefined\n ? { offset, length }\n : undefined\n if (entry.onError) entry.onError(streamErrorMessage(data.message))\n else reportStreamError(data.message, entry.name, range)\n } else if (data?.type === 'sia-source') {\n const port = event.ports[0]\n if (!port) return\n const entry = entries.get(data.token)\n const reply: SourceReply =\n entry\n ? {\n type: 'source',\n session: entry.handle.session,\n connection: entry.handle.connection,\n objectId: entry.objectId,\n ...(entry.sealed && { sealed: entry.sealed }),\n size: entry.size,\n mime: entry.mime,\n name: entry.name,\n }\n : { type: 'error' }\n port.postMessage(reply)\n port.close()\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/relay.ts","../src/stream/fallback.ts","../src/stream/page.ts"],"sourcesContent":["// Browser entry; lazily initializes WASM on first use.\n\nimport wasmInit from '../wasm/sia_storage_wasm.js'\n\nexport { detectPrivateRelay } from './relay'\nexport { enableStreaming, openStreams } from './stream/page'\nexport type {\n AppCredentials,\n CredentialsFor,\n FileEvents,\n FileOptions,\n SharedCredentials,\n StreamedFile,\n StreamStatus,\n Streams,\n StreamingOptions,\n} from './stream/page'\n\nlet initPromise: Promise<unknown> | null = null\n\n/** Initialize the WASM module. Safe to call multiple times. */\nexport async function initSia(): Promise<void> {\n if (!initPromise) initPromise = wasmInit()\n await initPromise\n}\n\nexport {\n AppKey,\n Builder,\n KeyRecord,\n ObjectEvent,\n PackedUpload,\n PinnedObject,\n Sdk,\n SharedSdk,\n SharingKey,\n encodedSize,\n generateRecoveryPhrase,\n setLogger,\n validateRecoveryPhrase,\n} from '../wasm/sia_storage_wasm.js'\nexport type {\n Account,\n AppMetadata,\n DownloadOptions,\n Host,\n HostQuery,\n KeyStats,\n PinnedSlab,\n Sector,\n SealedObject,\n ShardProgress,\n Slab,\n UploadOptions,\n} from '../wasm/sia_storage_wasm.js'\n","/**\n * Whether the visitor is on iCloud Private Relay, which the SDK does not work\n * through. Show them a way to turn it off in iCloud settings.\n *\n * Asks check-relay.sia.storage, a Sia Foundation service that compares the\n * caller's address with Apple's published list of Private Relay addresses.\n * Only Safari's traffic goes through Private Relay, so other browsers resolve\n * false without a request. The answer is kept for the page's lifetime, and a\n * failure to get one resolves false.\n */\nconst RELAY_CHECK_URL = 'https://check-relay.sia.storage'\nconst TIMEOUT = 5000\n\nlet answer: Promise<boolean> | undefined\n\nexport function detectPrivateRelay(): Promise<boolean> {\n answer ??=\n typeof navigator === 'undefined'\n ? Promise.resolve(false)\n : checkPrivateRelay(navigator.userAgent, fetch)\n return answer\n}\n\n/** `detectPrivateRelay` without the caching, for tests. */\nexport async function checkPrivateRelay(\n userAgent: string,\n request: typeof fetch,\n): Promise<boolean> {\n const safari =\n /Version\\/\\S+.*Safari\\//.test(userAgent) &&\n !/Chrome|Chromium|CriOS|FxiOS|Edg|OPR|OPiOS|Ddg\\/|Android/.test(userAgent)\n if (!safari) return false\n try {\n const response = await request(RELAY_CHECK_URL, {\n signal: AbortSignal.timeout(TIMEOUT),\n })\n const body = (await response.json()) as { relay?: unknown }\n return body.relay === true\n } catch {\n return false\n }\n}\n","/**\n * Previews and downloads for pages that cannot stream through the service\n * worker, where the page reads the object itself. A preview holds the whole\n * file in memory as a Blob. A download writes straight to disk through the\n * save picker where the browser has one, and buffers a Blob where it does not.\n */\nimport type {\n PinnedObject,\n Sdk,\n SharedSdk,\n ShardProgress,\n} from '../../wasm/sia_storage_wasm.js'\nimport type { StreamStatus } from './protocol'\n\n/** What a file reports while it downloads, on the worker or in the page. */\nexport type FileEvents = {\n /**\n * Total bytes this URL has received. It is a transfer count, not how much of\n * the file is held: a range read again after a seek counts again, so it can\n * pass the file size, and a new URL for the same file starts from zero. For\n * how much of a video is buffered, read the media element's `buffered`.\n */\n onProgress?: (bytesDownloaded: number) => void\n /**\n * One call per piece the SDK read from a host. A download reads each slab in\n * chunks, so the same slab and shard come again for every chunk, and only\n * the first `minShards` hosts to answer for a chunk are reported.\n */\n onShard?: (progress: ShardProgress) => void\n /** Each change of what the URL is doing. See `StreamStatus`. */\n onStatus?: (status: StreamStatus) => void\n}\n\ntype AnySdk = Pick<Sdk | SharedSdk, 'download'>\n\ntype Chunk = Uint8Array<ArrayBuffer>\n\n// The File System Access save picker, in Chromium only and not yet in\n// TypeScript's DOM types.\ntype SavePicker = (options: {\n suggestedName: string\n}) => Promise<FileSystemFileHandle & { createWritable(): Promise<WritableStream<Chunk>> }>\n\nfunction chunks(stream: ReadableStream): ReadableStreamDefaultReader<Chunk> {\n return stream.getReader() as ReadableStreamDefaultReader<Chunk>\n}\n\n/**\n * Calls an app's callback. One that throws is reported to the console rather\n * than ending the download or skipping the callbacks after it.\n */\nexport function notify<T>(callback: ((value: T) => void) | undefined, value: T) {\n if (!callback) return\n try {\n callback(value)\n } catch (error) {\n reportError(error)\n }\n}\n\n// Asks the SDK for shard reports only when someone listens for them.\nfunction download(sdk: AnySdk, object: PinnedObject, events: FileEvents) {\n const { onShard } = events\n return sdk.download(\n object,\n onShard\n ? { onShardDownloaded: (shard: ShardProgress) => notify(onShard, shard) }\n : {},\n )\n}\n\n// The status cycle the worker reports for a stream URL, for a read in the page.\nfunction track({ onProgress, onStatus }: FileEvents) {\n let received = 0\n let started = false\n notify(onStatus, 'connecting')\n return {\n chunk(length: number) {\n if (!started) notify(onStatus, 'downloading')\n started = true\n received += length\n notify(onProgress, received)\n },\n end() {\n notify(onStatus, 'idle')\n },\n }\n}\n\n/** The whole object as a Blob, stopping early when `signal` aborts. */\nexport async function readBlob(\n sdk: AnySdk,\n object: PinnedObject,\n type: string | undefined,\n events: FileEvents,\n signal: AbortSignal,\n) {\n const progress = track(events)\n let reader: ReadableStreamDefaultReader<Chunk> | undefined\n const onAbort = () => void reader?.cancel()\n signal.addEventListener('abort', onAbort)\n const parts: Chunk[] = []\n try {\n // Inside the try, so a download that fails to start still reports idle.\n reader = chunks(download(sdk, object, events))\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n parts.push(value)\n progress.chunk(value.length)\n }\n } finally {\n signal.removeEventListener('abort', onAbort)\n progress.end()\n }\n signal.throwIfAborted()\n return new Blob(parts, type ? { type } : {})\n}\n\nexport function saveBlob(blob: Blob, name: string) {\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = name\n a.click()\n // Some browsers cancel the download if the URL goes away in the same task.\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/** False if the visitor closes the save picker. */\nexport async function saveToDisk(\n sdk: AnySdk,\n object: PinnedObject,\n name: string,\n type: string | undefined,\n events: FileEvents,\n) {\n const picker = (window as { showSaveFilePicker?: SavePicker })\n .showSaveFilePicker\n if (!picker) {\n const blob = await readBlob(\n sdk,\n object,\n type,\n events,\n new AbortController().signal,\n )\n saveBlob(blob, name)\n return true\n }\n\n // The picker must open inside the click that asked for it, so nothing is\n // awaited before this call.\n let handle: Awaited<ReturnType<SavePicker>>\n try {\n handle = await picker.call(window, { suggestedName: name })\n } catch (e) {\n if (e instanceof DOMException && e.name === 'AbortError') return false\n throw e\n }\n const file = await handle.createWritable()\n const progress = track(events)\n const counter = new TransformStream<Chunk, Chunk>({\n transform(chunk, controller) {\n progress.chunk(chunk.length)\n controller.enqueue(chunk)\n },\n })\n try {\n await (download(sdk, object, events) as ReadableStream<Chunk>)\n .pipeThrough(counter)\n .pipeTo(file)\n } finally {\n progress.end()\n }\n return true\n}\n","/**\n * The page side of streaming.\n *\n * `openStreams` hands out same-origin URLs that the library's service worker\n * answers with ranged reads of an object, so a `<video>` seeks without loading\n * the whole file and a download goes to the browser's download manager instead\n * of the page's memory. The worker connects its own SDK of the same kind, with\n * credentials this page hands it over a message port and it keeps in memory.\n * Where the worker is unavailable, the same calls read the object in the page.\n */\nimport type {\n AppKey,\n AppMetadata,\n PinnedObject,\n SealedObject,\n Sdk,\n SharedSdk,\n} from '../../wasm/sia_storage_wasm.js'\nimport { reportStreamError, streamErrorMessage } from './errors'\nimport { type FileEvents, notify, readBlob, saveToDisk } from './fallback'\nimport {\n type ClientReply,\n type Connection,\n isAppMetadata,\n type PageMessage,\n requestReply,\n type SourceReply,\n type StreamEvent,\n streamPath,\n type WorkerMessage,\n} from './protocol'\n\nexport type { FileEvents } from './fallback'\nexport type { StreamStatus } from './protocol'\n\nexport type StreamingOptions = {\n /** Where the site serves the worker file. Defaults to `/sia-storage-sw.js`. */\n workerUrl?: string\n /** The worker's scope. Defaults to the directory `workerUrl` is in. */\n scope?: string\n /** `'module'` when `workerUrl` is an ES module worker of your own. */\n type?: WorkerType\n}\n\n/** What a `SharedSdk` was connected with. */\nexport type SharedCredentials = { indexerUrl: string; seed: string }\n\n/** What an `Sdk`'s `Builder` was made with. The key comes from `sdk.appKey()`. */\nexport type AppCredentials = { indexerUrl: string; appMeta: AppMetadata }\n\n/** The credentials `openStreams` takes for each kind of SDK. */\nexport type CredentialsFor<S extends Sdk | SharedSdk> = S extends SharedSdk\n ? SharedCredentials\n : AppCredentials\n\nexport type FileOptions = FileEvents & {\n /** Used for downloads and for the browser's media handling. */\n name: string\n /** The MIME type. Defaults to `application/octet-stream`. */\n type?: string\n /** A failure after streaming started. Without it, failures are logged. */\n onError?: (message: string) => void\n}\n\n/** A URL for an `<img>`, `<video>`, `<audio>`, `<iframe>` or `fetch`. */\nexport type StreamedFile = {\n url: string\n /** The whole file, when the page read it itself instead of streaming. */\n blob?: Blob\n /** Frees the URL. Call it when the element showing it goes away. */\n release(): void\n}\n\nexport type Streams = {\n /** A URL serving the object. Streams where it can, else reads it whole. */\n url(\n object: PinnedObject,\n options: FileOptions & { signal?: AbortSignal },\n ): Promise<StreamedFile>\n /**\n * Saves the object, from a click handler. Streams to the browser's download\n * manager where it can, else opens the save picker, else saves a Blob.\n * Awaits nothing before the picker opens, so the browser still counts it as\n * part of the click.\n */\n download(\n object: PinnedObject,\n options: FileOptions,\n ): Promise<'streaming' | 'saved' | 'cancelled'>\n /**\n * Cancels the streams this handle made. Other handles on the same SDK keep\n * theirs. Call it when you stop using the handle.\n */\n close(): void\n}\n\nconst DEFAULT_WORKER_URL = '/sia-storage-sw.js'\n// Covers registering the worker, its first activation, and the handshake.\nconst SETUP_TIMEOUT = 5000\n\n/** A stream URL this page handed out, and what the worker needs to serve it. */\ntype StreamEntry = {\n handle: Opened\n objectId: string\n sealed: SealedObject | undefined\n size: number\n name: string\n mime: string\n onError: ((message: string) => void) | undefined\n events: FileEvents\n // Bytes reported for this URL so far, across all of its range requests.\n received: number\n}\n\n/**\n * One `openStreams` call, and how the worker reconnects its SDK. Each call gets\n * its own session, so closing one handle leaves another on the same SDK open.\n */\ntype Opened = { session: string; connection: Connection; appKey?: AppKey }\n\nconst entries = new Map<string, StreamEntry>()\nconst opened = new Set<Opened>()\nlet worker: { clientId: string; scope: string } | undefined\nlet initialization: Promise<boolean> | undefined\nlet initOptions: StreamingOptions = {}\n// The page's controller when setup last failed. Undefined unless it failed.\nlet failedController: ServiceWorker | null | undefined\n\n/**\n * Registers the streaming worker and resolves whether this page can stream.\n * `openStreams` calls it with the defaults, so call it yourself only to pass\n * options, and before the first `openStreams`. Later calls return the first\n * call's answer, except when setup failed or timed out and a different worker\n * has since taken control of the page. Then the next call tries again, with\n * the first call's options.\n */\nexport function enableStreaming(\n options: StreamingOptions = {},\n): Promise<boolean> {\n if (!initialization) {\n initOptions = options\n } else if (failedController === undefined) {\n return initialization\n } else {\n // Only a new controller is worth a retry. Retrying against the same one,\n // or none, would wait out SETUP_TIMEOUT again on every call.\n const controller = navigator.serviceWorker.controller\n if (!controller || controller === failedController) return initialization\n }\n failedController = undefined\n initialization = initialize(initOptions).catch(() => {\n failedController = navigator.serviceWorker.controller\n return false\n })\n return initialization\n}\n\nasync function initialize({\n workerUrl = DEFAULT_WORKER_URL,\n scope,\n type,\n}: StreamingOptions) {\n if (\n typeof window === 'undefined' ||\n !('serviceWorker' in navigator) ||\n !window.isSecureContext\n ) {\n return false\n }\n const { serviceWorker } = navigator\n serviceWorker.addEventListener('message', onWorkerMessage)\n const deadline = AbortSignal.timeout(SETUP_TIMEOUT)\n // A hard reload (Shift+Reload) loads the page without its worker, and an\n // active worker only claims pages when it first activates. Waiting would\n // run out the whole deadline, so fall back at once.\n const existing = await within(serviceWorker.getRegistration(), deadline)\n if (existing?.active && !serviceWorker.controller) return false\n const registration = await within(\n serviceWorker\n .register(workerUrl, { ...(scope && { scope }), ...(type && { type }) })\n .then(() => serviceWorker.ready),\n deadline,\n )\n // On a first visit, the page is controlled once the new worker claims it.\n if (!serviceWorker.controller) {\n await within(\n new Promise((resolve) =>\n serviceWorker.addEventListener('controllerchange', resolve, {\n once: true,\n }),\n ),\n deadline,\n )\n }\n const controller = serviceWorker.controller\n if (!controller) return false\n const request = { type: 'sia-client' } satisfies PageMessage\n const reply = (await requestReply(controller, request, {\n signal: deadline,\n })) as ClientReply\n if (!reply.supported) return false\n worker = { clientId: reply.clientId, scope: registration.scope }\n return true\n}\n\n/** `promise`, or a rejection if `signal` aborts first. */\nfunction within<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {\n return new Promise((resolve, reject) => {\n signal.throwIfAborted()\n signal.addEventListener('abort', () => reject(signal.reason), {\n once: true,\n })\n promise.then(resolve, reject)\n })\n}\n\n/**\n * Previews and downloads for one SDK's objects, from an `Sdk` or a `SharedSdk`.\n *\n * The worker cannot use this page's SDK, so it connects its own with\n * `credentials`: the indexer URL and seed a `SharedSdk` was connected with, or\n * the indexer URL and app metadata an `Sdk`'s `Builder` was made with.\n */\nexport function openStreams<S extends Sdk | SharedSdk>(\n sdk: S,\n credentials: CredentialsFor<S>,\n): Streams {\n const handle = openedFor(sdk, credentials)\n opened.add(handle)\n // Started now, so it has usually finished by the first download click,\n // which cannot wait for it.\n void enableStreaming()\n return {\n async url(object, options) {\n await enableStreaming()\n options.signal?.throwIfAborted()\n const stream = streamUrl(handle, object, options)\n if (stream) return stream\n const blob = await readBlob(\n sdk,\n object,\n options.type,\n options,\n options.signal ?? new AbortController().signal,\n )\n const url = URL.createObjectURL(blob)\n return { url, blob, release: () => URL.revokeObjectURL(url) }\n },\n async download(object, options) {\n const stream = streamUrl(handle, object, options)\n if (stream) {\n // A hidden frame, so a worker error loads there instead of replacing\n // the page. Nothing signals when the download has started, so it stays.\n const frame = document.createElement('iframe')\n frame.hidden = true\n frame.src = `${stream.url}?download=1`\n document.body.append(frame)\n // Never released here. The URL stays valid until `close()`, so\n // delayed starts and browser retries still work.\n return 'streaming'\n }\n const saved = await saveToDisk(\n sdk,\n object,\n options.name,\n options.type,\n options,\n )\n return saved ? 'saved' : 'cancelled'\n },\n close() {\n releaseStreams(handle)\n },\n }\n}\n\n/**\n * Checked at runtime too, for callers without types. An `Sdk` is told apart\n * by its `appKey()`, which a `SharedSdk` does not have.\n */\nfunction openedFor(sdk: Sdk | SharedSdk, credentials: unknown): Opened {\n const { indexerUrl, seed, appMeta } = (credentials ?? {}) as {\n indexerUrl?: unknown\n seed?: unknown\n appMeta?: unknown\n }\n const session = crypto.randomUUID()\n if (typeof (sdk as Partial<Sdk>).appKey === 'function') {\n if (typeof indexerUrl !== 'string' || !isAppMetadata(appMeta)) {\n throw new TypeError('openStreams(Sdk) needs { indexerUrl, appMeta }.')\n }\n const appKey = (sdk as Sdk).appKey()\n const connection: Connection = {\n kind: 'app',\n indexerUrl,\n appKey: toHex(appKey.export()),\n appMeta,\n }\n return { session, connection, appKey }\n }\n if (typeof indexerUrl !== 'string' || typeof seed !== 'string') {\n throw new TypeError('openStreams(SharedSdk) needs { indexerUrl, seed }.')\n }\n return { session, connection: { kind: 'shared', indexerUrl, seed } }\n}\n\nfunction toHex(bytes: Uint8Array) {\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n\nfunction streamUrl(\n handle: Opened,\n object: PinnedObject,\n { name, type, onError, onProgress, onShard, onStatus }: FileOptions,\n): StreamedFile | undefined {\n if (!worker || !navigator.serviceWorker.controller || !opened.has(handle)) {\n return undefined\n }\n const token = crypto.randomUUID()\n entries.set(token, {\n handle,\n objectId: object.id(),\n sealed: handle.appKey && object.seal(handle.appKey),\n size: object.size(),\n name,\n mime: type || 'application/octet-stream',\n onError,\n events: { onProgress, onShard, onStatus },\n received: 0,\n })\n return {\n url: streamPath(worker.scope, worker.clientId, token),\n release() {\n entries.delete(token)\n const message = { type: 'sia-release', token } satisfies PageMessage\n navigator.serviceWorker.controller?.postMessage(message)\n },\n }\n}\n\nfunction releaseStreams(handle: Opened) {\n for (const [token, entry] of entries) {\n if (entry.handle === handle) entries.delete(token)\n }\n if (!opened.delete(handle)) return\n const message = {\n type: 'sia-release-share',\n session: handle.session,\n } satisfies PageMessage\n navigator.serviceWorker.controller?.postMessage(message)\n}\n\nfunction onWorkerMessage(event: MessageEvent) {\n if (event.source !== navigator.serviceWorker.controller) return\n const data = event.data as WorkerMessage | undefined\n if (data?.type === 'sia-stream-error') {\n const entry = entries.get(data.token)\n if (!entry) return\n const { offset, length } = data\n const range =\n offset !== undefined && length !== undefined\n ? { offset, length }\n : undefined\n if (entry.onError) entry.onError(streamErrorMessage(data.message))\n else reportStreamError(data.message, entry.name, range)\n } else if (data?.type === 'sia-stream-status') {\n notify(entries.get(data.token)?.events.onStatus, data.status)\n } else if (data?.type === 'sia-stream-progress') {\n const entry = entries.get(data.token)\n // The worker can come from a different deploy, so its fields are checked.\n if (!entry || !Array.isArray(data.shards)) return\n const { onProgress, onShard } = entry.events\n for (const shard of data.shards) notify(onShard, shard)\n if (Number.isSafeInteger(data.bytes) && data.bytes > 0) {\n entry.received += data.bytes\n notify(onProgress, entry.received)\n }\n } else if (data?.type === 'sia-source') {\n const port = event.ports[0]\n if (!port) return\n const entry = entries.get(data.token)\n const events: StreamEvent[] = []\n if (entry?.events.onStatus) events.push('status')\n if (entry?.events.onProgress) events.push('progress')\n if (entry?.events.onShard) events.push('shards')\n const reply: SourceReply =\n entry\n ? {\n type: 'source',\n session: entry.handle.session,\n connection: entry.handle.connection,\n objectId: entry.objectId,\n ...(entry.sealed && { sealed: entry.sealed }),\n size: entry.size,\n mime: entry.mime,\n name: entry.name,\n ...(events.length > 0 && { events }),\n }\n : { type: 'error' }\n port.postMessage(reply)\n port.close()\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.node.cjs
CHANGED
|
@@ -40,32 +40,51 @@ __export(index_node_exports, {
|
|
|
40
40
|
});
|
|
41
41
|
module.exports = __toCommonJS(index_node_exports);
|
|
42
42
|
|
|
43
|
+
// src/node/platforms.ts
|
|
44
|
+
var NATIVE_PACKAGES = {
|
|
45
|
+
"darwin-arm64": "@siafoundation/sia-storage-darwin-arm64",
|
|
46
|
+
"darwin-x64": "@siafoundation/sia-storage-darwin-x64",
|
|
47
|
+
"linux-x64": "@siafoundation/sia-storage-linux-x64-gnu",
|
|
48
|
+
"linux-arm64": "@siafoundation/sia-storage-linux-arm64-gnu",
|
|
49
|
+
"win32-x64": "@siafoundation/sia-storage-win32-x64-msvc"
|
|
50
|
+
};
|
|
51
|
+
|
|
43
52
|
// src/node/load.ts
|
|
44
53
|
var addon = null;
|
|
45
54
|
function loadNativeAddon() {
|
|
46
55
|
if (addon) return addon;
|
|
47
|
-
const
|
|
48
|
-
|
|
56
|
+
const target = `${process.platform}-${process.arch}`;
|
|
57
|
+
let cause;
|
|
49
58
|
try {
|
|
50
|
-
if (
|
|
59
|
+
if (target === "darwin-arm64") {
|
|
51
60
|
addon = require("@siafoundation/sia-storage-darwin-arm64");
|
|
52
|
-
} else if (
|
|
61
|
+
} else if (target === "darwin-x64") {
|
|
53
62
|
addon = require("@siafoundation/sia-storage-darwin-x64");
|
|
54
|
-
} else if (
|
|
63
|
+
} else if (target === "linux-x64") {
|
|
55
64
|
addon = require("@siafoundation/sia-storage-linux-x64-gnu");
|
|
56
|
-
} else if (
|
|
65
|
+
} else if (target === "linux-arm64") {
|
|
57
66
|
addon = require("@siafoundation/sia-storage-linux-arm64-gnu");
|
|
58
|
-
} else if (
|
|
67
|
+
} else if (target === "win32-x64") {
|
|
59
68
|
addon = require("@siafoundation/sia-storage-win32-x64-msvc");
|
|
60
69
|
}
|
|
61
|
-
} catch {
|
|
70
|
+
} catch (error) {
|
|
71
|
+
cause = error;
|
|
62
72
|
}
|
|
63
|
-
if (!addon)
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
if (!addon) throw addonLoadError(target, cause);
|
|
74
|
+
return addon;
|
|
75
|
+
}
|
|
76
|
+
function addonLoadError(target, cause) {
|
|
77
|
+
const packageName = NATIVE_PACKAGES[target];
|
|
78
|
+
if (!packageName) {
|
|
79
|
+
return new Error(
|
|
80
|
+
`@siafoundation/sia-storage: no native addon is published for ${target}. Use the browser/WASM build.`
|
|
66
81
|
);
|
|
67
82
|
}
|
|
68
|
-
|
|
83
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
84
|
+
return new Error(
|
|
85
|
+
`@siafoundation/sia-storage: could not load the native addon for ${target} from ${packageName}: ${reason}`,
|
|
86
|
+
{ cause }
|
|
87
|
+
);
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
// src/node/napi.ts
|
package/dist/index.node.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.node.ts","../src/node/load.ts","../src/node/napi.ts"],"sourcesContent":["export * from './node/napi'\nexport type * from './node/napi'\n\n/** Node's traffic never goes through iCloud Private Relay, which is Safari's. */\nexport async function detectPrivateRelay(): Promise<boolean> {\n return false\n}\n\n// openStreams hands a browser page URLs that a service worker serves. Node\n// streams objects with sdk.download() directly, so these only let code shared\n// with the browser import them, and server rendering call them, without\n// crashing.\nexport async function enableStreaming(): Promise<boolean> {\n return false\n}\n\nexport function openStreams() {\n const unavailable = () =>\n Promise.reject(\n new Error('openStreams is for browser pages. In Node, use sdk.download().'),\n )\n return { url: unavailable, download: unavailable, close() {} }\n}\n","import type * as Addon from './napi.generated'\n\nlet addon: typeof Addon | null = null\n\nexport function loadNativeAddon(): typeof Addon {\n if (addon) return addon\n\n const
|
|
1
|
+
{"version":3,"sources":["../src/index.node.ts","../src/node/platforms.ts","../src/node/load.ts","../src/node/napi.ts"],"sourcesContent":["export * from './node/napi'\nexport type * from './node/napi'\n\n/** Node's traffic never goes through iCloud Private Relay, which is Safari's. */\nexport async function detectPrivateRelay(): Promise<boolean> {\n return false\n}\n\n// openStreams hands a browser page URLs that a service worker serves. Node\n// streams objects with sdk.download() directly, so these only let code shared\n// with the browser import them, and server rendering call them, without\n// crashing.\nexport async function enableStreaming(): Promise<boolean> {\n return false\n}\n\nexport function openStreams() {\n const unavailable = () =>\n Promise.reject(\n new Error('openStreams is for browser pages. In Node, use sdk.download().'),\n )\n return { url: unavailable, download: unavailable, close() {} }\n}\n","/**\n * The npm package that ships the native addon for each platform, keyed by\n * `${process.platform}-${process.arch}`. The Linux and Windows packages name\n * the C library or toolchain their binary was built against, so a package\n * name is not always the platform key with a prefix.\n *\n * scripts/publish.ts publishes these and lists them as the main package's\n * optionalDependencies.\n */\nexport const NATIVE_PACKAGES: Record<string, string> = {\n 'darwin-arm64': '@siafoundation/sia-storage-darwin-arm64',\n 'darwin-x64': '@siafoundation/sia-storage-darwin-x64',\n 'linux-x64': '@siafoundation/sia-storage-linux-x64-gnu',\n 'linux-arm64': '@siafoundation/sia-storage-linux-arm64-gnu',\n 'win32-x64': '@siafoundation/sia-storage-win32-x64-msvc',\n}\n","import type * as Addon from './napi.generated'\nimport { NATIVE_PACKAGES } from './platforms'\n\nlet addon: typeof Addon | null = null\n\nexport function loadNativeAddon(): typeof Addon {\n if (addon) return addon\n\n const target = `${process.platform}-${process.arch}`\n let cause: unknown\n\n // One literal require per package, rather than require(NATIVE_PACKAGES[...]),\n // so bundlers, including `bun build --compile`, can find and embed the addon.\n try {\n if (target === 'darwin-arm64') {\n addon = require('@siafoundation/sia-storage-darwin-arm64')\n } else if (target === 'darwin-x64') {\n addon = require('@siafoundation/sia-storage-darwin-x64')\n } else if (target === 'linux-x64') {\n addon = require('@siafoundation/sia-storage-linux-x64-gnu')\n } else if (target === 'linux-arm64') {\n addon = require('@siafoundation/sia-storage-linux-arm64-gnu')\n } else if (target === 'win32-x64') {\n addon = require('@siafoundation/sia-storage-win32-x64-msvc')\n }\n } catch (error) {\n cause = error\n }\n\n if (!addon) throw addonLoadError(target, cause)\n return addon\n}\n\n/**\n * Why the addon could not load. A missing package and a binary that fails to\n * load read differently, so the require error is kept as the cause and its\n * message is part of this one.\n */\nexport function addonLoadError(target: string, cause: unknown): Error {\n const packageName = NATIVE_PACKAGES[target]\n if (!packageName) {\n return new Error(\n `@siafoundation/sia-storage: no native addon is published for ${target}. ` +\n 'Use the browser/WASM build.',\n )\n }\n const reason = cause instanceof Error ? cause.message : String(cause)\n return new Error(\n `@siafoundation/sia-storage: could not load the native addon for ${target} ` +\n `from ${packageName}: ${reason}`,\n { cause },\n )\n}\n","import { loadNativeAddon } from './load'\n\nconst addon = loadNativeAddon()\n\nexport const {\n AppKey,\n Builder,\n Sdk,\n PinnedObject,\n PackedUpload,\n ObjectEvent,\n SharingKey,\n KeyRecord,\n SharedSdk,\n generateRecoveryPhrase,\n validateRecoveryPhrase,\n setLogger,\n encodedSize,\n} = addon\n\nexport type * from './napi.generated'\n\n/** Force-load the native addon to surface missing-binary errors at startup. */\nexport async function initSia(): Promise<void> {\n loadNativeAddon()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSO,IAAM,kBAA0C;AAAA,EACrD,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,aAAa;AAAA,EACb,eAAe;AAAA,EACf,aAAa;AACf;;;ACZA,IAAI,QAA6B;AAE1B,SAAS,kBAAgC;AAC9C,MAAI,MAAO,QAAO;AAElB,QAAM,SAAS,GAAG,QAAQ,QAAQ,IAAI,QAAQ,IAAI;AAClD,MAAI;AAIJ,MAAI;AACF,QAAI,WAAW,gBAAgB;AAC7B,cAAQ,QAAQ,yCAAyC;AAAA,IAC3D,WAAW,WAAW,cAAc;AAClC,cAAQ,QAAQ,uCAAuC;AAAA,IACzD,WAAW,WAAW,aAAa;AACjC,cAAQ,QAAQ,0CAA0C;AAAA,IAC5D,WAAW,WAAW,eAAe;AACnC,cAAQ,QAAQ,4CAA4C;AAAA,IAC9D,WAAW,WAAW,aAAa;AACjC,cAAQ,QAAQ,2CAA2C;AAAA,IAC7D;AAAA,EACF,SAAS,OAAO;AACd,YAAQ;AAAA,EACV;AAEA,MAAI,CAAC,MAAO,OAAM,eAAe,QAAQ,KAAK;AAC9C,SAAO;AACT;AAOO,SAAS,eAAe,QAAgB,OAAuB;AACpE,QAAM,cAAc,gBAAgB,MAAM;AAC1C,MAAI,CAAC,aAAa;AAChB,WAAO,IAAI;AAAA,MACT,gEAAgE,MAAM;AAAA,IAExE;AAAA,EACF;AACA,QAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,SAAO,IAAI;AAAA,IACT,mEAAmE,MAAM,SAC/D,WAAW,KAAK,MAAM;AAAA,IAChC,EAAE,MAAM;AAAA,EACV;AACF;;;AClDA,IAAMA,SAAQ,gBAAgB;AAEvB,IAAM;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,IAAIA;AAKJ,eAAsB,UAAyB;AAC7C,kBAAgB;AAClB;;;AHrBA,eAAsB,qBAAuC;AAC3D,SAAO;AACT;AAMA,eAAsB,kBAAoC;AACxD,SAAO;AACT;AAEO,SAAS,cAAc;AAC5B,QAAM,cAAc,MAClB,QAAQ;AAAA,IACN,IAAI,MAAM,gEAAgE;AAAA,EAC5E;AACF,SAAO,EAAE,KAAK,aAAa,UAAU,aAAa,QAAQ;AAAA,EAAC,EAAE;AAC/D;","names":["addon"]}
|
package/dist/index.node.d.ts
CHANGED
|
@@ -485,9 +485,21 @@ export declare function initSia(): Promise<void>
|
|
|
485
485
|
export declare function detectPrivateRelay(): Promise<boolean>
|
|
486
486
|
/** Always false. Service worker streaming is for browser pages. */
|
|
487
487
|
export declare function enableStreaming(options?: { workerUrl?: string; scope?: string; type?: 'classic' | 'module' }): Promise<boolean>
|
|
488
|
+
export type StreamStatus = 'connecting' | 'downloading' | 'idle'
|
|
489
|
+
/** The same callbacks browser code passes, so code shared with the browser typechecks. */
|
|
490
|
+
export type FileEvents = {
|
|
491
|
+
onProgress?: (bytesDownloaded: number) => void
|
|
492
|
+
onShard?: (progress: ShardProgress) => void
|
|
493
|
+
onStatus?: (status: StreamStatus) => void
|
|
494
|
+
}
|
|
495
|
+
export type FileOptions = FileEvents & {
|
|
496
|
+
name: string
|
|
497
|
+
type?: string
|
|
498
|
+
onError?: (message: string) => void
|
|
499
|
+
}
|
|
488
500
|
/** For browser pages. In Node, `url` and `download` reject, and `sdk.download()` streams objects directly. */
|
|
489
501
|
export declare function openStreams(sdk: object, credentials: { indexerUrl: string; appMeta: AppMetadata } | { indexerUrl: string; seed: string }): {
|
|
490
|
-
url(object: PinnedObject, options:
|
|
491
|
-
download(object: PinnedObject, options:
|
|
502
|
+
url(object: PinnedObject, options: FileOptions & { signal?: AbortSignal }): Promise<never>
|
|
503
|
+
download(object: PinnedObject, options: FileOptions): Promise<never>
|
|
492
504
|
close(): void
|
|
493
505
|
}
|