Files
gods-eye-view/src/director/packs/source.js
T

64 lines
1.9 KiB
JavaScript

import { PACK_LIMITS, validateAssetPath } from './manifest.js';
/** Register a fixed asset directory. Scene files cannot replace its origin or escape its path. */
export function createAssetDirectorySource({
baseUrl,
fetchImpl = globalThis.fetch,
}) {
const base = new URL(baseUrl);
if (
!['https:', 'http:'].includes(base.protocol) ||
base.username ||
base.password ||
base.search ||
base.hash ||
!base.pathname.endsWith('/')
)
throw new TypeError(
'Asset source requires an explicit HTTP(S) directory URL',
);
return async ({ path, signal, maxBytes = PACK_LIMITS.bytes }) => {
validateAssetPath(path);
const response = await fetchImpl(new URL(path, base).href, {
signal,
credentials: 'omit',
redirect: 'error',
referrerPolicy: 'no-referrer',
cache: 'no-store',
});
if (!response.ok) throw new Error('Asset unavailable');
const reader = response.body?.getReader();
if (!reader) throw new Error('Asset stream unavailable');
const chunks = [];
let length = 0;
try {
if (Number(response.headers.get('content-length')) > maxBytes)
throw new Error('Asset exceeds byte limit');
for (;;) {
signal?.throwIfAborted();
const { done, value } = await reader.read();
if (done) break;
length += value.byteLength;
if (length > maxBytes) throw new Error('Asset exceeds byte limit');
chunks.push(value);
}
} finally {
await reader.cancel().catch(() => {});
reader.releaseLock();
}
const bytes = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return {
bytes,
mimeType: (response.headers.get('content-type') || '')
.split(';')[0]
.trim()
.toLowerCase(),
};
};
}