@siafoundation/sia-storage 0.1.0 → 0.1.2
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 +93 -1
- package/dist/chunk-QYQQIWFH.js +74 -0
- package/dist/chunk-QYQQIWFH.js.map +1 -0
- package/dist/cli.js +13 -0
- package/dist/index.d.ts +89 -1
- package/dist/index.js +303 -0
- package/dist/index.js.map +1 -1
- package/dist/index.node.cjs +52 -12
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.d.ts +32 -6
- package/dist/next.d.ts +3 -0
- package/dist/next.js +12 -0
- package/dist/sia-storage-sw.js +2 -0
- package/dist/stream-worker.d.ts +51 -0
- package/dist/stream-worker.js +441 -0
- package/dist/stream-worker.js.map +1 -0
- package/dist/vite.d.ts +39 -0
- package/dist/vite.js +32 -0
- package/package.json +26 -10
- package/wasm/sia_storage_wasm.d.ts +21 -12
- package/wasm/sia_storage_wasm.js +33 -32
- package/wasm/sia_storage_wasm_bg.wasm +0 -0
- package/wasm/sia_storage_wasm_bg.wasm.d.ts +8 -8
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { InitInput } from '../wasm/sia_storage_wasm.js';
|
|
2
|
+
|
|
3
|
+
type StreamClient = {
|
|
4
|
+
id: string;
|
|
5
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
6
|
+
};
|
|
7
|
+
type WaitUntil = {
|
|
8
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
9
|
+
};
|
|
10
|
+
type StreamFetchEvent = {
|
|
11
|
+
request: Request;
|
|
12
|
+
respondWith(response: Promise<Response>): void;
|
|
13
|
+
};
|
|
14
|
+
type StreamMessageEvent = WaitUntil & {
|
|
15
|
+
data: unknown;
|
|
16
|
+
source: unknown;
|
|
17
|
+
ports: readonly MessagePort[];
|
|
18
|
+
};
|
|
19
|
+
type StreamScope = {
|
|
20
|
+
location: {
|
|
21
|
+
origin: string;
|
|
22
|
+
};
|
|
23
|
+
skipWaiting(): Promise<void>;
|
|
24
|
+
clients: {
|
|
25
|
+
get(id: string): Promise<StreamClient | undefined>;
|
|
26
|
+
claim(): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
addEventListener(type: 'install' | 'activate', listener: (event: WaitUntil) => void): void;
|
|
29
|
+
addEventListener(type: 'message', listener: (event: StreamMessageEvent) => void): void;
|
|
30
|
+
addEventListener(type: 'fetch', listener: (event: StreamFetchEvent) => void): void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The worker side of streaming, for sites that already run a service worker.
|
|
35
|
+
*
|
|
36
|
+
* A page has only one service worker, so a site with its own calls
|
|
37
|
+
* `installStreams(self)` in it and passes that worker's URL to
|
|
38
|
+
* `enableStreaming`. Every other site serves the prebuilt `sia-storage-sw.js`,
|
|
39
|
+
* which is this function with the WebAssembly built in.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
type InstallStreamsOptions = {
|
|
43
|
+
/**
|
|
44
|
+
* The SDK's WebAssembly, as a URL, bytes or compiled module. Only needed
|
|
45
|
+
* when the worker's bundler does not emit `sia_storage_wasm_bg.wasm`.
|
|
46
|
+
*/
|
|
47
|
+
wasm?: InitInput;
|
|
48
|
+
};
|
|
49
|
+
declare function installStreams(scope: StreamScope, options?: InstallStreamsOptions): void;
|
|
50
|
+
|
|
51
|
+
export { type InstallStreamsOptions, type StreamScope, installStreams };
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSourceReply,
|
|
3
|
+
parseStreamPath,
|
|
4
|
+
requestReply,
|
|
5
|
+
streamErrorMessage
|
|
6
|
+
} from "./chunk-QYQQIWFH.js";
|
|
7
|
+
|
|
8
|
+
// src/stream-worker.ts
|
|
9
|
+
import wasmInit, {
|
|
10
|
+
AppKey,
|
|
11
|
+
Builder,
|
|
12
|
+
PinnedObject,
|
|
13
|
+
SharedSdk
|
|
14
|
+
} from "../wasm/sia_storage_wasm.js";
|
|
15
|
+
|
|
16
|
+
// src/stream/response.ts
|
|
17
|
+
var PROTECTIVE_HEADERS = {
|
|
18
|
+
"Cache-Control": "no-store",
|
|
19
|
+
"X-Content-Type-Options": "nosniff",
|
|
20
|
+
"Content-Security-Policy": "sandbox; default-src 'none'",
|
|
21
|
+
"Referrer-Policy": "no-referrer"
|
|
22
|
+
};
|
|
23
|
+
function parseRange(range, size) {
|
|
24
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
25
|
+
throw new RangeError("Invalid file size");
|
|
26
|
+
}
|
|
27
|
+
if (range === null) return { status: 200, offset: 0, length: size };
|
|
28
|
+
const invalid = { status: 416, offset: 0, length: 0 };
|
|
29
|
+
const match = /^bytes=(\d*)-(\d*)$/i.exec(range.trim());
|
|
30
|
+
if (!match || !match[1] && !match[2] || size === 0) return invalid;
|
|
31
|
+
const start = match[1];
|
|
32
|
+
const end = match[2];
|
|
33
|
+
const total = BigInt(size);
|
|
34
|
+
let first;
|
|
35
|
+
let last;
|
|
36
|
+
if (!start) {
|
|
37
|
+
const suffix = BigInt(end);
|
|
38
|
+
if (suffix === 0n) return invalid;
|
|
39
|
+
first = suffix >= total ? 0n : total - suffix;
|
|
40
|
+
last = total - 1n;
|
|
41
|
+
} else {
|
|
42
|
+
first = BigInt(start);
|
|
43
|
+
last = end ? BigInt(end) : total - 1n;
|
|
44
|
+
if (first >= total || last < first) return invalid;
|
|
45
|
+
if (last >= total) last = total - 1n;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
status: 206,
|
|
49
|
+
offset: Number(first),
|
|
50
|
+
length: Number(last - first + 1n)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function contentDisposition(name, download) {
|
|
54
|
+
const wellFormed = new TextDecoder().decode(new TextEncoder().encode(name));
|
|
55
|
+
const filename = wellFormed.replace(/[\u0000-\u001f\u007f/\\]/g, "_").trim() || "download";
|
|
56
|
+
const fallback = filename.replace(/[^\u0020-\u007e]|[";]/g, "_");
|
|
57
|
+
const encoded = encodeURIComponent(filename).replace(
|
|
58
|
+
/[!'()*]/g,
|
|
59
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
|
|
60
|
+
);
|
|
61
|
+
return `${download ? "attachment" : "inline"}; filename="${fallback}"; filename*=UTF-8''${encoded}`;
|
|
62
|
+
}
|
|
63
|
+
function responseHeaders(metadata, range, download) {
|
|
64
|
+
const mime = (metadata.mime.split(";", 1)[0] ?? "").trim();
|
|
65
|
+
const headers = new Headers({
|
|
66
|
+
"Accept-Ranges": "bytes",
|
|
67
|
+
"Content-Length": String(range.length),
|
|
68
|
+
"Content-Type": /^[\w!#$&^.+-]+\/[\w!#$&^.+-]+$/.test(mime) ? mime : "application/octet-stream",
|
|
69
|
+
"Content-Disposition": contentDisposition(metadata.name, download),
|
|
70
|
+
...PROTECTIVE_HEADERS
|
|
71
|
+
});
|
|
72
|
+
if (range.status === 206) {
|
|
73
|
+
headers.set(
|
|
74
|
+
"Content-Range",
|
|
75
|
+
`bytes ${range.offset}-${range.offset + range.length - 1}/${metadata.size}`
|
|
76
|
+
);
|
|
77
|
+
} else if (range.status === 416) {
|
|
78
|
+
headers.set("Content-Range", `bytes */${metadata.size}`);
|
|
79
|
+
}
|
|
80
|
+
return headers;
|
|
81
|
+
}
|
|
82
|
+
function errorResponse(status) {
|
|
83
|
+
const headers = new Headers(PROTECTIVE_HEADERS);
|
|
84
|
+
if (status === 405) headers.set("Allow", "GET, HEAD");
|
|
85
|
+
return new Response(null, { status, headers });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/stream/worker.ts
|
|
89
|
+
var SOURCE_TIMEOUT = 5e3;
|
|
90
|
+
var IDLE_TIMEOUT = 5 * 6e4;
|
|
91
|
+
function serveStreams(scope, connect, supportsDownloads) {
|
|
92
|
+
const sources = /* @__PURE__ */ new Map();
|
|
93
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
94
|
+
const released = /* @__PURE__ */ new Set();
|
|
95
|
+
const key = (clientId, id) => JSON.stringify([clientId, id]);
|
|
96
|
+
function idle(session) {
|
|
97
|
+
clearTimeout(session.timer);
|
|
98
|
+
if (!session.closed && session.jobs.size === 0) {
|
|
99
|
+
session.timer = setTimeout(
|
|
100
|
+
() => void disposeSession(session),
|
|
101
|
+
IDLE_TIMEOUT
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function sessionFor(clientId, details) {
|
|
106
|
+
const sessionKey = key(clientId, details.session);
|
|
107
|
+
let session = sessions.get(sessionKey);
|
|
108
|
+
if (!session) {
|
|
109
|
+
session = {
|
|
110
|
+
key: sessionKey,
|
|
111
|
+
connection: details.connection,
|
|
112
|
+
sources: /* @__PURE__ */ new Set(),
|
|
113
|
+
objects: /* @__PURE__ */ new Map(),
|
|
114
|
+
jobs: /* @__PURE__ */ new Set(),
|
|
115
|
+
closed: false
|
|
116
|
+
};
|
|
117
|
+
sessions.set(sessionKey, session);
|
|
118
|
+
}
|
|
119
|
+
return session;
|
|
120
|
+
}
|
|
121
|
+
async function disposeSession(session) {
|
|
122
|
+
if (session.closed) return;
|
|
123
|
+
session.closed = true;
|
|
124
|
+
sessions.delete(session.key);
|
|
125
|
+
clearTimeout(session.timer);
|
|
126
|
+
for (const source of session.sources) dropSource(source);
|
|
127
|
+
await Promise.allSettled(session.jobs);
|
|
128
|
+
await Promise.allSettled(
|
|
129
|
+
[...session.objects.values()].map(
|
|
130
|
+
async (object) => (await object).free()
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
try {
|
|
134
|
+
;
|
|
135
|
+
(await session.sdk)?.free();
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function sdkFor(session) {
|
|
140
|
+
if (session.sdk) return session.sdk;
|
|
141
|
+
const { connection } = session;
|
|
142
|
+
const connecting = Promise.resolve().then(() => connect(connection));
|
|
143
|
+
session.sdk = connecting;
|
|
144
|
+
connecting.catch(() => {
|
|
145
|
+
if (session.sdk === connecting) delete session.sdk;
|
|
146
|
+
});
|
|
147
|
+
return connecting;
|
|
148
|
+
}
|
|
149
|
+
function objectFor(session, sdk, ref) {
|
|
150
|
+
const { objectId } = ref;
|
|
151
|
+
const cached = session.objects.get(objectId);
|
|
152
|
+
if (cached) return cached;
|
|
153
|
+
const lookup = Promise.resolve().then(() => sdk.object(ref));
|
|
154
|
+
session.objects.set(objectId, lookup);
|
|
155
|
+
lookup.catch(() => {
|
|
156
|
+
if (session.objects.get(objectId) === lookup) {
|
|
157
|
+
session.objects.delete(objectId);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return lookup;
|
|
161
|
+
}
|
|
162
|
+
function getSource(client, token) {
|
|
163
|
+
const sourceKey = key(client.id, token);
|
|
164
|
+
const cached = sources.get(sourceKey);
|
|
165
|
+
if (cached) return cached;
|
|
166
|
+
const setup = new AbortController();
|
|
167
|
+
const request = { type: "sia-source", token };
|
|
168
|
+
const ready = requestReply(client, request, {
|
|
169
|
+
timeout: SOURCE_TIMEOUT,
|
|
170
|
+
signal: setup.signal
|
|
171
|
+
}).then((reply) => {
|
|
172
|
+
if (!isSourceReply(reply)) {
|
|
173
|
+
throw new Error("Shared file source unavailable.");
|
|
174
|
+
}
|
|
175
|
+
if (source.closed || released.has(key(client.id, reply.session))) {
|
|
176
|
+
throw new DOMException("Released", "AbortError");
|
|
177
|
+
}
|
|
178
|
+
const session = sessionFor(client.id, reply);
|
|
179
|
+
source.session = session;
|
|
180
|
+
session.sources.add(source);
|
|
181
|
+
idle(session);
|
|
182
|
+
const { objectId, sealed, name, mime, size } = reply;
|
|
183
|
+
return { objectId, sealed, name, mime, size };
|
|
184
|
+
});
|
|
185
|
+
const source = {
|
|
186
|
+
key: sourceKey,
|
|
187
|
+
client,
|
|
188
|
+
closed: false,
|
|
189
|
+
requests: /* @__PURE__ */ new Set(),
|
|
190
|
+
ready,
|
|
191
|
+
setup
|
|
192
|
+
};
|
|
193
|
+
sources.set(sourceKey, source);
|
|
194
|
+
return source;
|
|
195
|
+
}
|
|
196
|
+
function dropSource(source) {
|
|
197
|
+
if (source.closed) return;
|
|
198
|
+
source.closed = true;
|
|
199
|
+
sources.delete(source.key);
|
|
200
|
+
source.setup.abort(new DOMException("Released", "AbortError"));
|
|
201
|
+
for (const cancel of source.requests) cancel();
|
|
202
|
+
source.session?.sources.delete(source);
|
|
203
|
+
}
|
|
204
|
+
function startJob(request, token) {
|
|
205
|
+
let complete = () => {
|
|
206
|
+
};
|
|
207
|
+
const lifetime = new Promise((resolve) => {
|
|
208
|
+
complete = resolve;
|
|
209
|
+
});
|
|
210
|
+
const job = {
|
|
211
|
+
closed: false,
|
|
212
|
+
lifetime,
|
|
213
|
+
finish(cancel = false) {
|
|
214
|
+
if (job.closed) return;
|
|
215
|
+
job.closed = true;
|
|
216
|
+
request.signal.removeEventListener("abort", job.abort);
|
|
217
|
+
const { reader } = job;
|
|
218
|
+
const cancellation = cancel && reader ? reader.cancel().catch(() => {
|
|
219
|
+
}) : void 0;
|
|
220
|
+
void Promise.resolve(cancellation).finally(() => {
|
|
221
|
+
reader?.releaseLock();
|
|
222
|
+
job.source?.requests.delete(job.abort);
|
|
223
|
+
job.session?.jobs.delete(lifetime);
|
|
224
|
+
if (job.session) idle(job.session);
|
|
225
|
+
complete();
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
abort() {
|
|
229
|
+
if (job.closed) return;
|
|
230
|
+
job.controller?.error(new DOMException("Aborted", "AbortError"));
|
|
231
|
+
job.finish(true);
|
|
232
|
+
},
|
|
233
|
+
fail(error) {
|
|
234
|
+
if (job.closed) return;
|
|
235
|
+
const message = streamErrorMessage(error);
|
|
236
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
237
|
+
const { source, range } = job;
|
|
238
|
+
if (!aborted && source && !source.closed) {
|
|
239
|
+
try {
|
|
240
|
+
source.client.postMessage({
|
|
241
|
+
type: "sia-stream-error",
|
|
242
|
+
token,
|
|
243
|
+
message,
|
|
244
|
+
...range && { offset: range.offset, length: range.length }
|
|
245
|
+
});
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
job.controller?.error(new Error(message));
|
|
250
|
+
job.finish(true);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return job;
|
|
254
|
+
}
|
|
255
|
+
async function serve(request, clientId, token, download) {
|
|
256
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
257
|
+
return errorResponse(405);
|
|
258
|
+
}
|
|
259
|
+
if (!supportsDownloads) return errorResponse(503);
|
|
260
|
+
const job = startJob(request, token);
|
|
261
|
+
try {
|
|
262
|
+
const client = await scope.clients.get(clientId);
|
|
263
|
+
if (!client || request.signal.aborted) return errorResponse(404);
|
|
264
|
+
const source = getSource(client, token);
|
|
265
|
+
job.source = source;
|
|
266
|
+
source.requests.add(job.abort);
|
|
267
|
+
request.signal.addEventListener("abort", job.abort, { once: true });
|
|
268
|
+
const metadata = await source.ready;
|
|
269
|
+
if (job.closed) return errorResponse(502);
|
|
270
|
+
const session = source.session;
|
|
271
|
+
job.session = session;
|
|
272
|
+
session.jobs.add(job.lifetime);
|
|
273
|
+
clearTimeout(session.timer);
|
|
274
|
+
const range = parseRange(
|
|
275
|
+
request.method === "HEAD" ? null : request.headers.get("range"),
|
|
276
|
+
metadata.size
|
|
277
|
+
);
|
|
278
|
+
job.range = range;
|
|
279
|
+
const headers = responseHeaders(metadata, range, download);
|
|
280
|
+
if (request.method === "HEAD" || range.status === 416 || range.length === 0) {
|
|
281
|
+
job.finish();
|
|
282
|
+
return new Response(null, { status: range.status, headers });
|
|
283
|
+
}
|
|
284
|
+
const sdk = await sdkFor(session);
|
|
285
|
+
if (job.closed) return errorResponse(502);
|
|
286
|
+
const object = await objectFor(session, sdk, metadata);
|
|
287
|
+
if (job.closed) return errorResponse(502);
|
|
288
|
+
const chunks = sdk.download(object, { offset: range.offset, length: range.length }).getReader();
|
|
289
|
+
job.reader = chunks;
|
|
290
|
+
return new Response(rangeBody(job, chunks, range.length), {
|
|
291
|
+
status: range.status,
|
|
292
|
+
headers
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
job.fail(error);
|
|
296
|
+
if (job.source && !job.source.session) dropSource(job.source);
|
|
297
|
+
return errorResponse(502);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
scope.addEventListener(
|
|
301
|
+
"install",
|
|
302
|
+
(event) => event.waitUntil(scope.skipWaiting())
|
|
303
|
+
);
|
|
304
|
+
scope.addEventListener(
|
|
305
|
+
"activate",
|
|
306
|
+
(event) => event.waitUntil(scope.clients.claim())
|
|
307
|
+
);
|
|
308
|
+
scope.addEventListener("message", (event) => {
|
|
309
|
+
const source = event.source;
|
|
310
|
+
const clientId = typeof source?.id === "string" ? source.id : void 0;
|
|
311
|
+
if (!clientId) return;
|
|
312
|
+
const data = event.data;
|
|
313
|
+
if (data?.type === "sia-client") {
|
|
314
|
+
const reply = { clientId, supported: supportsDownloads };
|
|
315
|
+
event.ports[0]?.postMessage(reply);
|
|
316
|
+
event.ports[0]?.close();
|
|
317
|
+
} else if (data?.type === "sia-release" && typeof data.token === "string") {
|
|
318
|
+
const source2 = sources.get(key(clientId, data.token));
|
|
319
|
+
if (source2) dropSource(source2);
|
|
320
|
+
} else if (data?.type === "sia-release-share" && typeof data.session === "string") {
|
|
321
|
+
const sessionKey = key(clientId, data.session);
|
|
322
|
+
released.add(sessionKey);
|
|
323
|
+
const session = sessions.get(sessionKey);
|
|
324
|
+
if (session) event.waitUntil(disposeSession(session));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
scope.addEventListener("fetch", (event) => {
|
|
328
|
+
const url = new URL(event.request.url);
|
|
329
|
+
if (url.origin !== scope.location.origin) return;
|
|
330
|
+
const stream = parseStreamPath(url.pathname);
|
|
331
|
+
if (!stream) return;
|
|
332
|
+
const download = url.searchParams.get("download") === "1";
|
|
333
|
+
event.respondWith(
|
|
334
|
+
serve(event.request, stream.clientId, stream.token, download)
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function rangeBody(job, chunks, length) {
|
|
339
|
+
let remaining = length;
|
|
340
|
+
return new ReadableStream(
|
|
341
|
+
{
|
|
342
|
+
start(controller) {
|
|
343
|
+
job.controller = controller;
|
|
344
|
+
},
|
|
345
|
+
async pull(controller) {
|
|
346
|
+
try {
|
|
347
|
+
const { done, value } = await chunks.read();
|
|
348
|
+
if (job.closed) return;
|
|
349
|
+
if (done) {
|
|
350
|
+
if (remaining !== 0) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
"Shared file stream ended before all bytes arrived."
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
controller.close();
|
|
356
|
+
job.finish();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
remaining -= value.byteLength;
|
|
360
|
+
if (remaining < 0) {
|
|
361
|
+
throw new Error("Shared file stream exceeded the requested length.");
|
|
362
|
+
}
|
|
363
|
+
controller.enqueue(value);
|
|
364
|
+
if (remaining === 0) {
|
|
365
|
+
controller.close();
|
|
366
|
+
job.finish(true);
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
job.fail(error);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
cancel() {
|
|
373
|
+
job.finish(true);
|
|
374
|
+
return job.lifetime;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{ highWaterMark: 0 }
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/stream-worker.ts
|
|
382
|
+
function installStreams(scope, options = {}) {
|
|
383
|
+
let ready;
|
|
384
|
+
serveStreams(
|
|
385
|
+
scope,
|
|
386
|
+
async (connection) => {
|
|
387
|
+
ready ??= wasmInit(
|
|
388
|
+
options.wasm === void 0 ? void 0 : { module_or_path: options.wasm }
|
|
389
|
+
);
|
|
390
|
+
await ready;
|
|
391
|
+
return connection.kind === "shared" ? connectShared(connection) : connectApp(connection);
|
|
392
|
+
},
|
|
393
|
+
// Downloads from hosts need WebTransport, which some browsers leave out
|
|
394
|
+
// of service workers. The page then reads objects itself.
|
|
395
|
+
typeof WebTransport === "function"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
async function connectShared({
|
|
399
|
+
indexerUrl,
|
|
400
|
+
seed
|
|
401
|
+
}) {
|
|
402
|
+
const sdk = await SharedSdk.connect(indexerUrl, seed);
|
|
403
|
+
return {
|
|
404
|
+
object: ({ objectId }) => sdk.object(objectId),
|
|
405
|
+
download: (object, range) => sdk.download(object, range),
|
|
406
|
+
free: () => sdk.free()
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function connectApp({
|
|
410
|
+
indexerUrl,
|
|
411
|
+
appKey,
|
|
412
|
+
appMeta
|
|
413
|
+
}) {
|
|
414
|
+
const key = new AppKey(fromHex(appKey));
|
|
415
|
+
const builder = new Builder(indexerUrl, appMeta);
|
|
416
|
+
try {
|
|
417
|
+
const sdk = await builder.connected(key);
|
|
418
|
+
if (!sdk) throw new Error("The app key is not registered with the indexer.");
|
|
419
|
+
return {
|
|
420
|
+
// The page sent the object sealed with this key, so no lookup is needed.
|
|
421
|
+
object: async ({ sealed }) => PinnedObject.open(key, sealed),
|
|
422
|
+
download: (object, range) => sdk.download(object, range),
|
|
423
|
+
free: () => {
|
|
424
|
+
sdk.free();
|
|
425
|
+
key.free();
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
} catch (error) {
|
|
429
|
+
key.free();
|
|
430
|
+
throw error;
|
|
431
|
+
} finally {
|
|
432
|
+
builder.free();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function fromHex(hex) {
|
|
436
|
+
return Uint8Array.from(hex.match(/../g) ?? [], (byte) => parseInt(byte, 16));
|
|
437
|
+
}
|
|
438
|
+
export {
|
|
439
|
+
installStreams
|
|
440
|
+
};
|
|
441
|
+
//# sourceMappingURL=stream-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stream-worker.ts","../src/stream/response.ts","../src/stream/worker.ts"],"sourcesContent":["/**\n * The worker side of streaming, for sites that already run a service worker.\n *\n * A page has only one service worker, so a site with its own calls\n * `installStreams(self)` in it and passes that worker's URL to\n * `enableStreaming`. Every other site serves the prebuilt `sia-storage-sw.js`,\n * which is this function with the WebAssembly built in.\n */\nimport wasmInit, {\n AppKey,\n Builder,\n type InitInput,\n PinnedObject,\n SharedSdk,\n} from '../wasm/sia_storage_wasm.js'\nimport type { Connection } from './stream/protocol'\nimport { serveStreams, type StreamScope, type StreamSdk } from './stream/worker'\n\nexport type { StreamScope }\n\nexport type InstallStreamsOptions = {\n /**\n * The SDK's WebAssembly, as a URL, bytes or compiled module. Only needed\n * when the worker's bundler does not emit `sia_storage_wasm_bg.wasm`.\n */\n wasm?: InitInput\n}\n\nexport function installStreams(\n scope: StreamScope,\n options: InstallStreamsOptions = {},\n) {\n let ready: Promise<unknown> | undefined\n serveStreams(\n scope,\n async (connection) => {\n ready ??= wasmInit(\n options.wasm === undefined ? undefined : { module_or_path: options.wasm },\n )\n await ready\n return connection.kind === 'shared'\n ? connectShared(connection)\n : connectApp(connection)\n },\n // Downloads from hosts need WebTransport, which some browsers leave out\n // of service workers. The page then reads objects itself.\n typeof WebTransport === 'function',\n )\n}\n\nasync function connectShared({\n indexerUrl,\n seed,\n}: Extract<Connection, { kind: 'shared' }>): Promise<StreamSdk> {\n const sdk = await SharedSdk.connect(indexerUrl, seed)\n return {\n object: ({ objectId }) => sdk.object(objectId),\n download: (object, range) => sdk.download(object, range),\n free: () => sdk.free(),\n }\n}\n\nasync function connectApp({\n indexerUrl,\n appKey,\n appMeta,\n}: Extract<Connection, { kind: 'app' }>): Promise<StreamSdk> {\n const key = new AppKey(fromHex(appKey))\n const builder = new Builder(indexerUrl, appMeta)\n try {\n const sdk = await builder.connected(key)\n if (!sdk) throw new Error('The app key is not registered with the indexer.')\n return {\n // The page sent the object sealed with this key, so no lookup is needed.\n object: async ({ sealed }) => PinnedObject.open(key, sealed!),\n download: (object, range) => sdk.download(object, range),\n free: () => {\n sdk.free()\n key.free()\n },\n }\n } catch (error) {\n key.free()\n throw error\n } finally {\n builder.free()\n }\n}\n\n// Uint8Array.fromHex is too new for some browsers that run service workers.\nfunction fromHex(hex: string) {\n return Uint8Array.from(hex.match(/../g) ?? [], (byte) => parseInt(byte, 16))\n}\n","// File bytes come from whoever owns the share, so the browser must never cache\n// them or run them as this origin's content.\nconst PROTECTIVE_HEADERS = {\n 'Cache-Control': 'no-store',\n 'X-Content-Type-Options': 'nosniff',\n 'Content-Security-Policy': \"sandbox; default-src 'none'\",\n 'Referrer-Policy': 'no-referrer',\n}\n\nexport type ByteRange = {\n status: 200 | 206 | 416\n offset: number\n length: number\n}\n\nexport function parseRange(range: string | null, size: number): ByteRange {\n if (!Number.isSafeInteger(size) || size < 0) {\n throw new RangeError('Invalid file size')\n }\n if (range === null) return { status: 200, offset: 0, length: size }\n\n const invalid: ByteRange = { status: 416, offset: 0, length: 0 }\n const match = /^bytes=(\\d*)-(\\d*)$/i.exec(range.trim())\n if (!match || (!match[1] && !match[2]) || size === 0) return invalid\n // Both groups always participate in a match, possibly as empty strings.\n const start = match[1]!\n const end = match[2]!\n\n // Range numerals are not limited to JS's safe integers. Compare before converting.\n const total = BigInt(size)\n let first: bigint\n let last: bigint\n if (!start) {\n const suffix = BigInt(end)\n if (suffix === 0n) return invalid\n first = suffix >= total ? 0n : total - suffix\n last = total - 1n\n } else {\n first = BigInt(start)\n last = end ? BigInt(end) : total - 1n\n if (first >= total || last < first) return invalid\n if (last >= total) last = total - 1n\n }\n return {\n status: 206,\n offset: Number(first),\n length: Number(last - first + 1n),\n }\n}\n\nexport function contentDisposition(name: string, download: boolean) {\n // TextEncoder replaces lone surrogates without requiring toWellFormed(),\n // which older browsers with module service workers do not implement.\n const wellFormed = new TextDecoder().decode(new TextEncoder().encode(name))\n const filename =\n wellFormed\n // Filenames must not contain header controls or path separators.\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f\\u007f/\\\\]/g, '_')\n .trim() || 'download'\n const fallback = filename.replace(/[^\\u0020-\\u007e]|[\";]/g, '_')\n const encoded = encodeURIComponent(filename).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n )\n return `${download ? 'attachment' : 'inline'}; filename=\"${fallback}\"; filename*=UTF-8''${encoded}`\n}\n\nexport function responseHeaders(\n metadata: { name: string; mime: string; size: number },\n range: ByteRange,\n download: boolean,\n) {\n // Only use a media type, never arbitrary header syntax from shared metadata.\n const mime = (metadata.mime.split(';', 1)[0] ?? '').trim()\n const headers = new Headers({\n 'Accept-Ranges': 'bytes',\n 'Content-Length': String(range.length),\n 'Content-Type': /^[\\w!#$&^.+-]+\\/[\\w!#$&^.+-]+$/.test(mime)\n ? mime\n : 'application/octet-stream',\n 'Content-Disposition': contentDisposition(metadata.name, download),\n ...PROTECTIVE_HEADERS,\n })\n if (range.status === 206) {\n headers.set(\n 'Content-Range',\n `bytes ${range.offset}-${range.offset + range.length - 1}/${metadata.size}`,\n )\n } else if (range.status === 416) {\n headers.set('Content-Range', `bytes */${metadata.size}`)\n }\n return headers\n}\n\nexport function errorResponse(status: number) {\n const headers = new Headers(PROTECTIVE_HEADERS)\n if (status === 405) headers.set('Allow', 'GET, HEAD')\n return new Response(null, { status, headers })\n}\n","import type { PinnedObject } from '../../wasm/sia_storage_wasm.js'\nimport { streamErrorMessage } from './errors'\nimport {\n type ClientReply,\n isSourceReply,\n type PageMessage,\n parseStreamPath,\n requestReply,\n type Connection,\n type SourceDetails,\n type WorkerMessage,\n} from './protocol'\nimport {\n type ByteRange,\n errorResponse,\n parseRange,\n responseHeaders,\n} from './response'\n\nconst SOURCE_TIMEOUT = 5_000\nconst IDLE_TIMEOUT = 5 * 60_000\n\n/** An object as the page describes it. See `SourceDetails`. */\nexport type ObjectRef = Pick<SourceDetails, 'objectId' | 'sealed'>\n\n/** A connected SDK of either kind, as the worker uses it. */\nexport type StreamSdk = {\n object(ref: ObjectRef): Promise<PinnedObject>\n download(\n object: PinnedObject,\n range: { offset: number; length: number },\n ): ReadableStream\n free(): void\n}\n\n// The parts of ServiceWorkerGlobalScope this module uses, written out so it\n// type-checks alongside page code, whose DOM types conflict with the service\n// worker ones. A real ServiceWorkerGlobalScope satisfies them.\ntype StreamClient = { id: string; postMessage(message: unknown, transfer?: Transferable[]): void }\ntype WaitUntil = { waitUntil(promise: Promise<unknown>): void }\ntype StreamFetchEvent = { request: Request; respondWith(response: Promise<Response>): void }\ntype StreamMessageEvent = WaitUntil & {\n data: unknown\n source: unknown\n ports: readonly MessagePort[]\n}\nexport type StreamScope = {\n location: { origin: string }\n skipWaiting(): Promise<void>\n clients: {\n get(id: string): Promise<StreamClient | undefined>\n claim(): Promise<void>\n }\n addEventListener(type: 'install' | 'activate', listener: (event: WaitUntil) => void): void\n addEventListener(type: 'message', listener: (event: StreamMessageEvent) => void): void\n addEventListener(type: 'fetch', listener: (event: StreamFetchEvent) => void): void\n}\ntype Metadata = Pick<\n SourceDetails,\n 'objectId' | 'sealed' | 'name' | 'mime' | 'size'\n>\ntype Chunks = ReadableStreamDefaultReader<Uint8Array>\n\n/** One page's open share, and the SDK the worker connects for it on first use. */\ntype Session = {\n key: string\n connection: Connection\n sources: Set<Source>\n objects: Map<string, Promise<PinnedObject>>\n // One `Job.lifetime` per request in flight.\n jobs: Set<Promise<void>>\n closed: boolean\n timer?: ReturnType<typeof setTimeout>\n sdk?: Promise<StreamSdk>\n}\n\n/** One stream token, resolved to its file by asking the page that made it. */\ntype Source = {\n key: string\n client: StreamClient\n closed: boolean\n // The `abort` of each job serving this token.\n requests: Set<() => void>\n ready: Promise<Metadata>\n setup: AbortController\n session?: Session\n}\n\n/** One HTTP request for a stream URL, from the first await to its last byte. */\ntype Job = {\n closed: boolean\n lifetime: Promise<void>\n source?: Source\n session?: Session\n range?: ByteRange\n reader?: Chunks\n controller?: ReadableStreamDefaultController<Uint8Array>\n finish(cancel?: boolean): void\n abort(): void\n fail(error: unknown): void\n}\n\n// The control port carries credentials and metadata only. Bytes never leave\n// this worker, since each response consumes the SDK's own stream with native\n// backpressure.\nexport function serveStreams(\n scope: StreamScope,\n connect: (connection: Connection) => Promise<StreamSdk>,\n supportsDownloads: boolean,\n) {\n const sources = new Map<string, Source>()\n const sessions = new Map<string, Session>()\n // Released session keys. A page reply already in flight when its share is\n // released must not bring the session back.\n const released = new Set<string>()\n const key = (clientId: string, id: string) => JSON.stringify([clientId, id])\n\n function idle(session: Session) {\n clearTimeout(session.timer)\n if (!session.closed && session.jobs.size === 0) {\n session.timer = setTimeout(\n () => void disposeSession(session),\n IDLE_TIMEOUT,\n )\n }\n }\n\n function sessionFor(clientId: string, details: SourceDetails) {\n const sessionKey = key(clientId, details.session)\n let session = sessions.get(sessionKey)\n if (!session) {\n session = {\n key: sessionKey,\n connection: details.connection,\n sources: new Set(),\n objects: new Map(),\n jobs: new Set(),\n closed: false,\n }\n sessions.set(sessionKey, session)\n }\n return session\n }\n\n async function disposeSession(session: Session) {\n if (session.closed) return\n session.closed = true\n sessions.delete(session.key)\n clearTimeout(session.timer)\n for (const source of session.sources) dropSource(source)\n // Do not free wasm handles while a connect, lookup or cancellation uses them.\n await Promise.allSettled(session.jobs)\n await Promise.allSettled(\n [...session.objects.values()].map(async (object) =>\n (await object).free(),\n ),\n )\n try {\n ;(await session.sdk)?.free()\n } catch {\n // Connection failures are reported by the request, not during disposal.\n }\n }\n\n // A failed connect or lookup is forgotten, so the next request retries it\n // instead of failing with the cached error.\n function sdkFor(session: Session) {\n if (session.sdk) return session.sdk\n const { connection } = session\n const connecting = Promise.resolve().then(() => connect(connection))\n session.sdk = connecting\n connecting.catch(() => {\n if (session.sdk === connecting) delete session.sdk\n })\n return connecting\n }\n\n function objectFor(session: Session, sdk: StreamSdk, ref: ObjectRef) {\n const { objectId } = ref\n const cached = session.objects.get(objectId)\n if (cached) return cached\n const lookup = Promise.resolve().then(() => sdk.object(ref))\n session.objects.set(objectId, lookup)\n lookup.catch(() => {\n if (session.objects.get(objectId) === lookup) {\n session.objects.delete(objectId)\n }\n })\n return lookup\n }\n\n function getSource(client: StreamClient, token: string) {\n const sourceKey = key(client.id, token)\n const cached = sources.get(sourceKey)\n if (cached) return cached\n const setup = new AbortController()\n const request = { type: 'sia-source', token } satisfies WorkerMessage\n const ready = requestReply(client, request, {\n timeout: SOURCE_TIMEOUT,\n signal: setup.signal,\n }).then((reply) => {\n if (!isSourceReply(reply)) {\n throw new Error('Shared file source unavailable.')\n }\n if (source.closed || released.has(key(client.id, reply.session))) {\n throw new DOMException('Released', 'AbortError')\n }\n const session = sessionFor(client.id, reply)\n source.session = session\n session.sources.add(source)\n idle(session)\n // Keep credentials in the session record, never in response metadata.\n const { objectId, sealed, name, mime, size } = reply\n return { objectId, sealed, name, mime, size }\n })\n const source: Source = {\n key: sourceKey,\n client,\n closed: false,\n requests: new Set(),\n ready,\n setup,\n }\n sources.set(sourceKey, source)\n return source\n }\n\n function dropSource(source: Source) {\n if (source.closed) return\n source.closed = true\n sources.delete(source.key)\n source.setup.abort(new DOMException('Released', 'AbortError'))\n for (const cancel of source.requests) cancel()\n source.session?.sources.delete(source)\n }\n\n /**\n * Tracks one request until it settles, whichever way that happens: its last\n * byte, the page aborting it, its source being released, or an SDK failure.\n */\n function startJob(request: Request, token: string): Job {\n let complete = () => {}\n // Kept out of event.waitUntil. Chromium kills a worker whose event runs\n // past 5 minutes, which would end any download still running at that mark.\n const lifetime = new Promise<void>((resolve) => {\n complete = resolve\n })\n const job: Job = {\n closed: false,\n lifetime,\n finish(cancel = false) {\n if (job.closed) return\n job.closed = true\n request.signal.removeEventListener('abort', job.abort)\n const { reader } = job\n const cancellation =\n cancel && reader ? reader.cancel().catch(() => {}) : undefined\n void Promise.resolve(cancellation).finally(() => {\n reader?.releaseLock()\n job.source?.requests.delete(job.abort)\n job.session?.jobs.delete(lifetime)\n if (job.session) idle(job.session)\n complete()\n })\n },\n abort() {\n if (job.closed) return\n job.controller?.error(new DOMException('Aborted', 'AbortError'))\n job.finish(true)\n },\n fail(error) {\n if (job.closed) return\n const message = streamErrorMessage(error)\n const aborted = error instanceof Error && error.name === 'AbortError'\n const { source, range } = job\n if (!aborted && source && !source.closed) {\n // Diagnostics go only to the owning page. Never log SDK objects here.\n try {\n source.client.postMessage({\n type: 'sia-stream-error',\n token,\n message,\n ...(range && { offset: range.offset, length: range.length }),\n } satisfies WorkerMessage)\n } catch {\n // The owning tab may already be gone.\n }\n }\n job.controller?.error(new Error(message))\n job.finish(true)\n },\n }\n return job\n }\n\n async function serve(\n request: Request,\n clientId: string,\n token: string,\n download: boolean,\n ): Promise<Response> {\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return errorResponse(405)\n }\n if (!supportsDownloads) return errorResponse(503)\n const job = startJob(request, token)\n try {\n const client = await scope.clients.get(clientId)\n if (!client || request.signal.aborted) return errorResponse(404)\n const source = getSource(client, token)\n job.source = source\n source.requests.add(job.abort)\n request.signal.addEventListener('abort', job.abort, { once: true })\n const metadata = await source.ready\n if (job.closed) return errorResponse(502)\n // Set by `ready` before it resolves.\n const session = source.session!\n job.session = session\n session.jobs.add(job.lifetime)\n clearTimeout(session.timer)\n const range = parseRange(\n request.method === 'HEAD' ? null : request.headers.get('range'),\n metadata.size,\n )\n job.range = range\n const headers = responseHeaders(metadata, range, download)\n if (\n request.method === 'HEAD' ||\n range.status === 416 ||\n range.length === 0\n ) {\n job.finish()\n return new Response(null, { status: range.status, headers })\n }\n const sdk = await sdkFor(session)\n if (job.closed) return errorResponse(502)\n const object = await objectFor(session, sdk, metadata)\n if (job.closed) return errorResponse(502)\n const chunks: Chunks = sdk\n .download(object, { offset: range.offset, length: range.length })\n .getReader()\n job.reader = chunks\n return new Response(rangeBody(job, chunks, range.length), {\n status: range.status,\n headers,\n })\n } catch (error) {\n job.fail(error)\n if (job.source && !job.source.session) dropSource(job.source)\n return errorResponse(502)\n }\n }\n\n scope.addEventListener('install', (event) =>\n event.waitUntil(scope.skipWaiting()),\n )\n scope.addEventListener('activate', (event) =>\n event.waitUntil(scope.clients.claim()),\n )\n scope.addEventListener('message', (event) => {\n const source = event.source as Partial<StreamClient> | null\n const clientId = typeof source?.id === 'string' ? source.id : undefined\n if (!clientId) return\n const data = event.data as PageMessage | undefined\n if (data?.type === 'sia-client') {\n const reply: ClientReply = { clientId, supported: supportsDownloads }\n event.ports[0]?.postMessage(reply)\n event.ports[0]?.close()\n } else if (data?.type === 'sia-release' && typeof data.token === 'string') {\n // The session outlives its last source until IDLE_TIMEOUT, so moving to\n // the next file reuses the connected SDK.\n const source = sources.get(key(clientId, data.token))\n if (source) dropSource(source)\n } else if (\n data?.type === 'sia-release-share' &&\n typeof data.session === 'string'\n ) {\n const sessionKey = key(clientId, data.session)\n released.add(sessionKey)\n const session = sessions.get(sessionKey)\n if (session) event.waitUntil(disposeSession(session))\n }\n })\n scope.addEventListener('fetch', (event) => {\n const url = new URL(event.request.url)\n if (url.origin !== scope.location.origin) return\n const stream = parseStreamPath(url.pathname)\n if (!stream) return\n const download = url.searchParams.get('download') === '1'\n event.respondWith(\n serve(event.request, stream.clientId, stream.token, download),\n )\n })\n}\n\n/**\n * Exactly `length` bytes from `chunks`, or an error if the SDK stream ends\n * early or runs over. Each pull reads one chunk, so backpressure reaches the SDK.\n */\nfunction rangeBody(job: Job, chunks: Chunks, length: number) {\n let remaining = length\n return new ReadableStream<Uint8Array>(\n {\n start(controller) {\n job.controller = controller\n },\n async pull(controller) {\n try {\n const { done, value } = await chunks.read()\n if (job.closed) return\n if (done) {\n if (remaining !== 0) {\n throw new Error(\n 'Shared file stream ended before all bytes arrived.',\n )\n }\n controller.close()\n job.finish()\n return\n }\n remaining -= value.byteLength\n if (remaining < 0) {\n throw new Error('Shared file stream exceeded the requested length.')\n }\n controller.enqueue(value)\n if (remaining === 0) {\n controller.close()\n job.finish(true)\n }\n } catch (error) {\n job.fail(error)\n }\n },\n cancel() {\n job.finish(true)\n return job.lifetime\n },\n },\n { highWaterMark: 0 },\n )\n}\n"],"mappings":"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","names":["source"]}
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type Middleware = (request: {
|
|
2
|
+
url?: string;
|
|
3
|
+
}, response: {
|
|
4
|
+
setHeader(name: string, value: string): void;
|
|
5
|
+
end(body: Uint8Array): void;
|
|
6
|
+
}, next: () => void) => void;
|
|
7
|
+
type Server = {
|
|
8
|
+
config: {
|
|
9
|
+
base: string;
|
|
10
|
+
};
|
|
11
|
+
middlewares: {
|
|
12
|
+
use(middleware: Middleware): void;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
type Config = {
|
|
16
|
+
build: {
|
|
17
|
+
ssr?: boolean | string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
type BuildContext = {
|
|
21
|
+
emitFile(file: {
|
|
22
|
+
type: 'asset';
|
|
23
|
+
fileName: string;
|
|
24
|
+
source: Uint8Array;
|
|
25
|
+
}): string;
|
|
26
|
+
};
|
|
27
|
+
declare function siaStorage(): {
|
|
28
|
+
name: string;
|
|
29
|
+
config: () => {
|
|
30
|
+
optimizeDeps: {
|
|
31
|
+
exclude: string[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
configResolved(config: Config): void;
|
|
35
|
+
configureServer(server: Server): void;
|
|
36
|
+
generateBundle(this: BuildContext): void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export { siaStorage };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/vite.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
var WORKER_FILE = "sia-storage-sw.js";
|
|
4
|
+
function siaStorage() {
|
|
5
|
+
const source = () => readFileSync(new URL(`./${WORKER_FILE}`, import.meta.url));
|
|
6
|
+
let serverBuild = false;
|
|
7
|
+
return {
|
|
8
|
+
name: "sia-storage",
|
|
9
|
+
config: () => ({
|
|
10
|
+
optimizeDeps: { exclude: ["@siafoundation/sia-storage"] }
|
|
11
|
+
}),
|
|
12
|
+
configResolved(config) {
|
|
13
|
+
serverBuild = Boolean(config.build.ssr);
|
|
14
|
+
},
|
|
15
|
+
configureServer(server) {
|
|
16
|
+
server.middlewares.use((request, response, next) => {
|
|
17
|
+
const path = request.url?.split("?")[0];
|
|
18
|
+
if (path !== `${server.config.base}${WORKER_FILE}`) return next();
|
|
19
|
+
response.setHeader("Content-Type", "text/javascript");
|
|
20
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
21
|
+
response.end(source());
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
generateBundle() {
|
|
25
|
+
if (serverBuild) return;
|
|
26
|
+
this.emitFile({ type: "asset", fileName: WORKER_FILE, source: source() });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
siaStorage
|
|
32
|
+
};
|