improve indexdb promised. fix multiple spaces access. add workspace persist + restore for mesh

This commit is contained in:
Stewart Allen 2022-01-03 17:13:50 -05:00
commit 96145b370b
9 changed files with 192 additions and 64 deletions

View file

@ -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();
};
})();

View file

@ -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

View file

@ -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}

View file

@ -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;

View file

@ -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';
}
}
};

View file

@ -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()});
}

View file

@ -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();
})();

View file

@ -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";

View file

@ -34,6 +34,10 @@ const dispatch = moto.worker = {
}
},
ready() {
dispatch.send({ready: true});
},
send(msg) {
self.postMessage(msg);
},