refactor: upfront/sync resource loading

This commit is contained in:
cha0s 2024-10-15 22:05:17 -05:00
parent ff17dd883a
commit cf4d21db1f
10 changed files with 104 additions and 131 deletions

View File

@ -1,14 +1,11 @@
import Components from '@/ecs/components/index.js';
import Ecs from '@/ecs/ecs.js';
import Systems from '@/ecs/systems/index.js';
import {readAsset} from '@/util/resources.js';
import {get, loadResources, readAsset} from '@/util/resources.js';
class PredictionEcs extends Ecs {
async readAsset(path) {
const resource = await readAsset(path);
return resource
? resource
: new ArrayBuffer(0);
readAsset(path) {
return readAsset(path) ?? new ArrayBuffer(0);
}
}
@ -17,6 +14,8 @@ const Flow = {
DOWN: 1,
};
await loadResources(await get());
const actions = new Map();
let ecs = new PredictionEcs({Components, Systems});
let mainEntityId = 0;

View File

@ -422,25 +422,19 @@ export default class Ecs {
}
}
async readJson(uri) {
readJson(uri) {
const key = ['$$json', uri].join(':');
if (!cache.has(key)) {
const {promise, resolve, reject} = withResolvers();
cache.set(key, promise);
this.readAsset(uri)
.then((chars) => {
resolve(
chars.byteLength > 0
? JSON.parse(textDecoder.decode(chars))
: {},
);
})
.catch(reject);
const buffer = this.readAsset(uri);
const json = buffer.byteLength > 0
? JSON.parse(textDecoder.decode(buffer))
: {};
cache.set(key, json);
}
return cache.get(key);
}
async readScript(uriOrCode, context = {}) {
readScript(uriOrCode, context = {}) {
if (!uriOrCode) {
return undefined;
}
@ -449,7 +443,7 @@ export default class Ecs {
code = uriOrCode;
}
else {
const buffer = await this.readAsset(uriOrCode);
const buffer = this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = textDecoder.decode(buffer);
}

View File

@ -13,6 +13,8 @@ export default class Server {
addPacketListener(type, listener) {
this.emitter.addListener(type, listener);
}
async load() {
}
async readJson(path) {
return JSON.parse(textDecoder.decode(await this.readData(path)));
}

View File

@ -13,10 +13,7 @@ export default class ClientEcs extends Ecs {
}
});
}
async readAsset(path) {
const resource = await readAsset(path);
return resource
? resource
: new ArrayBuffer(0);
readAsset(path) {
return readAsset(path) ?? new ArrayBuffer(0);
}
}

View File

@ -3,18 +3,12 @@ import {json, useLoaderData} from "@remix-run/react";
import {useEffect, useState} from 'react';
import {Outlet, useParams} from 'react-router-dom';
import {
computeMissing,
fetchResources,
get,
readAsset,
set,
} from '@/util/resources.js';
import {fetchMissingResources, readAsset} from '@/util/resources.js';
import styles from './play.module.css';
settings.ADAPTER.fetch = async (path) => {
const resource = await readAsset(path);
const resource = readAsset(path);
return resource ? new Response(resource) : new Response(undefined, {status: 404});
};
@ -29,34 +23,17 @@ export async function loader({request}) {
export default function Play() {
const {manifest} = useLoaderData();
const [assetsLoaded, setAssetsLoaded] = useState(false);
const [Client, setClient] = useState();
const params = useParams();
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();
fetchMissingResources(manifest, signal)
.then(() => {
setAssetsLoaded(true);
});
return () => {
controller.abort();
};
@ -78,7 +55,9 @@ export default function Play() {
}, [type]);
return (
<div className={styles.play}>
<Outlet context={Client} />
{assetsLoaded && (
<Outlet context={Client} />
)}
</div>
);
}

View File

@ -1,5 +1,3 @@
import {LRUCache} from 'lru-cache';
import Ecs from '@/ecs/ecs.js';
import {decode, encode} from '@/net/packets/index.js';
import {
@ -8,7 +6,6 @@ import {
TPS,
UPS,
} from '@/util/constants.js';
import {withResolvers} from '@/util/promise.js';
import createEcs from './create/ecs.js';
import createForest from './create/forest.js';
@ -19,10 +16,6 @@ import createTown from './create/town.js';
const UPS_PER_S = 1 / UPS;
const cache = new LRUCache({
max: 128,
});
const textEncoder = new TextEncoder();
export default class Engine {
@ -56,15 +49,8 @@ export default class Engine {
lookupPlayerEntity(id) {
return engine.lookupPlayerEntity(id);
}
async readAsset(uri) {
if (!cache.has(uri)) {
const {promise, resolve, reject} = withResolvers();
cache.set(uri, promise);
server.readAsset(uri)
.then(resolve)
.catch(reject);
}
return cache.get(uri);
readAsset(uri) {
return server.readAsset(uri);
}
async switchEcs(entity, path, updates) {
for (const [connection, connectedPlayer] of engine.connectedPlayers) {
@ -372,6 +358,7 @@ export default class Engine {
}
async load() {
await this.server.load();
let townData;
try {
townData = await this.server.readData('town');

View File

@ -5,35 +5,25 @@ import {WebSocketServer} from 'ws';
import Server from '@/net/server.js';
import {getSession} from '@/server/session.server.js';
import {loadResources, readAsset} from '@/util/resources.js';
import {loadResources as loadServerResources} from '@/util/resources.server.js';
import Engine from './engine.js';
const {
RESOURCES_PATH = [process.cwd(), 'resources'].join('/'),
} = process.env;
global.__silphiusWebsocket = null;
class SocketServer extends Server {
async ensurePath(path) {
await mkdir(path, {recursive: true});
}
async load() {
await loadResources(await loadServerResources());
}
static qualify(path) {
return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
}
async readAsset(path) {
const {pathname} = new URL(path, 'http://example.org');
const resourcePath = pathname.slice('/resources/'.length);
try {
const {buffer} = await readFile([RESOURCES_PATH, resourcePath].join('/'));
return buffer;
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return new ArrayBuffer(0);
}
readAsset(path) {
return readAsset(path) ?? new ArrayBuffer(0);
}
async readData(path) {
const qualified = this.constructor.qualify(path);

View File

@ -3,7 +3,7 @@ import {del, get, set} from 'idb-keyval';
import {encode} from '@/net/packets/index.js';
import Server from '@/net/server.js';
import {withResolvers} from '@/util/promise.js';
import {readAsset} from '@/util/resources.js';
import {get as getResources, loadResources, readAsset} from '@/util/resources.js';
import createEcs from './create/ecs.js';
import './create/forest.js';
@ -18,14 +18,14 @@ class WorkerServer extends Server {
super();
this.fs = {};
}
async load() {
await loadResources(await getResources());
}
static qualify(path) {
return ['UNIVERSE', path].join('/');
}
async readAsset(path) {
const resource = await readAsset(path);
return resource
? resource
: new ArrayBuffer(0);
readAsset(path) {
return readAsset(path) ?? new ArrayBuffer(0);
}
async readData(path) {
const data = await get(this.constructor.qualify(path));
@ -42,7 +42,9 @@ class WorkerServer extends Server {
async writeData(path, view) {
await set(this.constructor.qualify(path), view);
}
transmit(connection, packed) { postMessage(packed); }
transmit(connection, packed) {
postMessage(packed);
}
}
const engine = new Engine(WorkerServer);

View File

@ -1,9 +1,3 @@
import {LRUCache} from 'lru-cache';
const cache = new LRUCache({
max: 128,
});
export async function computeMissing(current, manifest) {
const missing = [];
for (const path in manifest) {
@ -14,6 +8,57 @@ export async function computeMissing(current, manifest) {
return missing;
}
export async function fetchMissingResources(manifest, signal) {
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
) {
throw e;
}
}
}
loadResources(current);
}
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 async function get() {
const {get} = await import('idb-keyval');
const resources = await get('$$silphius_resources');
return resources || {};
}
const cache = new Map();
export function loadResources(resources) {
for (const path in resources) {
cache.set(path, resources[path].asset);
}
}
const octets = [];
for (let i = 0; i < 256; ++i) {
octets.push(i.toString(16).padStart(2, '0'));
@ -44,35 +89,13 @@ export async function parseResources(resources, buffer) {
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 async function get() {
const {get} = await import('idb-keyval');
const resources = await get('$$silphius_resources');
return resources || {};
export function readAsset(path) {
const {pathname} = new URL(path, 'http://example.org');
const resourcePath = pathname.slice('/resources/'.length);
return cache.get(resourcePath);
}
export async function set(resources) {
const {set} = await import('idb-keyval');
cache.clear();
await set('$$silphius_resources', resources);
}
export async function readAsset(path) {
if (!cache.has(path)) {
const {pathname} = new URL(path, 'http://example.org');
const resourcePath = pathname.slice('/resources/'.length);
cache.set(path, get().then((resources) => resources[resourcePath]?.asset));
}
return cache.get(path);
}

View File

@ -90,17 +90,17 @@ export default class Script {
}
}
static async fromCode(code, context = {}) {
static fromCode(code, context = {}) {
if (!cache.has(code)) {
cache.set(code, this.parse(code));
}
return new this(
new Runner(await cache.get(code), this.createContext(context)),
new Runner(cache.get(code), this.createContext(context)),
code,
);
}
static async parse(code) {
static parse(code) {
return parse(
code,
{