refactor: resources
This commit is contained in:
parent
b20795137e
commit
9d176c2930
|
@ -64,6 +64,10 @@ function handleBotRequest(
|
||||||
const stream = createReadableStreamFromReadable(body);
|
const stream = createReadableStreamFromReadable(body);
|
||||||
|
|
||||||
responseHeaders.set("Content-Type", "text/html");
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
// security
|
||||||
|
responseHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||||
|
responseHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||||
|
responseHeaders.set("Cross-Origin-Resource-Policy", "same-site");
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
new Response(stream, {
|
new Response(stream, {
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import {LRUCache} from 'lru-cache';
|
|
||||||
|
|
||||||
import Ecs from '@/ecs/ecs.js';
|
import Ecs from '@/ecs/ecs.js';
|
||||||
import {withResolvers} from '@/util/promise.js';
|
import {readAsset} from '@/util/resources.js';
|
||||||
|
|
||||||
const cache = new LRUCache({
|
|
||||||
max: 128,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class ClientEcs extends Ecs {
|
export default class ClientEcs extends Ecs {
|
||||||
constructor(specification) {
|
constructor(specification) {
|
||||||
|
@ -19,16 +13,10 @@ export default class ClientEcs extends Ecs {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async readAsset(uri) {
|
async readAsset(path) {
|
||||||
if (!cache.has(uri)) {
|
const resource = await readAsset(path);
|
||||||
const {promise, resolve, reject} = withResolvers();
|
return resource
|
||||||
cache.set(uri, promise);
|
? resource
|
||||||
fetch(new URL(uri, location.origin))
|
: new ArrayBuffer(0);
|
||||||
.then(async (response) => {
|
|
||||||
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
}
|
|
||||||
return cache.get(uri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {RESOLUTION} from '@/util/constants.js';
|
||||||
import EventEmitter from '@/util/event-emitter.js';
|
import EventEmitter from '@/util/event-emitter.js';
|
||||||
|
|
||||||
import addKeyListener from './add-key-listener.js';
|
import addKeyListener from './add-key-listener.js';
|
||||||
import ClientEcs from './client-ecs.js';
|
|
||||||
import Disconnected from './dom/disconnected.jsx';
|
import Disconnected from './dom/disconnected.jsx';
|
||||||
import Chat from './dom/chat/chat.jsx';
|
import Chat from './dom/chat/chat.jsx';
|
||||||
import Bag from './dom/bag.jsx';
|
import Bag from './dom/bag.jsx';
|
||||||
|
@ -49,8 +48,6 @@ function Ui({disconnected}) {
|
||||||
const [inventorySlots, setInventorySlots] = useState(emptySlots());
|
const [inventorySlots, setInventorySlots] = useState(emptySlots());
|
||||||
const [activeSlot, setActiveSlot] = useState(0);
|
const [activeSlot, setActiveSlot] = useState(0);
|
||||||
const [scale, setScale] = useState(2);
|
const [scale, setScale] = useState(2);
|
||||||
const [Components, setComponents] = useState();
|
|
||||||
const [Systems, setSystems] = useState();
|
|
||||||
const monopolizers = useRef([]);
|
const monopolizers = useRef([]);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [chatIsOpen, setChatIsOpen] = useState(false);
|
const [chatIsOpen, setChatIsOpen] = useState(false);
|
||||||
|
@ -64,24 +61,6 @@ function Ui({disconnected}) {
|
||||||
const [externalInventory, setExternalInventory] = useState();
|
const [externalInventory, setExternalInventory] = useState();
|
||||||
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
||||||
const [particleWorker, setParticleWorker] = useState();
|
const [particleWorker, setParticleWorker] = useState();
|
||||||
const refreshEcs = useCallback(() => {
|
|
||||||
class ClientEcsPerf extends ClientEcs {
|
|
||||||
markChange() {}
|
|
||||||
}
|
|
||||||
ecsRef.current = new ClientEcsPerf({Components, Systems});
|
|
||||||
}, [ecsRef, Components, Systems]);
|
|
||||||
useEffect(() => {
|
|
||||||
async function setEcsStuff() {
|
|
||||||
const {default: Components} = await import('@/ecs/components/index.js');
|
|
||||||
const {default: Systems} = await import('@/ecs/systems/index.js');
|
|
||||||
setComponents(Components);
|
|
||||||
setSystems(Systems);
|
|
||||||
}
|
|
||||||
setEcsStuff();
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
refreshEcs();
|
|
||||||
}, [refreshEcs]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let handle;
|
let handle;
|
||||||
if (disconnected) {
|
if (disconnected) {
|
||||||
|
@ -238,10 +217,11 @@ function Ui({disconnected}) {
|
||||||
});
|
});
|
||||||
}, [client, keepHotbarOpen, setDebug]);
|
}, [client, keepHotbarOpen, setDebug]);
|
||||||
const onEcsChangePacket = useCallback(() => {
|
const onEcsChangePacket = useCallback(() => {
|
||||||
refreshEcs();
|
|
||||||
mainEntityRef.current = undefined;
|
mainEntityRef.current = undefined;
|
||||||
monopolizers.current = [];
|
monopolizers.current = [];
|
||||||
}, [refreshEcs, mainEntityRef]);
|
}, [
|
||||||
|
mainEntityRef,
|
||||||
|
]);
|
||||||
usePacket('EcsChange', onEcsChangePacket);
|
usePacket('EcsChange', onEcsChangePacket);
|
||||||
const onTickPacket = useCallback(async (payload, client) => {
|
const onTickPacket = useCallback(async (payload, client) => {
|
||||||
if (0 === Object.keys(payload.ecs).length) {
|
if (0 === Object.keys(payload.ecs).length) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {json} from "@remix-run/node";
|
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {useOutletContext, useParams} from 'react-router-dom';
|
import {useOutletContext, useParams} from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -10,13 +9,9 @@ import EcsContext from '@/react/context/ecs.js';
|
||||||
import MainEntityContext from '@/react/context/main-entity.js';
|
import MainEntityContext from '@/react/context/main-entity.js';
|
||||||
import RadiansContext from '@/react/context/radians.js';
|
import RadiansContext from '@/react/context/radians.js';
|
||||||
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||||
import {juggleSession} from '@/server/session.server.js';
|
|
||||||
import {TAU} from '@/util/math.js';
|
import {TAU} from '@/util/math.js';
|
||||||
|
|
||||||
export async function loader({request}) {
|
import ClientEcs from '@/react/components/client-ecs.js';
|
||||||
await juggleSession(request);
|
|
||||||
return json({});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PlaySpecific() {
|
export default function PlaySpecific() {
|
||||||
const Client = useOutletContext();
|
const Client = useOutletContext();
|
||||||
|
@ -24,6 +19,8 @@ export default function PlaySpecific() {
|
||||||
const [client, setClient] = useState();
|
const [client, setClient] = useState();
|
||||||
const mainEntityRef = useRef();
|
const mainEntityRef = useRef();
|
||||||
const debugTuple = useState(false);
|
const debugTuple = useState(false);
|
||||||
|
const [Components, setComponents] = useState();
|
||||||
|
const [Systems, setSystems] = useState();
|
||||||
const ecsRef = useRef();
|
const ecsRef = useRef();
|
||||||
const [disconnected, setDisconnected] = useState(false);
|
const [disconnected, setDisconnected] = useState(false);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
@ -64,6 +61,36 @@ export default function PlaySpecific() {
|
||||||
removeEventListener('beforeunload', onBeforeUnload);
|
removeEventListener('beforeunload', onBeforeUnload);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const refreshEcs = useCallback(() => {
|
||||||
|
class ClientEcsPerf extends ClientEcs {
|
||||||
|
markChange() {}
|
||||||
|
}
|
||||||
|
ecsRef.current = new ClientEcsPerf({Components, Systems});
|
||||||
|
}, [ecsRef, Components, Systems]);
|
||||||
|
useEffect(() => {
|
||||||
|
async function setEcsStuff() {
|
||||||
|
const {default: Components} = await import('@/ecs/components/index.js');
|
||||||
|
const {default: Systems} = await import('@/ecs/systems/index.js');
|
||||||
|
setComponents(Components);
|
||||||
|
setSystems(Systems);
|
||||||
|
}
|
||||||
|
setEcsStuff();
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
refreshEcs();
|
||||||
|
}, [refreshEcs]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onEcsChangePacket() {
|
||||||
|
refreshEcs();
|
||||||
|
}
|
||||||
|
client.addPacketListener('EcsChange', onEcsChangePacket);
|
||||||
|
return () => {
|
||||||
|
client.removePacketListener('EcsChange', onEcsChangePacket);
|
||||||
|
};
|
||||||
|
}, [client, refreshEcs]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,12 +1,66 @@
|
||||||
|
import {settings} from '@pixi/core';
|
||||||
|
import {json, useLoaderData} from "@remix-run/react";
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {Outlet, useParams} from 'react-router-dom';
|
import {Outlet, useParams} from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computeMissing,
|
||||||
|
fetchResources,
|
||||||
|
get,
|
||||||
|
readAsset,
|
||||||
|
set,
|
||||||
|
} from '@/util/resources.js';
|
||||||
|
|
||||||
import styles from './play.module.css';
|
import styles from './play.module.css';
|
||||||
|
|
||||||
|
settings.ADAPTER.fetch = async (path) => {
|
||||||
|
const resource = await readAsset(path);
|
||||||
|
return resource ? new Response(resource) : new Response(undefined, {status: 404});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loader({request}) {
|
||||||
|
const {juggleSession} = await import('@/server/session.server.js');
|
||||||
|
const {loadManifest} = await import('@/util/resources.server.js');
|
||||||
|
await juggleSession(request);
|
||||||
|
return json({
|
||||||
|
manifest: await loadManifest(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function Play() {
|
export default function Play() {
|
||||||
|
const {manifest} = useLoaderData();
|
||||||
const [Client, setClient] = useState();
|
const [Client, setClient] = useState();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [type] = params['*'].split('/');
|
const [type] = params['*'].split('/');
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const {signal} = controller;
|
||||||
|
async function receiveResources() {
|
||||||
|
const current = await get();
|
||||||
|
const paths = await computeMissing(current, manifest);
|
||||||
|
if (paths.length > 0 && !signal.aborted) {
|
||||||
|
try {
|
||||||
|
const resources = await fetchResources(paths, {signal});
|
||||||
|
if (resources) {
|
||||||
|
for (const key in resources) {
|
||||||
|
current[key] = resources[key];
|
||||||
|
}
|
||||||
|
await set(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if ((e instanceof DOMException) && 'AbortError' === e.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiveResources();
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [manifest]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadClient() {
|
async function loadClient() {
|
||||||
let Client;
|
let Client;
|
||||||
|
|
8
app/routes/resources.stream/route.jsx
Normal file
8
app/routes/resources.stream/route.jsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {encodeResources, loadResources} from '@/util/resources.server.js';
|
||||||
|
|
||||||
|
export async function action({request}) {
|
||||||
|
const paths = await request.json();
|
||||||
|
const assets = await loadResources();
|
||||||
|
const buffer = await encodeResources(paths.map((path) => assets[path]));
|
||||||
|
return new Response(buffer);
|
||||||
|
}
|
|
@ -8,7 +8,9 @@ import {getSession} from '@/server/session.server.js';
|
||||||
|
|
||||||
import Engine from './engine.js';
|
import Engine from './engine.js';
|
||||||
|
|
||||||
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
|
const {
|
||||||
|
RESOURCES_PATH = [process.cwd(), 'resources'].join('/'),
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
global.__silphiusWebsocket = null;
|
global.__silphiusWebsocket = null;
|
||||||
|
|
||||||
|
@ -20,13 +22,18 @@ class SocketServer extends Server {
|
||||||
return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
|
return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
|
||||||
}
|
}
|
||||||
async readAsset(path) {
|
async readAsset(path) {
|
||||||
const url = new URL(path, 'https://localhost:3000')
|
const {pathname} = new URL(path, 'http://example.org');
|
||||||
if (isInsecure) {
|
const resourcePath = pathname.slice('/resources/'.length);
|
||||||
url.protocol = 'http:';
|
try {
|
||||||
|
const {buffer} = await readFile([RESOURCES_PATH, resourcePath].join('/'));
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if ('ENOENT' !== error.code) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return new ArrayBuffer(0);
|
||||||
}
|
}
|
||||||
return fetch(url.href).then((response) => (
|
|
||||||
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
async readData(path) {
|
async readData(path) {
|
||||||
const qualified = this.constructor.qualify(path);
|
const qualified = this.constructor.qualify(path);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {del, get, set} from 'idb-keyval';
|
||||||
import {encode} from '@/net/packets/index.js';
|
import {encode} from '@/net/packets/index.js';
|
||||||
import Server from '@/net/server.js';
|
import Server from '@/net/server.js';
|
||||||
import {withResolvers} from '@/util/promise.js';
|
import {withResolvers} from '@/util/promise.js';
|
||||||
|
import {readAsset} from '@/util/resources.js';
|
||||||
|
|
||||||
import createEcs from './create/ecs.js';
|
import createEcs from './create/ecs.js';
|
||||||
import './create/forest.js';
|
import './create/forest.js';
|
||||||
|
@ -21,9 +22,10 @@ class WorkerServer extends Server {
|
||||||
return ['UNIVERSE', path].join('/');
|
return ['UNIVERSE', path].join('/');
|
||||||
}
|
}
|
||||||
async readAsset(path) {
|
async readAsset(path) {
|
||||||
return fetch(path).then((response) => (
|
const resource = await readAsset(path);
|
||||||
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
|
return resource
|
||||||
));
|
? resource
|
||||||
|
: new ArrayBuffer(0);
|
||||||
}
|
}
|
||||||
async readData(path) {
|
async readData(path) {
|
||||||
const data = await get(this.constructor.qualify(path));
|
const data = await get(this.constructor.qualify(path));
|
||||||
|
|
81
app/util/resources.js
Normal file
81
app/util/resources.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
export async function computeMissing(current, manifest) {
|
||||||
|
const missing = [];
|
||||||
|
for (const path in manifest) {
|
||||||
|
if (!current || !current[path] || current[path].hash !== manifest[path]) {
|
||||||
|
missing.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const octets = [];
|
||||||
|
for (let i = 0; i < 256; ++i) {
|
||||||
|
octets.push(i.toString(16).padStart(2, '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseResources(resources, buffer) {
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
let caret = 0;
|
||||||
|
const count = view.getUint32(caret);
|
||||||
|
caret += 4;
|
||||||
|
const manifest = {};
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
let hash = '';
|
||||||
|
for (let j = 0; j < 20; ++j) {
|
||||||
|
hash += octets[view.getUint8(caret)]
|
||||||
|
caret += 1;
|
||||||
|
}
|
||||||
|
const byteLength = view.getUint32(caret);
|
||||||
|
caret += 4;
|
||||||
|
const asset = new ArrayBuffer(byteLength);
|
||||||
|
const assetView = new DataView(asset);
|
||||||
|
for (let j = 0; j < asset.byteLength; ++j) {
|
||||||
|
assetView.setUint8(j, view.getUint8(caret));
|
||||||
|
caret += 1;
|
||||||
|
}
|
||||||
|
manifest[resources[i]] = {asset, hash};
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchResources(paths, {signal} = {}) {
|
||||||
|
const response = await fetch('/resources/stream', {
|
||||||
|
body: JSON.stringify(paths),
|
||||||
|
method: 'post',
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (signal.aborted) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return parseResources(paths, await response.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Cache {
|
||||||
|
async get() {
|
||||||
|
const {get} = await import('idb-keyval');
|
||||||
|
const resources = await get('$$silphius_resources');
|
||||||
|
return resources || {};
|
||||||
|
}
|
||||||
|
async set(manifest) {
|
||||||
|
const {set} = await import('idb-keyval');
|
||||||
|
return set('$$silphius_resources', manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
const {get} = await import('idb-keyval');
|
||||||
|
const resources = await get('$$silphius_resources');
|
||||||
|
return resources || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set(resources) {
|
||||||
|
const {set} = await import('idb-keyval');
|
||||||
|
await set('$$silphius_resources', resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readAsset(path) {
|
||||||
|
const {pathname} = new URL(path, 'http://example.org');
|
||||||
|
const resourcePath = pathname.slice('/resources/'.length);
|
||||||
|
const resources = await get();
|
||||||
|
return resources[resourcePath]?.asset;
|
||||||
|
}
|
110
app/util/resources.server.js
Normal file
110
app/util/resources.server.js
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
watcher.on('change', async (path) => {
|
||||||
|
const asset = await computeAsset(path);
|
||||||
|
manifest[asset.path] = asset.hash.toString('hex');
|
||||||
|
});
|
||||||
|
watcher.on('unlink', async (path) => {
|
||||||
|
delete manifest[path.slice(import.meta.dirname.length - 1)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
45
app/util/resources.test.js
Normal file
45
app/util/resources.test.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import {expect, test} from 'vitest';
|
||||||
|
|
||||||
|
import {computeMissing, parseResources} from './resources.js';
|
||||||
|
import {encodeResources} from './resources.server.js';
|
||||||
|
|
||||||
|
test('cache', async () => {
|
||||||
|
class TestCache {
|
||||||
|
current;
|
||||||
|
async get() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve(this.current);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async set(manifest) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.current = manifest;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cache = new TestCache();
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar'})).to.deep.equal(['foo']);
|
||||||
|
await cache.set({foo: {hash: 'bar'}});
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar'})).to.deep.equal([]);
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar', baz: '32'})).to.deep.equal(['baz']);
|
||||||
|
await cache.set({foo: {hash: 'bar'}, baz: '32'});
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar', baz: '64'})).to.deep.equal(['baz']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('codec', async () => {
|
||||||
|
const asset = {
|
||||||
|
asset: (new Uint8Array('hello'.split('').map((letter) => letter.charCodeAt(0)))).buffer,
|
||||||
|
hash: (new Uint8Array(Array(20).fill(0).map((_, i) => i))).buffer,
|
||||||
|
};
|
||||||
|
const buffer = await encodeResources([
|
||||||
|
asset,
|
||||||
|
]);
|
||||||
|
const parsed = await parseResources(['hello.txt'], buffer);
|
||||||
|
expect('hello.txt' in parsed).to.equal(true);
|
||||||
|
expect(parsed['hello.txt'].hash).to.equal('000102030405060708090a0b0c0d0e0f10111213');
|
||||||
|
const hello = new Uint8Array(parsed['hello.txt'].asset);
|
||||||
|
for (let i = 0; i < 5; ++i) {
|
||||||
|
expect(hello[i]).to.equal('hello'.charCodeAt(i));
|
||||||
|
}
|
||||||
|
});
|
9
app/util/singleton.js
Normal file
9
app/util/singleton.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export function singleton(key, value) {
|
||||||
|
global.__singletons ??= {};
|
||||||
|
global.__singletons[key] ??= value;
|
||||||
|
return global.__singletons[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
singleton.reset = function (key) {
|
||||||
|
delete global.__singletons[key];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user