diff --git a/package.json b/package.json index 7fae617..453cd61 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "test": "node --enable-source-maps --unhandled-rejections=strict out/run.js test", "debug": "cross-env DEBUG=vogue:* yarn test", - "debug:watch": "cross-env DEBUG=vogue:* supervisor -w out,test,lib -n exit --exec yarn -- test", + "debug:watch": "cross-env DEBUG=vogue:* supervisor -w out,test/**/*.v,lib -n exit --exec yarn -- test", "postinstall": "yarn compile && cd test && yarn", "postcompile:watch": "echo DONE", "compile": "tsc", diff --git a/src/Instance.ts b/src/Instance.ts index 567fc49..9ce88dd 100644 --- a/src/Instance.ts +++ b/src/Instance.ts @@ -2,7 +2,7 @@ import debug from 'debug'; import _ from 'lodash'; const log = debug('vogue:instance'); import vm from 'vm'; -import Module, { Link, Variable } from './Module.js'; +import Module, { LinkDescription, Variable } from './Module.js'; import System from './System.js'; import * as uuid from 'uuid'; /** @@ -10,6 +10,8 @@ import * as uuid from 'uuid'; * @typedef {import('./Module.js').default} Module */ +export type Link = any; // BUT PROXY + export type SerializedInstance = { type: string, links: { @@ -25,38 +27,49 @@ export default class Instance { module: Module; links = {} system: System; - context: vm.Context; + context: vm.Context | null = null; locals = []; internalFunctions = {}; _link: Instance; location: string; _id: string; + initialContext: any = {}; + + get currentContext(): any { + return this.context ?? this.initialContext; + } + + sync() { + this.system.saveInstance(this); + } createContext(): vm.Context { if(this.context) return this.context; + const initialContext: any = {}; // system globals! // TODO turn this into its own vogue module! system.create/instance.create // TODO request context from system... - initialContext.create = this.system.newInstance.bind(this.system); + initialContext.create = this.system.newLink.bind(this.system); initialContext.process = process; - for(const name in this.system.staticInstances) - initialContext[name] = this.system.staticInstances[name]; + for(const name in this.system.staticLinks) { + log('creating context with static link: ' + name); + initialContext[name] = this.system.staticLinks[name]; + } // local links! // optional arrays // TODO maybe make these property accessors to allow for some automation - for(const link of this.module.links.filter((v: Link) => v.array && !v.required)) + for(const link of this.module.links.filter((v: LinkDescription) => v.array && !v.required)) initialContext[link.name] = []; - for(const link of this.module.links.filter((v: Link) => !v.array && !v.required)) + for(const link of this.module.links.filter((v: LinkDescription) => !v.array && !v.required)) initialContext[link.name] = null; - for(const variable of this.module.variables) { - attachHookedProperty(initialContext, variable.name, null, () => { - this.system.saveInstance(this); - }) - } + for(const variable of this.module.variables) + attachHookedProperty(initialContext, variable.name, null, this.sync.bind(this)) + for(const name in this.initialContext) + initialContext[name] = this.initialContext[name] for(const name in this.module.imports) initialContext[name] = this.module.imports[name]; @@ -68,41 +81,70 @@ export default class Instance { // 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); -`; + const injectedScript = ` + var ${name} = ${async ? 'async ' : ''}function ${name}(${parameters.join(', ')}) ${code} + ${name} = ${name}.bind(this);`.trim(); + // log('injecting function...') + // log(injectedScript) vm.runInContext(injectedScript, context, {}); } + log('context created! ' + Object.keys(context)); + // log(context); + return context; }; - constructor(module: Module, location: string, parameters: {[name: string]: any}, system: System) { + setMember(name: string, value: any) { + log('setMember: ' + this.toString() + '.' + name + ' => ' + value); + this.currentContext[name] = value; + } + + setLink(name: string, value: Link) { + log('setLink: ' + this.toString() + '.' + name + ' => ' + value.__link__); + this.currentContext[name] = value; + } + + constructor( + module: Module, + location: string, + parameters: {[name: string]: any}, + system: System, + options?: { + id?: string + } + ) { this.module = module; this.location = location; this.system = system; - this.context = this.createContext(); - this._id = uuid.v4(); + // this.context = this.createContext(); + this._id = options?.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]})`); + log(`getting ${target.module.name.full}.${prop.toString()}: (${target.module.identifiers[prop]}|${typeof target.context?.[prop]})`); + + if(target.context === null) + target.restore(); + 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]; + return target.context?.[prop]; } throw new Error(DNEText); } }); + log('created ' + this); } restore() { - return this.context.restore?.(); + if(this.context === null) + this.context = this.createContext(); + + return this.context?.restore?.(); } get link () { @@ -112,9 +154,9 @@ ${name} = ${name}.bind(this); toSerializableObject(): SerializedInstance { const obj: any = {}; obj.type = this.module.name.full; - obj.links = Object.fromEntries(this.module.links.map((link: Link): [string, string] => { + obj.links = Object.fromEntries(this.module.links.map((link: LinkDescription): [string, string] => { const name = link.name; - const linkId = this.context[name]?.__link__; + const linkId = this.context?.[name]?.__link__; return [name, linkId]; })); obj.members = Object.fromEntries( @@ -124,7 +166,7 @@ ${name} = ${name}.bind(this); }) .map((member: Variable): [string, any] => { const name = member.name; - const value = this.context[name]; + const value = this.context?.[name]; return [name, value]; }) ); @@ -134,7 +176,7 @@ ${name} = ${name}.bind(this); } toString() { - return this.module.name.full + '(' + this._id + ')'; + return this.module.name.full + '(' + this._id.substr(0, 4) + ')'; } } diff --git a/src/Module.ts b/src/Module.ts index 5e9f3a7..c36c14e 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -14,7 +14,7 @@ import { createRequire } from 'module'; import { pathToFileURL } from 'url'; const log = debug('vogue:module'); -export type Link = { +export type LinkDescription = { name: string, array: boolean, required: boolean @@ -26,7 +26,7 @@ export type Variable = { } export default class Module { - links: Link[] = []; + links: LinkDescription[] = []; globals = []; functions: { [name: string]: { diff --git a/src/System.ts b/src/System.ts index 79e85d6..503bb6c 100644 --- a/src/System.ts +++ b/src/System.ts @@ -1,8 +1,8 @@ -import Instance from './Instance.js'; +import Instance, { Link, SerializedInstance } from './Instance.js'; import _ from 'lodash'; import Module from './Module.js'; import debug from 'debug'; -import { writeFileSync } from 'fs'; +import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; import { ensureDirSync } from 'fs-extra'; const log = debug('vogue:system') @@ -16,11 +16,11 @@ type ModuleNamespaceMap = { type ModuleName = string; class System { - instances: Instance[] = []; + instances: Map = new Map(); modules: Module[]; namespace: ModuleNamespaceMap = {}; - staticInstances: { - [key: string]: Instance + staticLinks: { + [key: string]: Link } = {}; rootDir: string; @@ -28,16 +28,87 @@ class System { this.rootDir = rootDir; this.modules = modules; this.createNamespace(); + + const vault = readdirSync(resolve(this.rootDir, '.system')).map(v => resolve(this.rootDir, '.system', v)); + const serializedInstances: SerializedInstance[] = vault.map((v) => JSON.parse(readFileSync(v).toString())); + + log('injecting serialized instances...'); + for(const serializedInstance of serializedInstances) + this.injectSerializedInstance(serializedInstance); + log('linking serialized instances...'); + for(const serializedInstance of serializedInstances) + this.linkSerializedInstance(serializedInstance); + + log('restoring static instances...'); + for(const [,instance] of this.instances) { + if(!!instance.module.static) { + instance.restore(); + } + } + + log('restoring boot instances...'); + for(const [,instance] of this.instances) { + if(!!instance.module.singleton) { + instance.restore(); + } + } + // this.inject(serializedInstance); + + + if (vault.length !== 0) { + return this; + } + + // TODO future workflow notes + // pull jsons into boots + // filter json boots + // create static / singletons into boots + // boot boots! + + const bootModules = this.deriveBootModules(); this.createStaticInstances(); log('instantiating boot modules...'); for(const name of bootModules) { log(' ' + name); - this.newInstance(name); + this.newLink(name); } } + linkSerializedInstance(serializedInstance: SerializedInstance): void { + const instance = this.getInstanceById(serializedInstance.id); + for(const name in serializedInstance.links) { + const linkId = serializedInstance.links[name] + const linkedInstance = this.getInstanceById(linkId); + const linkedInstanceLink = linkedInstance.link; + instance.setLink(name, linkedInstanceLink); + } + } + + injectSerializedInstance(serializedInstance: SerializedInstance): void { + const instance = new Instance(this.getModule(serializedInstance.type), this.rootDir, {}, this, { + id: serializedInstance.id + }); + this.instances.set(instance._id, instance); + + for(const name in serializedInstance.members) { + instance.setMember(name, serializedInstance.members[name]); + } + + if(instance.module.static) { + log('injected static instance ' + instance.module.static + ': ' + instance.module.name.full); + this.staticLinks[instance.module.static] = instance.link; + } + } + + getInstanceById(id: string): Instance { + if(!this.instances.has(id)) + throw new Error(`${id} is not a valid instance link id`); + + return this.instances.get(id) as Instance; + } + createStaticInstances() { log('deriving static modules...'); const staticModules = this.modules.filter((module) => { @@ -50,8 +121,8 @@ class System { log('instantiating static modules...'); for(const module of staticModules) { log(' ' + module.static + ': ' + module.name.full); - this.staticInstances[module.static] = - this.newInstance(module.name.full, {}); + this.staticLinks[module.static] = + this.newLink(module.name.full, {}); } } @@ -81,16 +152,13 @@ class System { } 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); + log('saving ' + instance + '...'); const json = JSON.stringify(instance.toSerializableObject(), null, 2) - log(json); writeFileSync(filepath, json); - log('synced ' + instance); } getModule(name: ModuleName): Module { @@ -100,12 +168,12 @@ class System { } createInstance(name: ModuleName, args = {}) { - const instance = new Instance(this.getModule(name), '', args, this); + const instance = new Instance(this.getModule(name), this.rootDir, args, this); this.saveInstance(instance); return instance; } - newInstance(name: ModuleName, args = {}) { + newLink(name: ModuleName, args = {}) { const instance = this.createInstance(name, args); const link = instance.link; instance.restore(); @@ -113,4 +181,14 @@ class System { } } -export default System; \ No newline at end of file +export default System; + +// class SerializedInstanceInjector { +// system: System; +// serializedInstances: SerializedInstance[]; + +// constructor(serializedInstances: SerializedInstance[], system: System) { +// this.serializedInstances = serializedInstances; +// this.system = system; +// } +// } \ No newline at end of file diff --git a/vogue-0.0.1.tgz b/vogue-0.0.1.tgz deleted file mode 100644 index 7b94f1e..0000000 Binary files a/vogue-0.0.1.tgz and /dev/null differ