silphius/app/util/resources.server.js
2024-09-28 10:22:16 -05:00

115 lines
3.3 KiB
JavaScript

import {createHash} from 'node:crypto';
import {readFile, realpath} from 'node:fs/promises';
import chokidar from 'chokidar';
import {glob} from 'glob';
import {singleton} from './singleton.js';
const {
RESOURCES_PATH = [process.cwd(), 'resources'].join('/'),
} = process.env;
const resources = singleton('resources', {});
const manifest = singleton('manifest', {});
const RESOURCES_PATH_REAL = await realpath(RESOURCES_PATH);
const RESOURCES_GLOB = [RESOURCES_PATH_REAL, '**', '*'].join('/');
async function computeAsset(path) {
const buffer = await readFile(path);
return {
asset: buffer,
hash: createHash('sha1').update(buffer).digest(),
path: path.slice(RESOURCES_PATH_REAL.length + 1),
};
}
async function assetPaths() {
return glob(RESOURCES_GLOB, {nodir: true});
}
export async function createManifest() {
const paths = await assetPaths();
const entries = await Promise.all(paths.map(async (path) => {
const asset = await computeAsset(path);
return [asset.path, asset.hash.toString('hex')];
}))
return Object.fromEntries(
entries.filter(Boolean),
);
}
export async function loadManifest() {
emptyCheck: {
for (const path in manifest) { // eslint-disable-line no-unused-vars
break emptyCheck;
}
const created = await createManifest();
for (const path in created) {
manifest[path] = created[path];
}
const watcher = chokidar.watch(RESOURCES_GLOB);
watcher.on('add', async (path) => {
const asset = await computeAsset(path);
manifest[asset.path] = asset.hash.toString('hex');
resources[asset.path] = {asset: asset.asset.buffer, hash: asset.hash.buffer};
});
watcher.on('change', async (path) => {
const asset = await computeAsset(path);
manifest[asset.path] = asset.hash.toString('hex');
resources[asset.path] = {asset: asset.asset.buffer, hash: asset.hash.buffer};
});
watcher.on('unlink', async (path) => {
const assetPath = path.slice(RESOURCES_PATH_REAL.length + 1);
delete resources[assetPath];
delete manifest[assetPath];
});
}
return manifest;
}
export async function encodeResources(resources) {
let byteLength = 4;
for (const {asset} of resources) {
byteLength += 20;
byteLength += 4;
byteLength += asset.byteLength;
}
const buffer = new ArrayBuffer(byteLength);
const view = new DataView(buffer);
let caret = 0;
view.setUint32(caret, resources.length);
caret += 4;
for (const {asset, hash} of resources) {
const hashView = new DataView(hash);
for (let j = 0; j < 20; ++j) {
view.setUint8(caret, hashView.getUint8(j));
caret += 1;
}
view.setUint32(caret, asset.byteLength);
caret += 4;
const assetView = new DataView(asset);
for (let j = 0; j < asset.byteLength; ++j) {
view.setUint8(caret, assetView.getUint8(j));
caret += 1;
}
}
return buffer;
}
export async function loadResources() {
emptyCheck: {
for (const path in resources) { // eslint-disable-line no-unused-vars
break emptyCheck;
}
const paths = await assetPaths();
await Promise.all(
paths.map(async (path) => {
const {asset, hash, path: assetPath} = await computeAsset(path);
resources[assetPath] = {asset: asset.buffer, hash: hash.buffer};
}),
);
}
return resources;
}