@siafoundation/sia-storage 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -3
- package/dist/{chunk-QYQQIWFH.js → chunk-YOMDUMYW.js} +7 -3
- package/dist/chunk-YOMDUMYW.js.map +1 -0
- package/dist/index.d.ts +59 -6
- package/dist/index.js +120 -29
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +40 -15
- package/dist/next.js +1 -1
- package/dist/sia-storage-sw.js +2 -2
- package/dist/stream-worker.js +111 -14
- package/dist/stream-worker.js.map +1 -1
- package/package.json +8 -7
- package/wasm/sia_storage_wasm.d.ts +61 -38
- package/wasm/sia_storage_wasm.js +141 -40
- package/wasm/sia_storage_wasm_bg.wasm +0 -0
- package/wasm/sia_storage_wasm_bg.wasm.d.ts +10 -4
- package/dist/chunk-QYQQIWFH.js.map +0 -1
package/README.md
CHANGED
|
@@ -96,6 +96,36 @@ 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
|
+
|
|
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
|
+
|
|
99
129
|
### Serve the worker
|
|
100
130
|
|
|
101
131
|
The worker is one file, `sia-storage-sw.js`, served from your site's root. Use the line for your setup.
|
|
@@ -188,6 +218,7 @@ Near-identical surfaces. Real differences:
|
|
|
188
218
|
- App identifier: `appMeta.appId: string (hex)` on browser, `appMeta.id: Buffer(32)` on Node.
|
|
189
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).
|
|
190
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.
|
|
191
222
|
- `sdk.unshareObject(key, object)` takes a `PinnedObject` on browser and an object id `string` on Node.
|
|
192
223
|
- `sdk.hosts(query?)` and `sharedSdk.hosts(query?)` accept a `HostQuery` on browser; on Node they take no arguments.
|
|
193
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.
|
|
@@ -219,7 +250,7 @@ Returned from `Builder.register()`, `Builder.connected()`, or `Builder.connectPr
|
|
|
219
250
|
| `updateObjectMetadata(object)` | Push local metadata changes to the indexer. |
|
|
220
251
|
| `objectShareUrl(object, validUntil)` / `objectFromShareUrl(url)` | Create / consume share URLs. |
|
|
221
252
|
| `objectEvents(cursor?, limit)` | Paginated change feed. |
|
|
222
|
-
| `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. |
|
|
223
254
|
| `createSharingKey(description, expiresAt?)` | Create a `SharingKey` for read-only sharing. |
|
|
224
255
|
| `sharingKeys(offset, limit)` / `sharingKey(key)` | List keys / fetch one key's `KeyRecord`. |
|
|
225
256
|
| `shareObject(key, object)` / `unshareObject(key, object)` | Attach / detach an object on a sharing key. |
|
|
@@ -280,20 +311,27 @@ sharing key's seed. Downloads are paid for by the key's owner.
|
|
|
280
311
|
|---|---|
|
|
281
312
|
| `stats()` | The key's `KeyStats` snapshot. |
|
|
282
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. |
|
|
283
315
|
| `download(object, options?)` | Returns a `ReadableStream`. |
|
|
284
316
|
| `hosts(query?)` | Hosts serving this key's objects. |
|
|
285
317
|
|
|
318
|
+
### `ObjectSummary`
|
|
319
|
+
|
|
320
|
+
From `sharedSdk.objectSummaries()`.
|
|
321
|
+
|
|
322
|
+
`id` · `size` · `metadata` · `createdAt` · `updatedAt`
|
|
323
|
+
|
|
286
324
|
### Streaming
|
|
287
325
|
|
|
288
326
|
| | |
|
|
289
327
|
|---|---|
|
|
290
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. |
|
|
291
|
-
| `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`. |
|
|
292
330
|
| `streams.download(object, options)` | Saves the object. Resolves `'streaming'`, `'saved'` or `'cancelled'`. Call it from a click handler. |
|
|
293
331
|
| `streams.close()` | Cancels this handle's streams. Other handles on the same SDK keep theirs. |
|
|
294
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`. |
|
|
295
333
|
|
|
296
|
-
`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`.
|
|
297
335
|
|
|
298
336
|
### `@siafoundation/sia-storage/stream-worker`
|
|
299
337
|
|
|
@@ -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`;
|
|
@@ -26,14 +27,17 @@ var strings = (data, fields) => fields.every((field) => typeof data?.[field] ===
|
|
|
26
27
|
function isAppMetadata(value) {
|
|
27
28
|
return strings(value, ["appId", "name", "description", "serviceUrl"]);
|
|
28
29
|
}
|
|
30
|
+
var isKeyHex = (value) => typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
|
|
29
31
|
function isConnection(value) {
|
|
30
32
|
const data = value;
|
|
31
|
-
if (data?.kind === "shared")
|
|
33
|
+
if (data?.kind === "shared") {
|
|
34
|
+
return strings(data, ["indexerUrl", "seed"]) && (data.sealKey === void 0 || isKeyHex(data.sealKey));
|
|
35
|
+
}
|
|
32
36
|
return data?.kind === "app" && strings(data, ["indexerUrl", "appKey"]) && isAppMetadata(data.appMeta);
|
|
33
37
|
}
|
|
34
38
|
function isSourceReply(reply) {
|
|
35
39
|
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;
|
|
40
|
+
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
41
|
}
|
|
38
42
|
function requestReply(target, message, { timeout, signal }) {
|
|
39
43
|
return new Promise((resolve, reject) => {
|
|
@@ -71,4 +75,4 @@ export {
|
|
|
71
75
|
isSourceReply,
|
|
72
76
|
requestReply
|
|
73
77
|
};
|
|
74
|
-
//# sourceMappingURL=chunk-
|
|
78
|
+
//# sourceMappingURL=chunk-YOMDUMYW.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\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\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":";AACO,SAAS,mBAAmB,OAAwB;AACzD,QAAM,UACJ,iBAAiB,QACb,MAAM,UACN,OAAO,UAAU,WACf,QACA;AACR,SAAO,QACJ,QAAQ,0CAA0C,gBAAgB,EAClE,QAAQ,2BAA2B,mBAAmB,EACtD;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,QAAQ,mBAAmB,YAAY,EACvC,QAAQ,aAAa,GAAG,EACxB,MAAM,GAAG,GAAI;AAClB;AAEO,SAAS,kBACd,OACA,MACA,OACA;AACA,QAAM,UAAU,mBAAmB,KAAK;AAExC,QAAM,QAAQ,QACV,UAAU,MAAM,MAAM,IAAI,MAAM,SAAS,MAAM,SAAS,CAAC,KACzD;AACJ,UAAQ,MAAM,gBAAgB,mBAAmB,IAAI,CAAC,GAAG,KAAK,KAAK,OAAO,EAAE;AAC5E,SAAO;AACT;;;ACNA,IAAM,gBAAoC,CAAC,UAAU,YAAY,QAAQ;AAiEzE,IAAM,cAAc;AAGb,SAAS,WAAW,OAAe,UAAkB,OAAe;AACzE,SAAO,GAAG,KAAK,kBAAkB,QAAQ,IAAI,KAAK;AACpD;AAEO,SAAS,gBAAgB,UAAkB;AAChD,QAAM,QAAQ,YAAY,KAAK,QAAQ;AACvC,SAAO,QAAQ,EAAE,UAAU,MAAM,CAAC,GAAI,OAAO,MAAM,CAAC,EAAG,IAAI;AAC7D;AAIA,IAAM,UAAU,CAAC,MAAc,WAC7B,OAAO,MAAM,CAAC,UAAU,OAAO,OAAO,KAAK,MAAM,QAAQ;AAGpD,SAAS,cAAc,OAAsC;AAClE,SAAO,QAAQ,OAAiB,CAAC,SAAS,QAAQ,eAAe,YAAY,CAAC;AAChF;AAGA,IAAM,WAAW,CAAC,UAChB,OAAO,UAAU,YAAY,kBAAkB,KAAK,KAAK;AAE3D,SAAS,aAAa,OAAqC;AACzD,QAAM,OAAO;AACb,MAAI,MAAM,SAAS,UAAU;AAC3B,WACE,QAAQ,MAAM,CAAC,cAAc,MAAM,CAAC,MACnC,KAAK,YAAY,UAAa,SAAS,KAAK,OAAO;AAAA,EAExD;AACA,SACE,MAAM,SAAS,SACf,QAAQ,MAAM,CAAC,cAAc,QAAQ,CAAC,KACtC,cAAc,KAAK,OAAO;AAE9B;AAEO,SAAS,cACd,OAC6C;AAC7C,QAAM,OAAO;AACb,SACE,MAAM,SAAS,YACf,QAAQ,MAAM,CAAC,WAAW,YAAY,QAAQ,MAAM,CAAC,KACrD,aAAa,KAAK,UAAU,MAC3B,KAAK,WAAW,SAAS,YACvB,OAAO,KAAK,WAAW,YAAY,KAAK,WAAW,SACtD,OAAO,cAAc,KAAK,IAAI,KAC7B,KAAK,QAAmB,MACxB,KAAK,WAAW,UACd,MAAM,QAAQ,KAAK,MAAM,KACxB,KAAK,OAAO,MAAM,CAAC,UAAU,cAAc,SAAS,KAAK,CAAC;AAElE;AAMO,SAAS,aACd,QACA,SACA,EAAE,SAAS,OAAO,GACA;AAClB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,EAAE,OAAO,MAAM,IAAI,IAAI,eAAe;AAC5C,UAAM,QACJ,YAAY,SACR,SACA;AAAA,MACE,MAAM,OAAO,MAAM,OAAO,IAAI,MAAM,mBAAmB,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AACN,UAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,QAAQ,MAAM,CAAC;AACvD,aAAS,OAAO,SAAqB;AACnC,mBAAa,KAAK;AAClB,cAAQ,oBAAoB,SAAS,KAAK;AAC1C,YAAM,MAAM;AACZ,YAAM,MAAM;AACZ,cAAQ;AAAA,IACV;AACA,QAAI,QAAQ,QAAS,QAAO,MAAM;AAClC,YAAQ,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AACvD,UAAM,YAAY,CAAC,EAAE,KAAK,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;AAC1D,UAAM,iBAAiB,MACrB,OAAO,MAAM,OAAO,IAAI,MAAM,mBAAmB,CAAC,CAAC;AACrD,QAAI;AACF,aAAO,YAAY,SAAS,CAAC,KAAK,CAAC;AAAA,IACrC,SAAS,OAAO;AACd,aAAO,MAAM,OAAO,KAAK,CAAC;AAAA,IAC5B;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,53 @@
|
|
|
1
|
-
import { AppMetadata, Sdk, SharedSdk, PinnedObject } from '../wasm/sia_storage_wasm.js';
|
|
2
|
-
export { Account, AppKey, AppMetadata, Builder, DownloadOptions, Host, HostQuery, KeyRecord, KeyStats, ObjectEvent, PackedUpload, PinnedObject,
|
|
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, 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
|
|
|
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,21 +79,29 @@ 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 = {
|
|
49
92
|
url: string;
|
|
50
93
|
/** The whole file, when the page read it itself instead of streaming. */
|
|
51
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>;
|
|
52
105
|
/** Frees the URL. Call it when the element showing it goes away. */
|
|
53
106
|
release(): void;
|
|
54
107
|
};
|
|
@@ -91,4 +144,4 @@ declare function openStreams<S extends Sdk | SharedSdk>(sdk: S, credentials: Cre
|
|
|
91
144
|
/** Initialize the WASM module. Safe to call multiple times. */
|
|
92
145
|
declare function initSia(): Promise<void>;
|
|
93
146
|
|
|
94
|
-
export { type AppCredentials, type CredentialsFor, type FileOptions, type SharedCredentials, type StreamedFile, type StreamingOptions, type Streams, detectPrivateRelay, enableStreaming, initSia, openStreams };
|
|
147
|
+
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-YOMDUMYW.js";
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
10
|
import wasmInit from "../wasm/sia_storage_wasm.js";
|
|
@@ -31,26 +31,61 @@ 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();
|
|
37
40
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
function notify(callback, value) {
|
|
42
|
+
if (!callback) return;
|
|
43
|
+
try {
|
|
44
|
+
callback(value);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
reportError(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function download(sdk, object, events) {
|
|
50
|
+
const { onShard } = events;
|
|
51
|
+
return sdk.download(
|
|
52
|
+
object,
|
|
53
|
+
onShard ? { onShardDownloaded: (shard) => notify(onShard, shard) } : {}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function track({ onProgress, onStatus }) {
|
|
57
|
+
let received = 0;
|
|
58
|
+
let started = false;
|
|
59
|
+
notify(onStatus, "connecting");
|
|
60
|
+
return {
|
|
61
|
+
chunk(length) {
|
|
62
|
+
if (!started) notify(onStatus, "downloading");
|
|
63
|
+
started = true;
|
|
64
|
+
received += length;
|
|
65
|
+
notify(onProgress, received);
|
|
66
|
+
},
|
|
67
|
+
end() {
|
|
68
|
+
notify(onStatus, "idle");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function readBlob(sdk, object, type, events, signal) {
|
|
73
|
+
const progress = track(events);
|
|
74
|
+
let reader;
|
|
75
|
+
const onAbort = () => void reader?.cancel();
|
|
41
76
|
signal.addEventListener("abort", onAbort);
|
|
42
77
|
const parts = [];
|
|
43
|
-
let bytesDownloaded = 0;
|
|
44
78
|
try {
|
|
79
|
+
reader = chunks(download(sdk, object, events));
|
|
45
80
|
while (true) {
|
|
46
81
|
const { done, value } = await reader.read();
|
|
47
82
|
if (done) break;
|
|
48
83
|
parts.push(value);
|
|
49
|
-
|
|
50
|
-
onProgress(bytesDownloaded);
|
|
84
|
+
progress.chunk(value.length);
|
|
51
85
|
}
|
|
52
86
|
} finally {
|
|
53
87
|
signal.removeEventListener("abort", onAbort);
|
|
88
|
+
progress.end();
|
|
54
89
|
}
|
|
55
90
|
signal.throwIfAborted();
|
|
56
91
|
return new Blob(parts, type ? { type } : {});
|
|
@@ -63,14 +98,14 @@ function saveBlob(blob, name) {
|
|
|
63
98
|
a.click();
|
|
64
99
|
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
65
100
|
}
|
|
66
|
-
async function saveToDisk(sdk, object, name, type,
|
|
101
|
+
async function saveToDisk(sdk, object, name, type, events) {
|
|
67
102
|
const picker = window.showSaveFilePicker;
|
|
68
103
|
if (!picker) {
|
|
69
104
|
const blob = await readBlob(
|
|
70
105
|
sdk,
|
|
71
106
|
object,
|
|
72
107
|
type,
|
|
73
|
-
|
|
108
|
+
events,
|
|
74
109
|
new AbortController().signal
|
|
75
110
|
);
|
|
76
111
|
saveBlob(blob, name);
|
|
@@ -83,16 +118,19 @@ async function saveToDisk(sdk, object, name, type, onProgress) {
|
|
|
83
118
|
if (e instanceof DOMException && e.name === "AbortError") return false;
|
|
84
119
|
throw e;
|
|
85
120
|
}
|
|
86
|
-
|
|
87
|
-
const progress =
|
|
121
|
+
const file = await handle.createWritable();
|
|
122
|
+
const progress = track(events);
|
|
123
|
+
const counter = new TransformStream({
|
|
88
124
|
transform(chunk, controller) {
|
|
89
|
-
|
|
90
|
-
onProgress(bytesDownloaded);
|
|
125
|
+
progress.chunk(chunk.length);
|
|
91
126
|
controller.enqueue(chunk);
|
|
92
127
|
}
|
|
93
128
|
});
|
|
94
|
-
|
|
95
|
-
|
|
129
|
+
try {
|
|
130
|
+
await download(sdk, object, events).pipeThrough(counter).pipeTo(file);
|
|
131
|
+
} finally {
|
|
132
|
+
progress.end();
|
|
133
|
+
}
|
|
96
134
|
return true;
|
|
97
135
|
}
|
|
98
136
|
|
|
@@ -181,12 +219,17 @@ function openStreams(sdk, credentials) {
|
|
|
181
219
|
sdk,
|
|
182
220
|
object,
|
|
183
221
|
options.type,
|
|
184
|
-
options
|
|
185
|
-
}),
|
|
222
|
+
options,
|
|
186
223
|
options.signal ?? new AbortController().signal
|
|
187
224
|
);
|
|
188
225
|
const url = URL.createObjectURL(blob);
|
|
189
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
url,
|
|
228
|
+
blob,
|
|
229
|
+
warm: async () => {
|
|
230
|
+
},
|
|
231
|
+
release: () => URL.revokeObjectURL(url)
|
|
232
|
+
};
|
|
190
233
|
},
|
|
191
234
|
async download(object, options) {
|
|
192
235
|
const stream = streamUrl(handle, object, options);
|
|
@@ -202,8 +245,7 @@ function openStreams(sdk, credentials) {
|
|
|
202
245
|
object,
|
|
203
246
|
options.name,
|
|
204
247
|
options.type,
|
|
205
|
-
options
|
|
206
|
-
})
|
|
248
|
+
options
|
|
207
249
|
);
|
|
208
250
|
return saved ? "saved" : "cancelled";
|
|
209
251
|
},
|
|
@@ -226,17 +268,35 @@ function openedFor(sdk, credentials) {
|
|
|
226
268
|
appKey: toHex(appKey.export()),
|
|
227
269
|
appMeta
|
|
228
270
|
};
|
|
229
|
-
return { session, connection, appKey };
|
|
271
|
+
return { session, connection, sealKey: appKey };
|
|
230
272
|
}
|
|
231
273
|
if (typeof indexerUrl !== "string" || typeof seed !== "string") {
|
|
232
274
|
throw new TypeError("openStreams(SharedSdk) needs { indexerUrl, seed }.");
|
|
233
275
|
}
|
|
234
|
-
|
|
276
|
+
const sealing = randomSealKey();
|
|
277
|
+
return {
|
|
278
|
+
session,
|
|
279
|
+
connection: {
|
|
280
|
+
kind: "shared",
|
|
281
|
+
indexerUrl,
|
|
282
|
+
seed,
|
|
283
|
+
...sealing && { sealKey: sealing.hex }
|
|
284
|
+
},
|
|
285
|
+
...sealing && { sealKey: sealing.key }
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function randomSealKey() {
|
|
289
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
290
|
+
try {
|
|
291
|
+
return { key: new AppKey(bytes), hex: toHex(bytes) };
|
|
292
|
+
} catch {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
235
295
|
}
|
|
236
296
|
function toHex(bytes) {
|
|
237
297
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
238
298
|
}
|
|
239
|
-
function streamUrl(handle, object, { name, type, onError }) {
|
|
299
|
+
function streamUrl(handle, object, { name, type, onError, onProgress, onShard, onStatus }) {
|
|
240
300
|
if (!worker || !navigator.serviceWorker.controller || !opened.has(handle)) {
|
|
241
301
|
return void 0;
|
|
242
302
|
}
|
|
@@ -244,14 +304,26 @@ function streamUrl(handle, object, { name, type, onError }) {
|
|
|
244
304
|
entries.set(token, {
|
|
245
305
|
handle,
|
|
246
306
|
objectId: object.id(),
|
|
247
|
-
sealed: handle.
|
|
307
|
+
sealed: handle.sealKey && object.seal(handle.sealKey),
|
|
248
308
|
size: object.size(),
|
|
249
309
|
name,
|
|
250
310
|
mime: type || "application/octet-stream",
|
|
251
|
-
onError
|
|
311
|
+
onError,
|
|
312
|
+
events: { onProgress, onShard, onStatus },
|
|
313
|
+
received: 0
|
|
252
314
|
});
|
|
315
|
+
const url = streamPath(worker.scope, worker.clientId, token);
|
|
253
316
|
return {
|
|
254
|
-
url
|
|
317
|
+
url,
|
|
318
|
+
async warm() {
|
|
319
|
+
try {
|
|
320
|
+
const response = await fetch(`${url}?warm=1`, {
|
|
321
|
+
headers: { Range: "bytes=0-0" }
|
|
322
|
+
});
|
|
323
|
+
await response.arrayBuffer();
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
},
|
|
255
327
|
release() {
|
|
256
328
|
entries.delete(token);
|
|
257
329
|
const message = { type: "sia-release", token };
|
|
@@ -264,6 +336,7 @@ function releaseStreams(handle) {
|
|
|
264
336
|
if (entry.handle === handle) entries.delete(token);
|
|
265
337
|
}
|
|
266
338
|
if (!opened.delete(handle)) return;
|
|
339
|
+
handle.sealKey?.free();
|
|
267
340
|
const message = {
|
|
268
341
|
type: "sia-release-share",
|
|
269
342
|
session: handle.session
|
|
@@ -280,10 +353,25 @@ function onWorkerMessage(event) {
|
|
|
280
353
|
const range = offset !== void 0 && length !== void 0 ? { offset, length } : void 0;
|
|
281
354
|
if (entry.onError) entry.onError(streamErrorMessage(data.message));
|
|
282
355
|
else reportStreamError(data.message, entry.name, range);
|
|
356
|
+
} else if (data?.type === "sia-stream-status") {
|
|
357
|
+
notify(entries.get(data.token)?.events.onStatus, data.status);
|
|
358
|
+
} else if (data?.type === "sia-stream-progress") {
|
|
359
|
+
const entry = entries.get(data.token);
|
|
360
|
+
if (!entry || !Array.isArray(data.shards)) return;
|
|
361
|
+
const { onProgress, onShard } = entry.events;
|
|
362
|
+
for (const shard of data.shards) notify(onShard, shard);
|
|
363
|
+
if (Number.isSafeInteger(data.bytes) && data.bytes > 0) {
|
|
364
|
+
entry.received += data.bytes;
|
|
365
|
+
notify(onProgress, entry.received);
|
|
366
|
+
}
|
|
283
367
|
} else if (data?.type === "sia-source") {
|
|
284
368
|
const port = event.ports[0];
|
|
285
369
|
if (!port) return;
|
|
286
370
|
const entry = entries.get(data.token);
|
|
371
|
+
const events = [];
|
|
372
|
+
if (entry?.events.onStatus) events.push("status");
|
|
373
|
+
if (entry?.events.onProgress) events.push("progress");
|
|
374
|
+
if (entry?.events.onShard) events.push("shards");
|
|
287
375
|
const reply = entry ? {
|
|
288
376
|
type: "source",
|
|
289
377
|
session: entry.handle.session,
|
|
@@ -292,7 +380,8 @@ function onWorkerMessage(event) {
|
|
|
292
380
|
...entry.sealed && { sealed: entry.sealed },
|
|
293
381
|
size: entry.size,
|
|
294
382
|
mime: entry.mime,
|
|
295
|
-
name: entry.name
|
|
383
|
+
name: entry.name,
|
|
384
|
+
...events.length > 0 && { events }
|
|
296
385
|
} : { type: "error" };
|
|
297
386
|
port.postMessage(reply);
|
|
298
387
|
port.close();
|
|
@@ -301,10 +390,11 @@ function onWorkerMessage(event) {
|
|
|
301
390
|
|
|
302
391
|
// src/index.ts
|
|
303
392
|
import {
|
|
304
|
-
AppKey,
|
|
393
|
+
AppKey as AppKey2,
|
|
305
394
|
Builder,
|
|
306
395
|
KeyRecord,
|
|
307
396
|
ObjectEvent,
|
|
397
|
+
ObjectSummary,
|
|
308
398
|
PackedUpload,
|
|
309
399
|
PinnedObject,
|
|
310
400
|
Sdk,
|
|
@@ -321,10 +411,11 @@ async function initSia() {
|
|
|
321
411
|
await initPromise;
|
|
322
412
|
}
|
|
323
413
|
export {
|
|
324
|
-
AppKey,
|
|
414
|
+
AppKey2 as AppKey,
|
|
325
415
|
Builder,
|
|
326
416
|
KeyRecord,
|
|
327
417
|
ObjectEvent,
|
|
418
|
+
ObjectSummary,
|
|
328
419
|
PackedUpload,
|
|
329
420
|
PinnedObject,
|
|
330
421
|
Sdk,
|