@siafoundation/sia-storage 0.1.3 → 0.1.5
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 +22 -4
- package/dist/{chunk-QICJ5CIY.js → chunk-WOQD3MPG.js} +5 -2
- package/dist/chunk-WOQD3MPG.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +67 -9
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +26 -13
- package/dist/next.js +1 -1
- package/dist/sia-storage-sw.js +2 -2
- package/dist/stream-worker.js +55 -12
- package/dist/stream-worker.js.map +1 -1
- package/package.json +8 -7
- package/wasm/sia_storage_wasm.d.ts +67 -44
- package/wasm/sia_storage_wasm.js +159 -58
- package/wasm/sia_storage_wasm_bg.wasm +0 -0
- package/wasm/sia_storage_wasm_bg.wasm.d.ts +16 -10
- package/dist/chunk-QICJ5CIY.js.map +0 -1
package/README.md
CHANGED
|
@@ -116,6 +116,16 @@ const video = await streams.url(object, {
|
|
|
116
116
|
|
|
117
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
118
|
|
|
119
|
+
### Start sooner
|
|
120
|
+
|
|
121
|
+
A stream connects to the hosts that hold the file when the video asks for its first bytes, which takes a few seconds. `warm()` does it ahead of time, by reading one byte, which asks each host of the file's first part for a single 64-byte segment. Call it when a play is likely, such as when the pointer reaches a Play button. It reports no status or progress and never rejects.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const video = await streams.url(object, { name: 'clip.mp4', type: 'video/mp4' })
|
|
125
|
+
videoElement.src = video.url
|
|
126
|
+
playButton.onpointerenter = () => void video.warm()
|
|
127
|
+
```
|
|
128
|
+
|
|
119
129
|
### Serve the worker
|
|
120
130
|
|
|
121
131
|
The worker is one file, `sia-storage-sw.js`, served from your site's root. Use the line for your setup.
|
|
@@ -151,7 +161,7 @@ export const dynamic = 'force-static'
|
|
|
151
161
|
- **A site with its own service worker** can't add a second one, since a page has only one. Call `installStreams(self)` from `@siafoundation/sia-storage/stream-worker` inside yours, and point `enableStreaming` at it: `enableStreaming({ workerUrl: '/sw.js', type: 'module' })`.
|
|
152
162
|
- **A Content Security Policy** needs `worker-src 'self'`.
|
|
153
163
|
- **The credentials stay in memory.** The worker connects its own SDK, so it needs what yours was made with: a `SharedSdk`'s seed, or an `Sdk`'s app key, which it takes from `sdk.appKey()`. The page hands them over a message port, and the worker never writes them anywhere.
|
|
154
|
-
- **Keep the tab open** until a download finishes. If the browser restarts the worker, it gets the credentials back from the page.
|
|
164
|
+
- **Keep the tab open** until a download finishes. The page messages the worker every 10 seconds while it streams, since Firefox otherwise stops the worker 30 seconds in and saves the partial file as complete. If the browser restarts the worker, it gets the credentials back from the page.
|
|
155
165
|
- **The first download click** within a few seconds of `openStreams`, before the worker is ready, uses the save picker or an in-memory save instead.
|
|
156
166
|
|
|
157
167
|
## iCloud Private Relay
|
|
@@ -208,6 +218,7 @@ Near-identical surfaces. Real differences:
|
|
|
208
218
|
- App identifier: `appMeta.appId: string (hex)` on browser, `appMeta.id: Buffer(32)` on Node.
|
|
209
219
|
- Numeric sizes are `number` on browser, `bigint` on Node. Byte arrays are `Uint8Array` on browser, `Buffer` on Node (`Buffer` is a `Uint8Array` subclass; both accept either as input).
|
|
210
220
|
- Sharing key seeds are hex `string` on browser, `Buffer` on Node. This covers `SharingKey.seed()`, `SharingKey.fromSeed(seed)`, and `SharedSdk.connect(indexerUrl, seed)`.
|
|
221
|
+
- `sharedSdk.objectSummaries(offset, limit)` resolves `ObjectSummary` class instances on browser, which own WASM memory and are disposable like `PinnedObject`. On Node it resolves plain objects.
|
|
211
222
|
- `sdk.unshareObject(key, object)` takes a `PinnedObject` on browser and an object id `string` on Node.
|
|
212
223
|
- `sdk.hosts(query?)` and `sharedSdk.hosts(query?)` accept a `HostQuery` on browser; on Node they take no arguments.
|
|
213
224
|
- `openStreams` gives a browser page URLs that a service worker serves. Node reads objects with `sdk.download(object, { offset, length })`, which already streams a range, so it needs no worker. On Node, `enableStreaming()` resolves false, and `url` and `download` on an `openStreams` handle reject.
|
|
@@ -239,7 +250,7 @@ Returned from `Builder.register()`, `Builder.connected()`, or `Builder.connectPr
|
|
|
239
250
|
| `updateObjectMetadata(object)` | Push local metadata changes to the indexer. |
|
|
240
251
|
| `objectShareUrl(object, validUntil)` / `objectFromShareUrl(url)` | Create / consume share URLs. |
|
|
241
252
|
| `objectEvents(cursor?, limit)` | Paginated change feed. |
|
|
242
|
-
| `hosts(query?)` / `
|
|
253
|
+
| `hosts(query?)` / `account()` / `pruneSlabs(before?)` | Indexer reads. `pruneSlabs` unpins slabs no object uses; `before` limits it to slabs pinned before that date, and omitting it leaves the cutoff to the indexer. |
|
|
243
254
|
| `createSharingKey(description, expiresAt?)` | Create a `SharingKey` for read-only sharing. |
|
|
244
255
|
| `sharingKeys(offset, limit)` / `sharingKey(key)` | List keys / fetch one key's `KeyRecord`. |
|
|
245
256
|
| `shareObject(key, object)` / `unshareObject(key, object)` | Attach / detach an object on a sharing key. |
|
|
@@ -300,20 +311,27 @@ sharing key's seed. Downloads are paid for by the key's owner.
|
|
|
300
311
|
|---|---|
|
|
301
312
|
| `stats()` | The key's `KeyStats` snapshot. |
|
|
302
313
|
| `object(id)` / `objects(offset, limit)` | Read the objects the key grants access to. |
|
|
314
|
+
| `objectSummaries(offset, limit)` | Lists the key's objects without their slabs, which is much smaller and faster than `objects`. Summaries describe an object but cannot be downloaded. Fetch the full object with `object(id)` for that. |
|
|
303
315
|
| `download(object, options?)` | Returns a `ReadableStream`. |
|
|
304
316
|
| `hosts(query?)` | Hosts serving this key's objects. |
|
|
305
317
|
|
|
318
|
+
### `ObjectSummary`
|
|
319
|
+
|
|
320
|
+
From `sharedSdk.objectSummaries()`.
|
|
321
|
+
|
|
322
|
+
`id` · `size` · `metadata` · `createdAt` · `updatedAt`
|
|
323
|
+
|
|
306
324
|
### Streaming
|
|
307
325
|
|
|
308
326
|
| | |
|
|
309
327
|
|---|---|
|
|
310
328
|
| `openStreams(sdk, credentials)` | `Streams` for one SDK's objects. An `Sdk` takes `{ indexerUrl, appMeta }` and a `SharedSdk` takes `{ indexerUrl, seed }`, typed by which one you pass. |
|
|
311
|
-
| `streams.url(object, options)` | `{ url, blob?, release() }`. Streams where it can, else reads the whole object into `blob`. |
|
|
329
|
+
| `streams.url(object, options)` | `{ url, blob?, warm(), release() }`. Streams where it can, else reads the whole object into `blob`. |
|
|
312
330
|
| `streams.download(object, options)` | Saves the object. Resolves `'streaming'`, `'saved'` or `'cancelled'`. Call it from a click handler. |
|
|
313
331
|
| `streams.close()` | Cancels this handle's streams. Other handles on the same SDK keep theirs. |
|
|
314
332
|
| `enableStreaming(options?)` | Registers the worker and resolves whether streaming is on. `openStreams` calls it for you, so call it only to pass `workerUrl`, `scope` or `type`. |
|
|
315
333
|
|
|
316
|
-
`options` for `url` and `download`: `name`, `type` (MIME), `onError(message)` for failures after streaming starts (logged when omitted), `onProgress
|
|
334
|
+
`options` for `url` and `download`: `name`, `type` (MIME), `onError(message)` for failures after streaming starts (logged when omitted), `onStatus`, `onProgress` and `onShard` (see [Status and progress](#status-and-progress)), and `signal` for `url`.
|
|
317
335
|
|
|
318
336
|
### `@siafoundation/sia-storage/stream-worker`
|
|
319
337
|
|
|
@@ -27,9 +27,12 @@ var strings = (data, fields) => fields.every((field) => typeof data?.[field] ===
|
|
|
27
27
|
function isAppMetadata(value) {
|
|
28
28
|
return strings(value, ["appId", "name", "description", "serviceUrl"]);
|
|
29
29
|
}
|
|
30
|
+
var isKeyHex = (value) => typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
|
|
30
31
|
function isConnection(value) {
|
|
31
32
|
const data = value;
|
|
32
|
-
if (data?.kind === "shared")
|
|
33
|
+
if (data?.kind === "shared") {
|
|
34
|
+
return strings(data, ["indexerUrl", "seed"]) && (data.sealKey === void 0 || isKeyHex(data.sealKey));
|
|
35
|
+
}
|
|
33
36
|
return data?.kind === "app" && strings(data, ["indexerUrl", "appKey"]) && isAppMetadata(data.appMeta);
|
|
34
37
|
}
|
|
35
38
|
function isSourceReply(reply) {
|
|
@@ -72,4 +75,4 @@ export {
|
|
|
72
75
|
isSourceReply,
|
|
73
76
|
requestReply
|
|
74
77
|
};
|
|
75
|
-
//# sourceMappingURL=chunk-
|
|
78
|
+
//# sourceMappingURL=chunk-WOQD3MPG.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 * A `SharedSdk`'s `sealKey` is the hex of the random key the page seals its\n * objects with. A page from before it existed sends none.\n */\nexport type Connection =\n | { kind: 'shared'; indexerUrl: string; seed: string; sealKey?: 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 // The object, sealed with its app key or the share's `sealKey`, so the\n // worker can open it without a lookup. Without it the worker looks the\n // object 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 // Sent while the worker reports `sia-streaming` active. The event it raises\n // in the worker is what keeps the worker running.\n | { type: 'sia-keep-alive' }\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 // Whether any request from this page is still being served. Sent when that\n // changes, and in reply to a keep-alive that arrives when none is.\n | { type: 'sia-streaming'; active: boolean }\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\n// 32 bytes as hex, which is what the worker builds an AppKey from.\nconst isKeyHex = (value: unknown) =>\n typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value)\n\nfunction isConnection(value: unknown): value is Connection {\n const data = value as Fields\n if (data?.kind === 'shared') {\n return (\n strings(data, ['indexerUrl', 'seed']) &&\n (data.sealKey === undefined || isKeyHex(data.sealKey))\n )\n }\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,5 +1,5 @@
|
|
|
1
1
|
import { ShardProgress, AppMetadata, Sdk, SharedSdk, PinnedObject } from '../wasm/sia_storage_wasm.js';
|
|
2
|
-
export { Account, AppKey, AppMetadata, Builder, DownloadOptions, Host, HostQuery, KeyRecord, KeyStats, ObjectEvent, PackedUpload, PinnedObject,
|
|
2
|
+
export { Account, AppKey, AppMetadata, Builder, DownloadOptions, Host, HostQuery, KeyRecord, KeyStats, ObjectEvent, ObjectSummary, PackedUpload, PinnedObject, 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
|
|
|
@@ -92,6 +92,16 @@ type StreamedFile = {
|
|
|
92
92
|
url: string;
|
|
93
93
|
/** The whole file, when the page read it itself instead of streaming. */
|
|
94
94
|
blob?: Blob;
|
|
95
|
+
/**
|
|
96
|
+
* Opens connections to the hosts holding the start of the file, so a play
|
|
97
|
+
* that follows gets its first bytes sooner. It reads one byte, which asks
|
|
98
|
+
* each host of the first slab for a single 64-byte segment, and reports no
|
|
99
|
+
* status or progress. Resolves when the byte arrives, or at once for a file
|
|
100
|
+
* the page read itself. Never rejects, since a play still works without it.
|
|
101
|
+
* Call it when a play is likely, such as when the pointer reaches a Play
|
|
102
|
+
* button. The earlier before the play, the more it saves.
|
|
103
|
+
*/
|
|
104
|
+
warm(): Promise<void>;
|
|
95
105
|
/** Frees the URL. Call it when the element showing it goes away. */
|
|
96
106
|
release(): void;
|
|
97
107
|
};
|
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-WOQD3MPG.js";
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
10
|
import wasmInit from "../wasm/sia_storage_wasm.js";
|
|
@@ -31,6 +31,9 @@ async function checkPrivateRelay(userAgent, request) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// src/wasm.ts
|
|
35
|
+
import { AppKey } from "../wasm/sia_storage_wasm.js";
|
|
36
|
+
|
|
34
37
|
// src/stream/fallback.ts
|
|
35
38
|
function chunks(stream) {
|
|
36
39
|
return stream.getReader();
|
|
@@ -134,12 +137,14 @@ async function saveToDisk(sdk, object, name, type, events) {
|
|
|
134
137
|
// src/stream/page.ts
|
|
135
138
|
var DEFAULT_WORKER_URL = "/sia-storage-sw.js";
|
|
136
139
|
var SETUP_TIMEOUT = 5e3;
|
|
140
|
+
var KEEP_ALIVE_INTERVAL = 1e4;
|
|
137
141
|
var entries = /* @__PURE__ */ new Map();
|
|
138
142
|
var opened = /* @__PURE__ */ new Set();
|
|
139
143
|
var worker;
|
|
140
144
|
var initialization;
|
|
141
145
|
var initOptions = {};
|
|
142
146
|
var failedController;
|
|
147
|
+
var keepAlive;
|
|
143
148
|
function enableStreaming(options = {}) {
|
|
144
149
|
if (!initialization) {
|
|
145
150
|
initOptions = options;
|
|
@@ -220,7 +225,13 @@ function openStreams(sdk, credentials) {
|
|
|
220
225
|
options.signal ?? new AbortController().signal
|
|
221
226
|
);
|
|
222
227
|
const url = URL.createObjectURL(blob);
|
|
223
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
url,
|
|
230
|
+
blob,
|
|
231
|
+
warm: async () => {
|
|
232
|
+
},
|
|
233
|
+
release: () => URL.revokeObjectURL(url)
|
|
234
|
+
};
|
|
224
235
|
},
|
|
225
236
|
async download(object, options) {
|
|
226
237
|
const stream = streamUrl(handle, object, options);
|
|
@@ -259,12 +270,30 @@ function openedFor(sdk, credentials) {
|
|
|
259
270
|
appKey: toHex(appKey.export()),
|
|
260
271
|
appMeta
|
|
261
272
|
};
|
|
262
|
-
return { session, connection, appKey };
|
|
273
|
+
return { session, connection, sealKey: appKey };
|
|
263
274
|
}
|
|
264
275
|
if (typeof indexerUrl !== "string" || typeof seed !== "string") {
|
|
265
276
|
throw new TypeError("openStreams(SharedSdk) needs { indexerUrl, seed }.");
|
|
266
277
|
}
|
|
267
|
-
|
|
278
|
+
const sealing = randomSealKey();
|
|
279
|
+
return {
|
|
280
|
+
session,
|
|
281
|
+
connection: {
|
|
282
|
+
kind: "shared",
|
|
283
|
+
indexerUrl,
|
|
284
|
+
seed,
|
|
285
|
+
...sealing && { sealKey: sealing.hex }
|
|
286
|
+
},
|
|
287
|
+
...sealing && { sealKey: sealing.key }
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function randomSealKey() {
|
|
291
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
292
|
+
try {
|
|
293
|
+
return { key: new AppKey(bytes), hex: toHex(bytes) };
|
|
294
|
+
} catch {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
268
297
|
}
|
|
269
298
|
function toHex(bytes) {
|
|
270
299
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -277,7 +306,7 @@ function streamUrl(handle, object, { name, type, onError, onProgress, onShard, o
|
|
|
277
306
|
entries.set(token, {
|
|
278
307
|
handle,
|
|
279
308
|
objectId: object.id(),
|
|
280
|
-
sealed: handle.
|
|
309
|
+
sealed: handle.sealKey && object.seal(handle.sealKey),
|
|
281
310
|
size: object.size(),
|
|
282
311
|
name,
|
|
283
312
|
mime: type || "application/octet-stream",
|
|
@@ -285,8 +314,18 @@ function streamUrl(handle, object, { name, type, onError, onProgress, onShard, o
|
|
|
285
314
|
events: { onProgress, onShard, onStatus },
|
|
286
315
|
received: 0
|
|
287
316
|
});
|
|
317
|
+
const url = streamPath(worker.scope, worker.clientId, token);
|
|
288
318
|
return {
|
|
289
|
-
url
|
|
319
|
+
url,
|
|
320
|
+
async warm() {
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(`${url}?warm=1`, {
|
|
323
|
+
headers: { Range: "bytes=0-0" }
|
|
324
|
+
});
|
|
325
|
+
await response.arrayBuffer();
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
},
|
|
290
329
|
release() {
|
|
291
330
|
entries.delete(token);
|
|
292
331
|
const message = { type: "sia-release", token };
|
|
@@ -299,16 +338,33 @@ function releaseStreams(handle) {
|
|
|
299
338
|
if (entry.handle === handle) entries.delete(token);
|
|
300
339
|
}
|
|
301
340
|
if (!opened.delete(handle)) return;
|
|
341
|
+
handle.sealKey?.free();
|
|
302
342
|
const message = {
|
|
303
343
|
type: "sia-release-share",
|
|
304
344
|
session: handle.session
|
|
305
345
|
};
|
|
306
346
|
navigator.serviceWorker.controller?.postMessage(message);
|
|
307
347
|
}
|
|
348
|
+
function keepWorkerAlive(streaming) {
|
|
349
|
+
clearInterval(keepAlive);
|
|
350
|
+
keepAlive = void 0;
|
|
351
|
+
const controller = navigator.serviceWorker.controller;
|
|
352
|
+
if (!streaming || !controller) return;
|
|
353
|
+
const message = { type: "sia-keep-alive" };
|
|
354
|
+
keepAlive = setInterval(() => {
|
|
355
|
+
if (navigator.serviceWorker.controller === controller) {
|
|
356
|
+
controller.postMessage(message);
|
|
357
|
+
} else {
|
|
358
|
+
keepWorkerAlive(false);
|
|
359
|
+
}
|
|
360
|
+
}, KEEP_ALIVE_INTERVAL);
|
|
361
|
+
}
|
|
308
362
|
function onWorkerMessage(event) {
|
|
309
363
|
if (event.source !== navigator.serviceWorker.controller) return;
|
|
310
364
|
const data = event.data;
|
|
311
|
-
if (data?.type === "sia-
|
|
365
|
+
if (data?.type === "sia-streaming") {
|
|
366
|
+
keepWorkerAlive(data.active === true);
|
|
367
|
+
} else if (data?.type === "sia-stream-error") {
|
|
312
368
|
const entry = entries.get(data.token);
|
|
313
369
|
if (!entry) return;
|
|
314
370
|
const { offset, length } = data;
|
|
@@ -352,10 +408,11 @@ function onWorkerMessage(event) {
|
|
|
352
408
|
|
|
353
409
|
// src/index.ts
|
|
354
410
|
import {
|
|
355
|
-
AppKey,
|
|
411
|
+
AppKey as AppKey2,
|
|
356
412
|
Builder,
|
|
357
413
|
KeyRecord,
|
|
358
414
|
ObjectEvent,
|
|
415
|
+
ObjectSummary,
|
|
359
416
|
PackedUpload,
|
|
360
417
|
PinnedObject,
|
|
361
418
|
Sdk,
|
|
@@ -372,10 +429,11 @@ async function initSia() {
|
|
|
372
429
|
await initPromise;
|
|
373
430
|
}
|
|
374
431
|
export {
|
|
375
|
-
AppKey,
|
|
432
|
+
AppKey2 as AppKey,
|
|
376
433
|
Builder,
|
|
377
434
|
KeyRecord,
|
|
378
435
|
ObjectEvent,
|
|
436
|
+
ObjectSummary,
|
|
379
437
|
PackedUpload,
|
|
380
438
|
PinnedObject,
|
|
381
439
|
Sdk,
|
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 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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/relay.ts","../src/wasm.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 ObjectSummary,\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 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","// The build keeps imports of the WebAssembly glue as written and resolves\n// them from dist/, so they must come from a file directly in src/. Code in\n// subfolders imports the glue through here.\nexport { AppKey } from '../wasm/sia_storage_wasm.js'\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 AppMetadata,\n PinnedObject,\n SealedObject,\n Sdk,\n SharedSdk,\n} from '../../wasm/sia_storage_wasm.js'\nimport { AppKey } from '../wasm'\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 /**\n * Opens connections to the hosts holding the start of the file, so a play\n * that follows gets its first bytes sooner. It reads one byte, which asks\n * each host of the first slab for a single 64-byte segment, and reports no\n * status or progress. Resolves when the byte arrives, or at once for a file\n * the page read itself. Never rejects, since a play still works without it.\n * Call it when a play is likely, such as when the pointer reaches a Play\n * button. The earlier before the play, the more it saves.\n */\n warm(): Promise<void>\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// Well inside the 30 seconds Firefox lets a worker go without an event.\nconst KEEP_ALIVE_INTERVAL = 10_000\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 *\n * `sealKey` seals each object the page streams, so the worker opens it instead\n * of asking the indexer for it again. An `Sdk`'s is a copy of its app key. A\n * `SharedSdk` has none, so it gets a random key that only this page and its\n * worker see. Either is freed on close.\n */\ntype Opened = { session: string; connection: Connection; sealKey?: 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\nlet keepAlive: ReturnType<typeof setInterval> | 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 {\n url,\n blob,\n warm: async () => {},\n release: () => URL.revokeObjectURL(url),\n }\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, sealKey: appKey }\n }\n if (typeof indexerUrl !== 'string' || typeof seed !== 'string') {\n throw new TypeError('openStreams(SharedSdk) needs { indexerUrl, seed }.')\n }\n const sealing = randomSealKey()\n return {\n session,\n connection: {\n kind: 'shared',\n indexerUrl,\n seed,\n ...(sealing && { sealKey: sealing.hex }),\n },\n ...(sealing && { sealKey: sealing.key }),\n }\n}\n\n/**\n * Undefined when the SDK's WebAssembly is not running, which a page holding a\n * connected `SharedSdk` has always started. The worker then looks objects up.\n */\nfunction randomSealKey() {\n const bytes = crypto.getRandomValues(new Uint8Array(32))\n try {\n return { key: new AppKey(bytes), hex: toHex(bytes) }\n } catch {\n return undefined\n }\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.sealKey && object.seal(handle.sealKey),\n size: object.size(),\n name,\n mime: type || 'application/octet-stream',\n onError,\n events: { onProgress, onShard, onStatus },\n received: 0,\n })\n const url = streamPath(worker.scope, worker.clientId, token)\n return {\n url,\n async warm() {\n try {\n const response = await fetch(`${url}?warm=1`, {\n headers: { Range: 'bytes=0-0' },\n })\n await response.arrayBuffer()\n } catch {\n // A play after a failed warm-up connects as it would have anyway.\n }\n },\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 handle.sealKey?.free()\n const message = {\n type: 'sia-release-share',\n session: handle.session,\n } satisfies PageMessage\n navigator.serviceWorker.controller?.postMessage(message)\n}\n\n/**\n * Messages the worker while it is serving this page, which Firefox needs to\n * keep the worker running past 30 seconds. See `hold` in the worker.\n */\nfunction keepWorkerAlive(streaming: boolean) {\n clearInterval(keepAlive)\n keepAlive = undefined\n const controller = navigator.serviceWorker.controller\n if (!streaming || !controller) return\n const message = { type: 'sia-keep-alive' } satisfies PageMessage\n keepAlive = setInterval(() => {\n // Streams end with the worker serving them, so a new controller has none.\n if (navigator.serviceWorker.controller === controller) {\n controller.postMessage(message)\n } else {\n keepWorkerAlive(false)\n }\n }, KEEP_ALIVE_INTERVAL)\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-streaming') {\n keepWorkerAlive(data.active === true)\n } else 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":["AppKey"]}
|
package/dist/index.node.d.ts
CHANGED
|
@@ -245,10 +245,13 @@ export declare class Sdk {
|
|
|
245
245
|
deleteObject(key: string): Promise<void>
|
|
246
246
|
/** Returns metadata about a specific object. */
|
|
247
247
|
object(key: string): Promise<PinnedObject>
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Unpins slabs not used by any object on the account.
|
|
250
|
+
*
|
|
251
|
+
* `before` prunes only slabs pinned before that time. Omitting it leaves
|
|
252
|
+
* the cutoff to the indexer.
|
|
253
|
+
*/
|
|
254
|
+
pruneSlabs(before?: Date | undefined | null): Promise<void>
|
|
252
255
|
/** Returns the current account. */
|
|
253
256
|
account(): Promise<Account>
|
|
254
257
|
/** Creates a signed URL for sharing an object. */
|
|
@@ -277,6 +280,11 @@ export declare class SharedSdk {
|
|
|
277
280
|
object(id: string): Promise<PinnedObject>
|
|
278
281
|
/** Lists and decrypts a page of the objects the key grants access to. */
|
|
279
282
|
objects(offset: number, limit: number): Promise<Array<PinnedObject>>
|
|
283
|
+
/**
|
|
284
|
+
* Lists a page of the objects the key grants access to without their
|
|
285
|
+
* slabs, which is much smaller and faster than `objects`.
|
|
286
|
+
*/
|
|
287
|
+
objectSummaries(offset: number, limit: number): Promise<Array<ObjectSummary>>
|
|
280
288
|
/**
|
|
281
289
|
* Returns the hosts serving this key's objects, optionally filtered by a
|
|
282
290
|
* query. Mirrors `Sdk.hosts()` but is scoped to the sharing key, so the
|
|
@@ -406,6 +414,20 @@ export interface ObjectsCursor {
|
|
|
406
414
|
after: Date
|
|
407
415
|
}
|
|
408
416
|
|
|
417
|
+
/**
|
|
418
|
+
* An object listed without its slabs, as `SharedSdk.objectSummaries`
|
|
419
|
+
* returns it. It cannot be downloaded. Fetch the full object with
|
|
420
|
+
* `SharedSdk.object(id)` to download it.
|
|
421
|
+
*/
|
|
422
|
+
export interface ObjectSummary {
|
|
423
|
+
id: string
|
|
424
|
+
/** The object's size in bytes. */
|
|
425
|
+
size: bigint
|
|
426
|
+
metadata: Buffer
|
|
427
|
+
createdAt: Date
|
|
428
|
+
updatedAt: Date
|
|
429
|
+
}
|
|
430
|
+
|
|
409
431
|
/** Packed upload options. */
|
|
410
432
|
export interface PackedUploadOptions {
|
|
411
433
|
maxBufferedSlabs?: number
|
|
@@ -420,15 +442,6 @@ export interface PinnedSector {
|
|
|
420
442
|
hostKey: string
|
|
421
443
|
}
|
|
422
444
|
|
|
423
|
-
/** A pinned slab from the indexer. */
|
|
424
|
-
export interface PinnedSlab {
|
|
425
|
-
version: number
|
|
426
|
-
id: string
|
|
427
|
-
encryptionKey: Buffer
|
|
428
|
-
minShards: number
|
|
429
|
-
sectors: Array<PinnedSector>
|
|
430
|
-
}
|
|
431
|
-
|
|
432
445
|
/** A sealed object for offline storage. */
|
|
433
446
|
export interface SealedObject {
|
|
434
447
|
id: string
|