systems save to disk. Closes #3

pull/22/head
Bronwen 2021-05-21 23:31:35 -04:00
parent 6a135bcd61
commit 6fb5c30b1d
10 changed files with 136 additions and 222 deletions

View File

@ -18,12 +18,15 @@
},
"dependencies": {
"@types/debug": "^4.1.5",
"@types/fs-extra": "^9.0.11",
"@types/lodash": "^4.14.169",
"@types/nearley": "^2.11.1",
"@types/node": "^15.3.0",
"@types/uglify-js": "^3.13.0",
"@types/uuid": "^8.3.0",
"cross-env": "^7.0.3",
"debug": "^4.3.1",
"fs-extra": "^10.0.0",
"lodash": "^4.17.21",
"moo": "^0.5.1",
"nearley": "^2.20.1",
@ -31,6 +34,7 @@
"supervisor": "^0.12.0",
"typescript": "^4.2.4",
"uglify-js": "^3.13.5",
"uuid": "^8.3.2",
"yarn": "^1.22.10"
}
}

View File

@ -1,20 +1,27 @@
import Serializable from './Serializable.js';
import minify from './minify.js';
import debug from 'debug';
import _ from 'lodash';
const log = debug('vogue:instance');
import vm from 'vm';
import Module, { Link } from './Module.js';
import Module, { Link, Variable } from './Module.js';
import System from './System.js';
import { KV } from './KV.js';
import * as uuid from 'uuid';
/**
* @typedef {import('./System.js').default} System
* @typedef {import('./Module.js').default} Module
*/
export type SerializedInstance = {
type: string,
links: {
[name: string]: string
},
members: {
[name: string]: any // SO LONG AS ITS SERIALIZABLE ._.*
},
id: string
}
export default class Instance extends Serializable {
export default class Instance {
module: Module;
links = {}
system: System;
@ -23,11 +30,12 @@ export default class Instance extends Serializable {
internalFunctions = {};
_link: Instance;
location: string;
_id: string;
createContext(): vm.Context {
if(this.context) return this.context;
const initialContext: KV = {};
const initialContext: any = {};
// system globals!
// TODO turn this into its own vogue module! system.create/instance.create
@ -49,34 +57,38 @@ export default class Instance extends Serializable {
for(const name in this.module.imports)
initialContext[name] = this.module.imports[name];
// instance defined functions
initialContext.sync = this.system.saveInstance.bind(this.system, this);
const context = vm.createContext(initialContext);
// user defined functions
for(const name in this.module.functions) {
const { code, parameters, async } = this.module.functions[name];
const injectedScript =
`
var ${name} = ${async ? 'async' : ''} function ${name}(${parameters.join(', ')}) ${code}
${name} = ${name}.bind(this);
`;
vm.runInContext(injectedScript, context, {
});
vm.runInContext(injectedScript, context, {});
}
return context;
};
constructor(module: Module, location: string, parameters: {[name: string]: any}, system: System) {
super();
this.module = module;
this.location = location;
this.system = system;
this.context = this.createContext();
this._id = uuid.v4();
this._link = new Proxy(this, {
get(target: Instance, prop: string, receiver) {
log(`getting ${target.module.name.full}.${prop}: (${target.module.identifiers[prop]}|${typeof target.context[prop]})`);
const DNEText = `${target.module.name.full}.${prop.toString()} either does not exist, or is not accessible`;
if(prop === 'restore') throw new Error(DNEText);
if(prop === '__link__') return target._id;
if(prop in target.module.functions) {
return target.context[prop];
@ -93,4 +105,32 @@ var ${name} = ${async ? 'async' : ''} function ${name}(${parameters.join(', ')})
get link () {
return this._link;
}
toSerializableObject(): SerializedInstance {
const obj: any = {};
obj.type = this.module.name.full;
obj.links = Object.fromEntries(this.module.links.map((link: Link): [string, string] => {
const name = link.name;
const linkId = this.context[name]?.__link__;
return [name, linkId];
}));
obj.members = Object.fromEntries(
this.module.variables
.filter((member: Variable): boolean => {
return member.persist;
})
.map((member: Variable): [string, any] => {
const name = member.name;
const value = this.context[name];
return [name, value];
})
);
obj.id = this._id;
return obj as SerializedInstance;
}
toString() {
return this.module.name.full + '(' + this._id + ')';
}
}

View File

@ -1,3 +0,0 @@
export type KV = {
[key: string]: any
};

View File

@ -1,198 +0,0 @@
// import { Ubjson } from '@shelacek/ubjson';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { KV } from './KV';
export default class Serializable {
constructor(...args: any[]) {}
// things that need to be stored only in cold
// storage are keyed with a special prefix
static CLASS_REFERENCE = '$$CLASS_NAME';
// things that need to be stored only at runtime
// are keyed with symbols to not interfere with
// user code.
static PERSIST_LOCATION = Symbol('PERSIST_LOCATION');
start() {}
// toUbj() {
// return Ubjson.encode(this.toSerializableObject());
// }
// static fromUbj(buffer) {
// return this.fromSerializableObject(Ubjson.decode(buffer));
// }
toJson() {
return JSON.stringify(this.toSerializableObject(), null, 2);
}
static serializationDependencies(): any[] {
return [];
}
static fromJson(str: string) {
return this.fromSerializableObject(JSON.parse(str));
}
toSerializableObject() {
const transformValue = (val: any): any => {
if(Array.isArray(val)) {
return transformArray(val);
} else if (val === null || val === undefined) {
return val;
} else if(typeof val === 'object') {
return transformObject(val);
} else {
return val;
}
}
const transformObject = (obj: KV): KV => {
const clone: KV = {};
for(const prop of Object.keys(obj)) {
if(prop.startsWith('_')) continue;
clone[prop] = transformValue(obj[prop]);
}
if(obj instanceof Serializable) {
clone[Serializable.CLASS_REFERENCE] = obj.constructor.name;
}
return clone;
}
const transformArray = (arr: any[]): any[] => {
const clone = [];
for(const item of arr) {
clone.push(transformValue(item));
}
return clone;
}
return transformObject(this);
}
static fromSerializableObject(obj: KV) {
if(obj[Serializable.CLASS_REFERENCE] !== this.name) return null;
const transformValue = (val: any): any => {
if(Array.isArray(val)) {
return transformArray(val);
} else if(val === null || val === undefined) {
return val;
} else if(typeof val === 'object') {
if(Serializable.CLASS_REFERENCE in val) {
const classes = this.serializationDependencies();
const matchingClasses = classes.filter((classObject) => {
classObject.name === val[Serializable.CLASS_REFERENCE]
});
if(matchingClasses.length === 1) {
return matchingClasses[0].fromSerializableObject(val);
} else {
return transformObject(val);
}
}
return transformObject(val);
} else {
return val;
}
}
const transformObject = (obj: KV): KV => {
const clone: KV = {};
for(const prop of Object.keys(obj)) {
if(prop.startsWith('_')) continue;
clone[prop] = transformValue(obj[prop]);
}
return clone;
}
const transformArray = (arr: any[]): any[] => {
const clone = [];
for(const item of arr) {
clone.push(transformValue(item));
}
return clone;
}
const clone = transformObject(obj);
if(Serializable.CLASS_REFERENCE in obj)
clone.__proto__ = this.prototype;
clone.restore();
return clone;
}
serialize({
encoding = 'json'
} = {}) {
switch(encoding) {
case 'json': return this.toJson();
case 'ubjson':
// case 'ubj': return this.toUbj();
default: {
throw new TypeError('Unknown encoding: ' + encoding);
}
}
}
static deserialize(obj: any, {
encoding = 'json'
} = {}) {
switch(encoding) {
case 'json': return this.fromJson(obj);
case 'ubjson':
// case 'ubj': return this.fromUbj(obj);
default: {
throw new TypeError('Unknown encoding: ' + encoding);
}
}
}
async restore() {}
static createFromDisk(filename: string, ...args: any[]) {
if(existsSync(filename)) {
const instance = this.deserialize(readFileSync(createFilepath(filename)));
// TS is plain and simply wrong... symbols can be used to index object...
// @ts-ignore
instance[Serializable.PERSIST_LOCATION] = createFilepath(filename);
instance?.restore();
return instance;
} else {
const instance = new this(...args);
// again... TS is wrong...
// @ts-ignore
instance[Serializable.PERSIST_LOCATION] = createFilepath(filename);
instance?.updateDisk();
return instance;
}
}
updateDisk(filepath?: string) {
// if it hasnt yet been written to disk...
// this can happen if the contrustor
// was called outside of createFromDisk
if(filepath) {
// see above... TS7053 is just _wrong_. incorrect. thats not how JS works.
// @ts-ignore
this[Serializable.PERSIST_LOCATION] = createFilepath(filepath);
}
const data = this.serialize();
// this is getting annoying...
// @ts-ignore
writeFileSync(this[Serializable.PERSIST_LOCATION], data);
}
}
function createFilepath(path: string) {
return `data/${path}`;
}

View File

@ -1,8 +1,10 @@
import Instance from './Instance.js';
import Serializable from './Serializable.js';
import _ from 'lodash';
import Module from './Module.js';
import debug from 'debug';
import { writeFileSync } from 'fs';
import { resolve } from 'path';
import { ensureDirSync } from 'fs-extra';
const log = debug('vogue:system')
const {get, set} = _;
@ -13,7 +15,7 @@ type ModuleNamespaceMap = {
type ModuleName = string;
class System extends Serializable {
class System {
instances: Instance[] = [];
modules: Module[];
namespace: ModuleNamespaceMap = {};
@ -23,12 +25,11 @@ class System extends Serializable {
rootDir: string;
constructor(modules: Module[], rootDir: string) {
super();
this.rootDir = rootDir;
this.modules = modules;
this.createNamespace();
const bootModules = this.deriveBootModules();
this.createStaticInstances();
this.rootDir = rootDir;
log('instantiating boot modules...');
for(const name of bootModules) {
@ -79,6 +80,19 @@ class System extends Serializable {
}, {});
}
saveInstance(instance: Instance): void {
log('saving ' + instance)
const path = resolve(this.rootDir, '.system');
ensureDirSync(path);
const file = instance._id + '.json';
const filepath = resolve(path, file);
log(filepath);
const json = JSON.stringify(instance.toSerializableObject(), null, 2)
log(json);
writeFileSync(filepath, json);
log('synced ' + instance);
}
getModule(name: ModuleName): Module {
const module = get(this.namespace, name);
if(module instanceof Module) return module;
@ -86,7 +100,9 @@ class System extends Serializable {
}
createInstance(name: ModuleName, args = {}) {
return new Instance(this.getModule(name), '', args, this);
const instance = new Instance(this.getModule(name), '', args, this);
this.saveInstance(instance);
return instance;
}
newInstance(name: ModuleName, args = {}) {

View File

@ -4,3 +4,8 @@ Object.defineProperty(Array.prototype, 'empty', {
return this.length === 0;
}
});
process.on('unhandledRejection', (reason: Error, p) => {
console.log(reason.stack ?? reason.name + '\n\nStack trace unavailable...');
});

View File

@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env node --enable-source-maps --unhandled-rejections=strict
import debug from 'debug';
const log = debug('vogue:cli');
const systemLocation = resolve(process.argv[2]);
@ -11,14 +11,11 @@ import Module from './Module.js';
import System from './System.js';
import './extensions.js';
import { fileURLToPath } from 'url';
// globals inside grammar context
import minify from './minify';
const { get, set } = _;
const standardLibrary = resolve(fileURLToPath(dirname(import.meta.url)), '..', 'lib', 'vogue');
(async () => {
// TODO simplify this line gaddam
const ignoreDeps = (path: string) => parse(path).name !== 'node_modules';
const files = [
@ -35,7 +32,7 @@ const standardLibrary = resolve(fileURLToPath(dirname(import.meta.url)), '..', '
const modules = await Promise.all(fullpaths.map(loc => Module.create(loc, systemLocation)));
const sys = new System(modules, systemLocation);
})()
})();
function walkdirSync(root: string, filter: ((path: string) => boolean) = () => true): string[] {
log('reading', root, '...');

View File

@ -2,6 +2,7 @@ member count;
increment() {
count ++;
sync();
}
getCount() {

View File

@ -19,4 +19,6 @@ async restore {
// window.setScene()
// await counter.render();
sync();
}

View File

@ -6,6 +6,13 @@
version "4.1.5"
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz"
"@types/fs-extra@^9.0.11":
version "9.0.11"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.11.tgz#8cc99e103499eab9f347dbc6ca4e99fb8d2c2b87"
integrity sha512-mZsifGG4QeQ7hlkhO56u7zt/ycBgGxSVsFI/6lGTU34VtwkiqrrSDgw0+ygs8kFGWcXnFQWMrzF2h7TtDFNixA==
dependencies:
"@types/node" "*"
"@types/lodash@^4.14.169":
version "4.14.169"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.169.tgz"
@ -14,6 +21,11 @@
version "2.11.1"
resolved "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.1.tgz"
"@types/node@*":
version "15.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.0.tgz#f0ddca5a61e52627c9dcb771a6039d44694597bc"
integrity sha512-gCYSfQpy+LYhOFTKAeE8BkyGqaxmlFxe+n4DKM6DR0wzw/HISUE/hAmkC/KT8Sw5PCJblqg062b3z9gucv3k0A==
"@types/node@^15.3.0":
version "15.3.0"
resolved "https://registry.npmjs.org/@types/node/-/node-15.3.0.tgz"
@ -24,6 +36,11 @@
dependencies:
source-map "^0.6.1"
"@types/uuid@^8.3.0":
version "8.3.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
async@0.2.10:
version "0.2.10"
resolved "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
@ -62,6 +79,20 @@ discontinuous-range@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz"
fs-extra@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1"
integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.6"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
@ -70,6 +101,15 @@ isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz"
@ -172,6 +212,16 @@ underscore@~1.4.4:
version "1.4.4"
resolved "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz"
universalify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"