From 96145b370b01a1cdc61d49ac9d5bd0cfe6884112 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 3 Jan 2022 17:13:50 -0500 Subject: [PATCH] improve indexdb promised. fix multiple spaces access. add workspace persist + restore for mesh --- src/data/index.js | 120 +++++++++++++++++++++++++++++---------------- src/main/mesh.js | 42 ++++++++++++++-- src/mesh/api.js | 4 +- src/mesh/group.js | 17 +++++-- src/mesh/model.js | 17 +++++-- src/mesh/object.js | 4 +- src/mesh/work.js | 11 +++-- src/moto/client.js | 37 +++++++++++++- src/moto/worker.js | 4 ++ 9 files changed, 192 insertions(+), 64 deletions(-) diff --git a/src/data/index.js b/src/data/index.js index 8580e904..b002473e 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -11,6 +11,10 @@ data.Index = IDBStore; + data.open = function() { + return new IDBStore(...arguments); + }; + // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API let IDB, IRR, local = null; @@ -35,6 +39,7 @@ this.queue = []; this.idle = []; this.initCalled = false; + this.created = {}; } IDBStore.delete = function(dbname) { @@ -56,32 +61,60 @@ return this; }; - SP.promise = function() { - let db = this; - return { - keys: (lower, upper) => new Promise(function(resolve, reject) { - db.keys(resolve,lower,upper); - }), - - iterate: (lower, upper, nullterm) => new Promise(function(resolve, reject) { - db.iterate(resolve, lower, upper, nullterm); + SP.promise = function(store) { + let db = this; + let pc = { + iterate: (opt = {}) => new Promise(function(resolve, reject) { + let { lower, upper, collect } = opt; + if (!collect) { + let rval = opt.map ? {} : []; + collect = (key, value) => { + if (key !== null) { + if (opt.map) { + rval[key] = value; + } else { + rval.push({key, value}); + } + } else { + resolve(rval); + } + }; + } else { + resolve(); + } + db.iterate(collect, lower, upper, true, store); }), get: (key) => new Promise(function(resolve, reject) { - db.get(key, resolve); + db.get(key, resolve, store); }), put: (key, value) => new Promise(function(resolve, reject) { - db.put(key, value, resolve); + db.put(key, value, resolve, store); }), remove: (key) => new Promise(function(resolve, reject) { - db.remove(key, resolve); - }) + db.remove(key, resolve, store); + }), + + // wait for all outstanding transactions to complete + wait: () => new Promise(function(resolve, reject) { + db.onIdle(promise); + }), + + use: (store) => { + db.use(store); + return pc; + }, }; + return pc; } - SP.keys = function(callback, lower, upper) { + SP.keys = function(callback, lower, upper, store) { + if (!this.db) { + this.init(); + return this.queue.push(["keys", callback, lower, upper, store]); + } let out = []; this.iterate(function(k,v) { if (k === null) { @@ -89,25 +122,25 @@ } else { out.push(k); } - }, lower, upper, true); + }, lower, upper, true, store); }; - SP.iterate = function(callback, lower, upper, nullterm) { + SP.iterate = function(callback, lower, upper, nullterm, store) { if (!this.db) { this.init(); - return this.queue.push(["iterate", callback, lower, upper]); + return this.queue.push(["iterate", callback, lower, upper, nullterm, store]); } let range = lower && upper ? IRR.bound(lower,upper) : lower ? IRR.lowerBound(lower) : upper ? IRR.upperBound(upper) : undefined; // iterate over all db values for debugging this.db - .transaction(this.name) - .objectStore(this.current) + .transaction(store) + .objectStore(store) .openCursor(range).onsuccess = function(event) { let cursor = event.target.result; if (cursor) { - callback(cursor.key,cursor.value); + callback(cursor.key, cursor.value); cursor.continue(); } else if (nullterm) { callback(null); @@ -149,7 +182,7 @@ } let db = storage.db = request.result; for (let store of storage.stores) { - storage.store = db.createObjectStore(store); + storage.created[store] = db.createObjectStore(store); storage.current = store; } event.target.transaction.oncomplete = function(event) { @@ -181,12 +214,13 @@ let i = 0, q = this.queue, e; while (i < q.length) { e = q[i++]; - switch (e[0]) { - case 'iterate': this.iterate(e[1], e[2], e[3]); break; - case 'put': this.put(e[1], e[2], e[3]); break; - case 'get': this.get(e[1], e[2]); break; - case 'remove': this.remove(e[1], e[2]); break; - case 'clear': this.clear(); + switch (e.shift()) { + case 'iterate': this.iterate(...e); break; + case 'keys': this.keys(...e); break; + case 'put': this.put(...e); break; + case 'get': this.get(...e); break; + case 'remove': this.remove(...e); break; + case 'clear': this.clear(...e); } } this.queue = []; @@ -196,7 +230,7 @@ } }; - SP.put = function(key, value, callback) { + SP.put = function(key, value, callback, store = this.current) { if (local) { local[key] = value; if (callback) callback(true); @@ -204,12 +238,12 @@ } if (!this.db) { this.init(); - return this.queue.push(['put', key, value, callback]); + return this.queue.push(['put', key, value, callback, store]); } try { let req = this.db - .transaction(this.name, "readwrite") - .objectStore(this.current).put(value, key); + .transaction(store, "readwrite") + .objectStore(store).put(value, key); if (callback) { req.onsuccess = function(event) { callback(true) }; req.onerror = function(event) { callback(false) }; @@ -220,19 +254,19 @@ } }; - SP.get = function(key, callback) { + SP.get = function(key, callback, store = this.current) { if (local) { if (callback) callback(local[key]); return; } if (!this.db) { this.init(); - return this.queue.push(['get', key, callback]); + return this.queue.push(['get', key, callback, store]); } try { let req = this.db - .transaction(this.name) - .objectStore(this.current).get(key); + .transaction(store) + .objectStore(store).get(key); if (callback) { req.onsuccess = function(event) { callback(req.result) }; req.onerror = function(event) { callback(null) }; @@ -243,7 +277,7 @@ } }; - SP.remove = function(key, callback) { + SP.remove = function(key, callback, store = this.current) { if (local) { delete local[key]; if (callback) callback(true); @@ -251,12 +285,12 @@ } if (!this.db) { this.init(); - return this.queue.push(['remove', key, callback]); + return this.queue.push(['remove', key, callback, store]); } try { let req = this.db - .transaction(this.name, "readwrite") - .objectStore(this.current).delete(key); + .transaction(store, "readwrite") + .objectStore(store).delete(key); if (callback) { req.onsuccess = function(event) { callback(true) }; req.onerror = function(event) { callback(false) }; @@ -267,14 +301,14 @@ } }; - SP.clear = function(key) { + SP.clear = function(key, store = this.current) { if (!this.db) { this.init(); - return this.queue.push(['clear']); + return this.queue.push(['clear', store]); } this.db - .transaction(this.name, "readwrite") - .objectStore(this.current).clear(); + .transaction(store, "readwrite") + .objectStore(store).clear(); }; })(); diff --git a/src/main/mesh.js b/src/main/mesh.js index 50b84eba..a2430a02 100644 --- a/src/main/mesh.js +++ b/src/main/mesh.js @@ -8,7 +8,8 @@ let broker = gapp.broker; let call = broker.send; function init() { - let moto = self.moto, + let stores = data.open('mesh', { stores:[ "admin", "cache", "space" ] }).init(), + moto = self.moto, api = mesh.api, sky = false, dark = false, @@ -16,7 +17,16 @@ function init() { zoomrev = true, zoomspd = 1, space = moto.Space, - platform = space.platform; + platform = space.platform, + db = mesh.db = { + admin: stores.promise('admin'), + space: stores.promise('space'), + cache: stores.promise('cache') + }; + + // mark init time and use count + db.admin.put("init", Date.now()); + db.admin.get("uses").then(v => db.admin.put("uses", (v||0) + 1)); // setup default workspace space.useDefaultKeys(false); @@ -33,7 +43,9 @@ function init() { color: 0xdddddd, zoom: { reverse: true, speed: 1 }, size: { width: 2000, depth: 2000, height: 1, maxz: 300 }, - grid: { major: 25, minor: 5, colorMajor: 0xcccccc, colorMinor: 0xeeeeee, colorX: 0xffaaaa, colorY: 0xaaaaff }, + grid: { major: 25, minor: 5, + colorMajor: 0xcccccc, colorMinor: 0xeeeeee, + colorX: 0xffaaaa, colorY: 0xaaaaff }, }); space.view.setZoom(zoomrev, zoomspd); @@ -46,10 +58,32 @@ function init() { // trigger ui building call.ui_build(); + // reload stored space + moto.client.on('ready', restore_space); + // hide loading curtain $d('curtain','none'); } +function restore_space() { + mesh.db.space.iterate({ map: true }).then(cached => { + for (let [id, data] of Object.entries(cached)) { + // restore group + if (Array.isArray(data)) { + let models = data.map(id => { + let md = cached[id]; + return new mesh.model(md, id); + }); + mesh.api.group.new(models, id) + .centerModels() + .centerXY() + .floor() + .focus(); + } + } + }) +} + function ui_build() { // set app version $h('app-name', "Mesh:Tool"); @@ -190,6 +224,7 @@ function space_init(data) { case 'Backspace': case 'Delete': for (let s of selection.list()) { + selection.remove(s); s.showBounds(false); s.remove(); } @@ -314,6 +349,7 @@ gapp.finalize("main.mesh", [ "moto.client", // dep: moto.client "moto.broker", // dep: moto.broker "moto.space", // dep: moto.space + "data.index", // dep: data.index "mesh.api", // dep: mesh.api "mesh.model", // dep: mesh.model "load.file", // dep: load.file diff --git a/src/mesh/api.js b/src/mesh/api.js index e4310b2a..32d31ac0 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -191,8 +191,8 @@ let group = { }, // @param group {MeshModel[]} - new(models) { - return group.add(new mesh.group(models)); + new(models, id) { + return group.add(new mesh.group(models, id)); }, // @param group {MeshGroup} diff --git a/src/mesh/group.js b/src/mesh/group.js index 1b879097..83c04044 100644 --- a/src/mesh/group.js +++ b/src/mesh/group.js @@ -26,8 +26,8 @@ let worker = moto.client.fn; mesh.group = class MeshGroup extends mesh.object { // @param group {MeshModel[]} - constructor(models = []) { - super(); + constructor(models = [], id) { + super(id); this.group = new THREE.Group(); this.models = []; for (let model of models) { @@ -51,11 +51,14 @@ mesh.group = class MeshGroup extends mesh.object { space.update(); call.model_add({model, group:this}); worker.group_add({id: this.id, model:model.id}); + // update data store + mesh.db.space.put(this.id, this.models.map(m => m.id)); return this; } // @param model {MeshModel} remove(model) { + // console.log('group',this,'remove',model); // remove all models and group if (arguments.length === 0) { for (let m of this.models.slice()) { @@ -63,18 +66,24 @@ mesh.group = class MeshGroup extends mesh.object { } return; } - model.group = undefined; this.models.remove(model); this.group.remove(model.mesh); // create message for listeners call.model_remove({model, group:this}); // trigger sync with worker - worker.group_remove({id: this.id, model:model.id}); + worker.group_remove({id: this.id, model: model.id}); + // ensure worker cleanup of model model.remove(true); // auto-remove group when empty if (this.group.children.length === 0) { mesh.api.group.remove(this); + // update worker state worker.object_destroy({id: this.id}); + // update object store + mesh.db.space.remove(this.id); + } else { + // update data store + mesh.db.space.put(this.id, this.models.map(m => m.id)); } space.update(); return this; diff --git a/src/mesh/model.js b/src/mesh/model.js index 5d315762..102e3da4 100644 --- a/src/mesh/model.js +++ b/src/mesh/model.js @@ -16,6 +16,7 @@ gapp.register("mesh.model", [ let mesh = self.mesh = self.mesh || {}; if (mesh.model) return; +let mapp = mesh; let space = moto.Space; let worker = moto.client.fn; @@ -41,8 +42,8 @@ let materials = mesh.material = { /** 3D model rendered on plaform **/ mesh.model = class MeshModel extends mesh.object { - constructor(data) { - super(); + constructor(data, id) { + super(id); let { file, mesh } = data; if (!mesh) { @@ -57,6 +58,9 @@ mesh.model = class MeshModel extends mesh.object { this.file = file || 'unnamed'; this.mesh = this.load(mesh); + + // persist in db so it can be restored on page load + mapp.db.space.put(this.id, { file, mesh }); } type() { @@ -179,9 +183,16 @@ mesh.model = class MeshModel extends mesh.object { remove() { if (arguments.length === 0) { // direct call requires pass through group + this.group.remove(this); + this.group = undefined; + this.removed = 'pending'; } else { - // called from group + // update worker state worker.object_destroy({id: this.id}); + // update object store + mesh.db.space.remove(this.id); + // tag removed for debugging + this.removed = 'complete'; } } }; diff --git a/src/mesh/object.js b/src/mesh/object.js index 5aab2055..14473cc0 100644 --- a/src/mesh/object.js +++ b/src/mesh/object.js @@ -18,8 +18,8 @@ let worker = moto.client.fn; mesh.object = class MeshObject { - constructor() { - this.id = mesh.util.uuid(); + constructor(id) { + this.id = id || mesh.util.uuid(); worker.object_create({id: this.id, type: this.type()}); } diff --git a/src/mesh/work.js b/src/mesh/work.js index 4a8c97fd..3f68d49a 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -27,11 +27,6 @@ function cacheUpdate(id, data) { Object.assign(cache[id], data); } -worker.bind("debug", (data, send) => { - console.log({work_cache: cache}); - send.done(); -}); - let model = { load(data) { let { vertices, name, id } = data; @@ -93,10 +88,16 @@ let object = { } }; +worker.bind("debug", (data, send) => { + console.log({work_cache: cache}); +}); + worker.bindObject({ model, group, object }); +worker.ready(); + })(); diff --git a/src/moto/client.js b/src/moto/client.js index 39de9520..fe3357ff 100644 --- a/src/moto/client.js +++ b/src/moto/client.js @@ -11,29 +11,51 @@ let moto = self.moto = self.moto || {}, workurl = null, // url to load worker worknum = 1, // number of workers to start workers = [], // array of all workers - queue = []; // array of work requests + queue = [], // array of work requests + topics = {}; // topic handler (broker lite for worker > client msgs) if (moto.client) return; -gapp.register('moto.client'); +gapp.register('moto.client', [ + "add.array", // dep: add.array +]); + +function dispatch(topic, message) { + for (let fn of topics[topic] || []) { + fn(message); + } +} // code is running in the browser / client context let client = moto.client = { + /** @returns [integer] total number of workers **/ live: () => { return workers.length; }, + /** @returns [integer] number of idle workers **/ free: () => { return workers.filter(w => w.run === null).length; }, + /** @returns [integer] max cpu cores available **/ max: () => { return ccmax; }, + /** process worker topic messages like "ready" **/ + on: (topic, fn) => { + (topics[topic] = topics[topic] || []).addOnce(fn); + }, + + /** + * @returns {Object} registered worker function map + * auto-filled when worker process uses bind() or bindObject() + **/ fn: {}, + /** binds a function endpoint to a task name **/ bind: (task) => { if (!client.fn[task]) { client.fn[task] = (data, options) => { @@ -42,6 +64,7 @@ let client = moto.client = { } }, + /** add a promised task call to the work queue **/ call: (task, data, options = {}) => { let { direct, stream } = options; return new Promise((resolve, reject) => { @@ -62,11 +85,13 @@ let client = moto.client = { }); }, + /** danger: directly add an entry to the work queue **/ queue: (task, data, options = {}) => { queue.push({task, data, options}); client.kick(); }, + /** check work queue and distribute tasks to free workers **/ kick: () => { if (queue.length === 0) { return; @@ -172,11 +197,19 @@ let client = moto.client = { worker.index = i; worker.onmessage = function(e) { let reply = e.data; + + // handle special case for task binding if (reply.bind) { client.bind(reply.bind); return; } + // handle special case when worker ready + if (reply.ready) { + dispatch('ready', reply.ready); + return; + } + if (worker.run === null) { console.log({client_unhandled: e}); throw "message on idle worker"; diff --git a/src/moto/worker.js b/src/moto/worker.js index 48bd3593..ea9db643 100644 --- a/src/moto/worker.js +++ b/src/moto/worker.js @@ -34,6 +34,10 @@ const dispatch = moto.worker = { } }, + ready() { + dispatch.send({ready: true}); + }, + send(msg) { self.postMessage(msg); },