// Load Vox/TOML files from a file import { Parse } from './vox'; import { IMessage } from './socket'; const assign = Object.assign; const b64 = require('base64-js'); const toml = require('toml'); const ASSETS = {}; const Memoize = (file: string, action: any): Promise => ASSETS[file] ? ASSETS[file] : Set(file, action(file)); type WatcherHandler = (payload: any, path?: string) => void; // oh baby alignment const InheritanceUp: { [key: string]: string[] } = {}; const InheritanceDown: { [key: string]: string } = {}; const Watchers: { [key: string]: WatcherHandler[] } = {}; const FromBase64 = (base64: string) => { return b64.toByteArray(base64); }; const exists = (obj: any) => obj === undefined ? {} : obj; const postProcess = { vox: (data: Response, file: string) => data.arrayBuffer().then(arrBuff => Parse(new Uint8Array(arrBuff))), toml: (data: Response | string, file: string) => { if (typeof data === 'string') { const parsed = toml.parse(data); return resolvePrefab(parsed, file); } else { return data.text() .then(toml.parse) .then(tomlData => resolvePrefab(tomlData, file)); } } }; export const assignComponents = (entity, prefab) => { Object.keys(prefab).forEach(key => { if(key === 'has') { return entity.has = assignComponents(exists(entity.has), exists(prefab.has)); } if(typeof prefab[key] === 'object') { if(Array.isArray(prefab[key])) { return entity[key] = entity[key] !== undefined ? entity[key] : prefab[key]; } return entity[key] = assign({}, prefab[key], entity[key]); } entity[key] = entity[key] !== undefined ? entity[key] : prefab[key]; }); return entity; }; // lets avoid side effects const resolvePrefab = async (fileData, file?: string) => { const entity = assignComponents({}, fileData); if (entity.is) { if(file) { // keep the inheritance tree so we know who to fire updates to const chain = InheritanceUp[entity.is] || []; chain.push(file); InheritanceUp[entity.is] = chain; InheritanceDown[file] = entity.is; } assignComponents(entity, await Get(entity.is)); } else if (InheritanceDown[file]) { const downFile = InheritanceDown[file]; const idx = InheritanceUp[downFile].indexOf(file); InheritanceUp[downFile] = InheritanceUp[downFile].splice(idx, 1); delete InheritanceDown[file]; } if(entity.has) { await Promise.all(Object.keys(entity.has).map(async key => { const childData = entity.has[key]; if(childData.is) { entity.has[key] = await resolvePrefab(childData); } })); } return entity; }; let fetchFile: (file: string) => Promise = (file: string) => { console.log('fetchFile not initialized while attempting ' + file); return new Promise((res, rej) => { res(''); }); }; const Initialize = (onMessage: any, aFetchFile: (file: string) => Promise) => { onMessage('asset', (msg: IMessage) => { const result = msg.payload; if (result.path.indexOf('.vox') !== -1) { result.data = Parse(FromBase64(result.data)); } else if (result.path.indexOf('.toml') !== -1) { result.data = resolvePrefab(toml.parse(result.data), result.path); } else { console.log('Unhandled asset', msg); } Set(result.path, Promise.resolve(result.data)); }); fetchFile = aFetchFile; }; const debounces: {[key: string]: Promise} = {}; const Debounce = (time: number, file: string) => { if(debounces[file]) Promise.reject(''); return new Promise(resolve => { setTimeout(resolve, time); }); }; const Fire = async (file: string) => { await Debounce(0, file); return Get(file).then(data => { const handlers = Watchers[file]; if (handlers) { handlers.forEach(fn => fn(data, file)); } const isToml = file.indexOf('.toml') !== -1; const hasUpstream = InheritanceUp[file] && InheritanceUp[file].length > 0; if (isToml && hasUpstream) { InheritanceUp[file].forEach(Fire); } }); }; const Set = (file: string, dataPromise: Promise) => { if ((process as any).browser) { dataPromise.then(data => { sessionStorage.setItem(file, JSON.stringify(data)); }); } ASSETS[file] = dataPromise; Fire(file); return dataPromise; }; const Off = (file: string, callback: WatcherHandler) => { const handlers = Watchers[file] || []; const idx = handlers.indexOf(callback); if (idx !== -1) { Watchers[file] = handlers.splice(idx, 1); } return idx !== -1; }; const Watch = async (file: string, callback: WatcherHandler) => { On(file, callback); Get(file); }; const On = (file: string, callback: WatcherHandler) => { const handlers = Watchers[file] || []; handlers.push(callback); Watchers[file] = handlers; }; const Get = (file: string) => Memoize(file, () => { if ((process as any).browser) { // const cache = sessionStorage.getItem(file); // if (cache) // return Promise.resolve(JSON.parse(cache)); } return fetchFile(file) .then((dataResponse) => { let processing = Promise.resolve(dataResponse); Object.keys(postProcess).some(key => { if (file.indexOf(`.${key}`) !== -1) { processing = postProcess[key](dataResponse, file); return true; } }); return processing; }).catch((e) => { console.log('fetch caught ', e, ' while processing ' + file); }); }); const Gets = (files: { [key: string]: string }) => { const returnObj: any = {}; Object.keys(files).forEach(key => { returnObj[key] = Get(files[key]); }); returnObj.all = Promise.all(Object.keys(returnObj).map(key => returnObj[key])); return returnObj; }; export const Asset = { get: Get, gets: Gets, on: On, watch: Watch, off: Off, initialize: Initialize, resolvePrefab };