mirror of
https://github.com/flynx/pWiki.git
synced 2025-10-28 09:30:07 +00:00
1560 lines
44 KiB
JavaScript
Executable File
1560 lines
44 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
|
|
(function(require){ var module={} // make module AMD/node compatible...
|
|
/*********************************************************************/
|
|
|
|
var flexsearch = require('flexsearch')
|
|
|
|
var object = require('ig-object')
|
|
var types = require('ig-types')
|
|
|
|
var pwpath = require('../path')
|
|
var index = require('../index')
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
// XXX EXPERIMENTAL...
|
|
// XXX this will not work on nodejs...
|
|
// ...possible solutions would be:
|
|
// LevelDB
|
|
// indexeddb (npm)
|
|
// XXX see bugs/issues...
|
|
// XXX not sure if we need this...
|
|
var JournalDB =
|
|
module.JournalDB =
|
|
object.Constructor('JournalDB', {
|
|
id: undefined,
|
|
__version__: 1,
|
|
|
|
__promisify: async function(req){
|
|
return new Promise(function(resolve, reject){
|
|
req.onsuccess = function(){
|
|
resolve(req.result) }
|
|
req.onerror = function(evt){
|
|
reject(evt.error) } }) },
|
|
|
|
__db: undefined,
|
|
get db(){
|
|
if(this.__db){
|
|
return this.__db }
|
|
|
|
var that = this
|
|
var id = this.id
|
|
|
|
if(id == null){
|
|
throw new Error('JournalDB(..): need an id.') }
|
|
if(typeof(indexedDB) == 'undefined'){
|
|
throw new Error('JournalDB(..): indexedDB not supported.') }
|
|
|
|
return new Promise(function(resolve, reject){
|
|
var req = indexedDB.open(id, this.__version__)
|
|
// setup/upgrade...
|
|
req.onupgradeneeded = function(evt){
|
|
var old = evt.oldVersion
|
|
if(old > this.__version__){
|
|
throw new Error(`JournalDB(..): stored version (${old}) newer than expected: ${this.__version__}`) }
|
|
// setup/update...
|
|
var db = that.__db = req.result
|
|
switch(old){
|
|
// setup...
|
|
case 0:
|
|
console.log('JournalDB.db: create.')
|
|
var journal = db.createObjectStore('journal', {keyPath: 'date'})
|
|
journal.createIndex('path', 'path')
|
|
// update...
|
|
case 1:
|
|
// update code...
|
|
}
|
|
resolve(db) }
|
|
req.onversionchange = function(evt){
|
|
// XXX close connections and reload...
|
|
// ...this means that two+ versions of code opened the
|
|
// same db and we need to reload the older versions...
|
|
// XXX
|
|
}
|
|
req.onerror = function(evt){
|
|
reject(evt.error) }
|
|
req.onsuccess = function(evt){
|
|
that.__db = req.result } }) },
|
|
|
|
// XXX should these be props???
|
|
// XXX should these be cached???
|
|
get length(){ return async function(){
|
|
return this.__promisify(
|
|
(await this.db)
|
|
.transaction('journal', 'readonly')
|
|
.objectStore('journal')
|
|
.count()) }.call(this) },
|
|
get paths(){ return async function(){
|
|
return (await this.slice())
|
|
.map(function(elem){
|
|
return elem.path })
|
|
.unique() }.call(this) },
|
|
|
|
// get entries by time...
|
|
slice: async function(from=0, to=Infinity){
|
|
return this.__promisify(
|
|
(await this.db)
|
|
.transaction('journal', 'readonly')
|
|
.objectStore('journal')
|
|
.getAll(IDBKeyRange.bound(
|
|
from ?? 0,
|
|
to ?? Infinity,
|
|
true, true))) },
|
|
// get entries by path...
|
|
path: async function(path){
|
|
return this.__promisify(
|
|
(await this.db)
|
|
.transaction('journal', 'readonly')
|
|
.objectStore('journal')
|
|
.index('path')
|
|
.getAll(...arguments)) },
|
|
|
|
//
|
|
// <journal-db>.add(<path>, <action>, ...)
|
|
// -> <promise>
|
|
//
|
|
add: async function(path, action, ...data){
|
|
return this.__promisify(
|
|
(await this.db)
|
|
.transaction('journal', 'readwrite')
|
|
.objectStore('journal')
|
|
.add({
|
|
// high-resolution date...
|
|
date: Date.hires(),
|
|
path,
|
|
action,
|
|
data,
|
|
})) },
|
|
|
|
// remove entries up to date...
|
|
trim: async function(to){
|
|
return this.__promisify(
|
|
(await this.db)
|
|
.transaction('journal', 'readwrite')
|
|
.objectStore('journal')
|
|
.delete(IDBKeyRange.upperBound(to ?? 0, true))) },
|
|
// clear journal...
|
|
// XXX BUG? for some reason .deleteDatabase(..) does not actually delete
|
|
// the database until a reload thus messing up the db creation
|
|
// process (as the .onversionchange event is not triggered)...
|
|
clear: function(){
|
|
indexedDB.deleteDatabase(this.id)
|
|
delete this.__db },
|
|
|
|
//
|
|
// JournalDB(<id>)
|
|
// -> <journal-db>
|
|
//
|
|
__init__: function(id){
|
|
var that = this
|
|
this.id = id ?? this.id
|
|
this.db },
|
|
})
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// Store...
|
|
|
|
//
|
|
// API levels:
|
|
// Level 1 -- implementation API
|
|
// This level is the base API, this is used by all other Levels.
|
|
// This is the only level that needs to be fully overloaded by store
|
|
// implementations (no super calls necessary).
|
|
// The base methods that need to be overloaded for the store to work:
|
|
// .__paths__()
|
|
// -> <keys>
|
|
// .__exists__(path, ..)
|
|
// -> <path>
|
|
// -> false
|
|
// .__get__(path, ..)
|
|
// -> <data>
|
|
// Optional for r/w stores:
|
|
// .__update__(path, ..)
|
|
// .__delete__(path, ..)
|
|
// Level 2 -- feature API
|
|
// This can use Level 1 and Level 2 internally.
|
|
// When overloading it is needed to to call the super method to
|
|
// retain base functionality.
|
|
// All overloading here is optional.
|
|
// .paths
|
|
// -> <path-list>
|
|
// .names
|
|
// -> <name-index>
|
|
//
|
|
// .exists(<path>)
|
|
// -> <real-path>
|
|
// -> false
|
|
// .get(<path>)
|
|
// -> <data>
|
|
// -> undefined
|
|
// .metadata(<path>[, <data>])
|
|
// -> <store> -- on write
|
|
// -> <data>
|
|
// -> undefined
|
|
// .update(<path>, <data>)
|
|
// -> <store>
|
|
// .delete(<path>)
|
|
// -> <store>
|
|
// .load(..)
|
|
// -> <store>
|
|
// .json(..)
|
|
// -> <json>
|
|
// Level 3
|
|
// ...
|
|
//
|
|
//
|
|
// To create a store adapter:
|
|
// - inherit from BaseStore
|
|
// - overload:
|
|
// .__paths__()
|
|
// -> <keys>
|
|
// .__exists__(path, ..)
|
|
// -> <path>
|
|
// -> false
|
|
// .__get__(path, ..)
|
|
// -> <data>
|
|
// - optionally (for writable stores)
|
|
// .__update__(path, ..)
|
|
// .__delete__(path, ..)
|
|
// .load(..)
|
|
//
|
|
//
|
|
// NOTE: store keys must be normalized to avoid conditions where two
|
|
// forms of the same path exist at the same time...
|
|
//
|
|
//
|
|
// XXX potential architectural problems:
|
|
// - .paths
|
|
// external index -- is this good???
|
|
// bottleneck??
|
|
// cache/index???
|
|
// ...can we avoid this??
|
|
//
|
|
// XXX might be a good idea to split this into a generic store and a MemStore...
|
|
// XXX LEADING_SLASH should this be strict about leading '/' in paths???
|
|
// ...this may lead to duplicate paths created -- '/a/b' and 'a/b'
|
|
// XXX should we support page symlinking???
|
|
// XXX async: not sure if we need to return this from async methods...
|
|
var BaseStore =
|
|
module.BaseStore = {
|
|
|
|
// XXX revise naming...
|
|
next: undefined,
|
|
|
|
|
|
// NOTE: .data is not part of the spec and can be implementation-specific,
|
|
// only .__<name>__(..) use it internally... (XXX check this)
|
|
__data: undefined,
|
|
get data(){
|
|
return this.__data
|
|
?? (this.__data = {}) },
|
|
set data(value){
|
|
this.__data = value },
|
|
|
|
__cache_path__: '.cache',
|
|
// XXX not sure if it is better to set these here or use index.IndexManagerMixin(..)
|
|
get index_attrs(){
|
|
return [...index.iter(this)] },
|
|
index: async function(action='get', ...args){
|
|
return index.index(this, ...arguments) },
|
|
|
|
//
|
|
// Format:
|
|
// [
|
|
// <path>,
|
|
// ...
|
|
// ]
|
|
//
|
|
__paths__: async function(){
|
|
return Object.keys(this.data) },
|
|
__paths_merge__: async function(data){
|
|
return (await data)
|
|
.concat((this.next
|
|
&& 'paths' in this.next) ?
|
|
await this.next.paths
|
|
: [])
|
|
.unique() },
|
|
__paths_isvalid__: function(t){
|
|
var changed =
|
|
!!this.__paths_next_exists != !!this.next
|
|
|| (!!this.next
|
|
&& this.next.__paths_modified > t)
|
|
this.__paths_next_exists = !this.next
|
|
return changed },
|
|
__paths: index.makeIndex('paths', {
|
|
update: async function(data, name, path){
|
|
data = await data
|
|
// XXX normalize???
|
|
data.includes(path)
|
|
|| data.push(path)
|
|
return data },
|
|
remove: async function(data, name, path){
|
|
data = await data
|
|
// XXX normalize???
|
|
data.includes(path)
|
|
&& data.splice(data.indexOf(path), 1)
|
|
return data }, }),
|
|
// XXX should this clone the data???
|
|
// XXX should we use 'lazy'???
|
|
get paths(){
|
|
return this.__paths() },
|
|
|
|
//
|
|
// Format:
|
|
// {
|
|
// <name>: [
|
|
// <path>,
|
|
// ...
|
|
// ],
|
|
// ...
|
|
// }
|
|
//
|
|
__names_isvalid__: function(t){
|
|
return this.__paths_isvalid__(t) },
|
|
// NOTE: this is built from .paths so there is no need to define a
|
|
// way to merge...
|
|
__names: index.makeIndex('names',
|
|
async function(){
|
|
var update = this.__names.options.update
|
|
var index = {}
|
|
for(var path of (await this.paths)){
|
|
index = update.call(this, index, 'names', path) }
|
|
return index }, {
|
|
update: async function(data, name, path){
|
|
data = await data
|
|
// XXX normalize???
|
|
var n = pwpath.basename(path)
|
|
if(!n.includes('*')
|
|
&& !(data[n] ?? []).includes(path)){
|
|
(data[n] = data[n] ?? []).push(path) }
|
|
return data },
|
|
remove: async function(data, name, path){
|
|
data = await data
|
|
// XXX normalize???
|
|
var n = pwpath.basename(path)
|
|
if(!(n in data)){
|
|
return data }
|
|
data[n].includes(path)
|
|
&& data[n].splice(data[n].indexOf(path), 1)
|
|
data[n].length == 0
|
|
&& (delete data[n])
|
|
return data }, }),
|
|
// XXX should this clone the data???
|
|
// XXX should we use 'lazy'???
|
|
get names(){
|
|
return this.__names() },
|
|
|
|
// XXX tags
|
|
//
|
|
// Format:
|
|
// {
|
|
// tags: {
|
|
// <tag>: Set([
|
|
// <path>,
|
|
// ...
|
|
// ]),
|
|
// ...
|
|
// },
|
|
// paths: {
|
|
// <path>: Set([
|
|
// <tag>,
|
|
// ...
|
|
// ]),
|
|
// ...
|
|
// }
|
|
// }
|
|
//
|
|
// XXX should this be here???
|
|
parseTags: function(str){
|
|
return str
|
|
.split(/\s*(?:([a-zA-Z1-9_-]+)|"(.+)"|'(.+)')\s*/g)
|
|
.filter(function(t){
|
|
return t
|
|
&& t != ''
|
|
&& t != ',' }) },
|
|
// XXX do we need these???
|
|
// ...the question is if we have .__tags__(..) how do we
|
|
// partially .__tags_merge__(..) things???
|
|
//__tags__: function(){ },
|
|
//__tags_merge__: function(data){ },
|
|
__tags_isvalid__: function(t){
|
|
return this.__paths_isvalid__(t) },
|
|
__tags: index.makeIndex('tags',
|
|
async function(){
|
|
// load index...
|
|
var path = this.__cache_path__ +'/tags_index'
|
|
if(this.__cache_path__
|
|
&& await this.exists(path)){
|
|
return this.__tags.options.load.call(this, null, 'tags')
|
|
// generate...
|
|
} else {
|
|
var index = {tags: {}, paths: {}}
|
|
var update = this.__tags.options.update
|
|
for(var path of (await this.paths)){
|
|
index = update.call(this, index, 'tags', path, await this.get(path)) }
|
|
return index } }, {
|
|
update: async function(data, name, path, update){
|
|
/*/ XXX CACHE_INDEX...
|
|
// do not index cache...
|
|
if(this.__cache_path__
|
|
&& path.startsWith(this.__cache_path__)){
|
|
return data }
|
|
//*/
|
|
// remove obsolete tags...
|
|
data = await this.__tags.options.remove.call(this, data, name, path)
|
|
// add...
|
|
var {tags, paths} = data
|
|
paths[path] = new Set(update.tags)
|
|
for(var tag of update.tags ?? []){
|
|
;(tags[tag] =
|
|
tags[tag] ?? new Set([]))
|
|
.add(path) }
|
|
return data },
|
|
remove: async function(data, name, path){
|
|
data = await data
|
|
if(!data){
|
|
return data }
|
|
var {tags, paths} = data
|
|
for(var tag of paths[path] ?? []){
|
|
tags[tag].delete(path) }
|
|
return data },
|
|
// XXX EXPERIMENTAL...
|
|
save: async function(data, name){
|
|
if(this.__cache_path__){
|
|
this.update(
|
|
this.__cache_path__ +'/'+ name+'_index',
|
|
{index: data})}
|
|
return data },
|
|
load: async function(data, name){
|
|
if(this.__cache_path__){
|
|
var path = this.__cache_path__ +'/'+ name+'_index'
|
|
var {index} = await this.get(path) ?? {}
|
|
return index ?? data }
|
|
return data },
|
|
reset: function(_, name){
|
|
this.__cache_path__
|
|
&& this.delete(this.__cache_path__ +'/'+ name+'_index') }, }),
|
|
get tags(){
|
|
return this.__tags() },
|
|
|
|
relatedTags: function*(...tags){
|
|
var cur = new Set(tags)
|
|
var {tags, paths} = this.tags
|
|
var seen = new Set()
|
|
for(var tag of cur){
|
|
for(var p of tags[tag] ?? []){
|
|
for(var t of paths[p] ?? []){
|
|
if(!seen.has(t)
|
|
&& !cur.has(t)){
|
|
seen.add(t)
|
|
yield t } } } } },
|
|
|
|
|
|
// XXX text search index (???)
|
|
// XXX do we index .data.text or .raw or .text
|
|
// XXX should we have separate indexes for path, text and tags or
|
|
// cram the three together?
|
|
// XXX need to store this...
|
|
/* XXX
|
|
__search_options: {
|
|
preset: 'match',
|
|
tokenize: 'full',
|
|
},
|
|
//*/
|
|
__search: index.makeIndex('search',
|
|
// XXX do a load if present...
|
|
async function(){
|
|
// load index...
|
|
var path = this.__cache_path__ +'/search_index'
|
|
if(this.__cache_path__
|
|
&& await this.exists(path)){
|
|
return this.__search.options.load.call(this, null, 'search')
|
|
// generate...
|
|
} else {
|
|
var index = new flexsearch.Index(
|
|
this.__search_options
|
|
?? {})
|
|
var update = this.__search.options.update
|
|
for(var path of (await this.paths)){
|
|
update.call(this, index, 'search', path, await this.get(path)) }
|
|
return index } }, {
|
|
update: async function(data, name, path, update){
|
|
/*/ XXX CACHE_INDEX...
|
|
// do not index cache...
|
|
if(this.__cache_path__
|
|
&& path.startsWith(this.__cache_path__)){
|
|
return data }
|
|
//*/
|
|
var {text, tags} = update
|
|
text = [
|
|
'',
|
|
path,
|
|
(text
|
|
&& typeof(text) != 'function') ?
|
|
text
|
|
: '',
|
|
tags ?
|
|
'#'+ tags.join(' #')
|
|
: '',
|
|
].join('\n\n')
|
|
;(await data)
|
|
.add(path, text)
|
|
// handle changes...
|
|
//this.__search.options.__save_changes.call(this, name, 'update', path, update)
|
|
return data },
|
|
remove: async function(data, name, path){
|
|
data = await data
|
|
data
|
|
&& data.remove(path)
|
|
// handle changes...
|
|
//this.__search.options.__save_changes.call(this, name, 'remove', path)
|
|
return data },
|
|
// XXX EXPERIMENTAL...
|
|
// XXX for this to work need to figure out how to save a page
|
|
// without triggering an index update...
|
|
// ...and do it fast!!
|
|
__save_changes: async function(name, action, path, ...args){
|
|
if(this.__cache_path__
|
|
&& !path.startsWith(this.__cache_path__)){
|
|
var p = [this.__cache_path__, name+'_index', 'changes'].join('/')
|
|
// XXX can we get/update in one op???
|
|
var {changes} = await this.get(p) ?? {}
|
|
changes = changes ?? []
|
|
changes.push([Date.now(), action, path, ...args])
|
|
// XXX this needs not to neither trigger handlers nor .index('update', ...)...
|
|
return this.__update(p, {changes}, 'unindexed') } },
|
|
save: async function(data, name){
|
|
if(this.__cache_path__){
|
|
var that = this
|
|
var path = this.__cache_path__ +'/'+ name+'_index'
|
|
//this.delete(path +'/changes')
|
|
// XXX HACK this thing runs async but does not return a promise...
|
|
// ...this is quote ugly but I can't figure out a way to
|
|
// track when exporting is done...
|
|
var index = {}
|
|
data.export(
|
|
function(key, value){
|
|
index[key] = value
|
|
return that.update(path, {index}) }) }
|
|
/*/
|
|
var index = {}
|
|
data.export(
|
|
function(key, value){
|
|
index[key] = value })
|
|
this.update(path, {index}) }
|
|
//*/
|
|
return data },
|
|
load: async function(data, name){
|
|
if(this.__cache_path__){
|
|
var path = this.__cache_path__ +'/'+ name+'_index'
|
|
var changes = path +'/changes'
|
|
var {index} = await this.get(path) ?? {}
|
|
var data =
|
|
new flexsearch.Index(
|
|
this.__search_options
|
|
?? {})
|
|
for(var [key, value] of Object.entries(index ?? {})){
|
|
data.import(key, value) } }
|
|
return data },
|
|
reset: function(_, name){
|
|
this.__cache_path__
|
|
&& this.delete(this.__cache_path__ +'/'+ name+'_index') }, }),
|
|
search: function(...args){
|
|
var s = this.__search()
|
|
return s instanceof Promise ?
|
|
s.then(function(s){
|
|
return s.search(...args)})
|
|
: s.search(...args) },
|
|
|
|
|
|
// XXX EXPERIMENTAL...
|
|
// This keeps the changes between saves...
|
|
//
|
|
// XXX see issues with indexedDB...
|
|
// XXX not sure if we need this...
|
|
// XXX need a persistent fast store of changes...
|
|
//__journal_id__: 'pWiki:test-journal',
|
|
__journal_db: undefined,
|
|
__journal: index.makeIndex('journal',
|
|
function(){
|
|
// XXX stub...
|
|
var journal = this.__journal_db =
|
|
this.__journal_db
|
|
?? (this.__journal_id__ ?
|
|
JournalDB(this.__journal_id__)
|
|
: undefined)
|
|
return journal ?
|
|
journal.slice()
|
|
: [] }, {
|
|
|
|
'__call__': function(data, name, from, to){
|
|
if(typeof(from) == 'object'){
|
|
var {from, to} = from }
|
|
var _get = function(data){
|
|
return data
|
|
.filter(function(elem){
|
|
return (!from
|
|
|| elem[0] > from)
|
|
&& (!to
|
|
|| elem[0] <= to)}) }
|
|
return data instanceof Promise ?
|
|
data.then(_get)
|
|
: _get(data) },
|
|
|
|
// XXX these need to be persistent...
|
|
update: async function(data, name, path, update){
|
|
var date = this.__journal_db ?
|
|
this.__journal_db.add(path, 'update', update)
|
|
: Date.now()
|
|
data = await data
|
|
// XXX need to reuse the actual date...
|
|
data.push([Date.now(), 'update', path, update])
|
|
return data },
|
|
remove: async function(data, name, path){
|
|
var date = this.__journal_db ?
|
|
this.__journal_db.add(path, 'update', update)
|
|
: Date.now()
|
|
data = await data
|
|
// XXX need to reuse the actual date...
|
|
data.push([Date.now(), 'remove', path])
|
|
return data },
|
|
|
|
save: async function(data, name){
|
|
this.__journal_db
|
|
&& this.__journal_db.clear()
|
|
return [] },
|
|
// XXX if .__journal_db is not empty then need to apply it to the index???
|
|
load: async function(data, name){
|
|
// XXX
|
|
return data },
|
|
|
|
reset: function(data, name){
|
|
this.__journal_db
|
|
&& this.__journal_db.clear()
|
|
return [] }, }),
|
|
journal: function(){
|
|
return this.__journal('__call__', ...arguments)},
|
|
//*/
|
|
|
|
|
|
|
|
//
|
|
// .sort(<pattern>, <by>, ..)
|
|
// .sort([<path>, ..], <by>, ..)
|
|
// -> <paths>
|
|
// -> <promise(paths)>
|
|
//
|
|
// <by> ::=
|
|
// 'path'
|
|
// | 'location'
|
|
// | 'dir'
|
|
// | 'name'
|
|
// | 'title'
|
|
// | 'depth'
|
|
// | <attr>
|
|
// | <cmp-func>
|
|
//
|
|
//
|
|
// NOTE: to reverse search order prepend '-' to method name, e.g. cahnge
|
|
// 'path' to '-path', etc.
|
|
// NOTE: all path based <by> values are sync, not requireing a .gat(..)
|
|
// and thus faster than sorting via arbitrary <attr>...
|
|
// NOTE: this performs a natural sort, i.e. numbers in strings are
|
|
// treated as numbers and not as strings of characters making
|
|
// "page2" precede "page10".
|
|
__sort_method__: {
|
|
// NOTE: path/location are special cases as they do not transform
|
|
// the path -- they are hard-coded in .sort(..)...
|
|
//path: function(p){ return p },
|
|
//location: function(p){ return p },
|
|
dir: pwpath.dirname.bind(pwpath),
|
|
name: pwpath.basename.bind(pwpath),
|
|
title: function(p){
|
|
return pwpath.decodeElem(
|
|
pwpath.basename(p)) },
|
|
depth: function(p){
|
|
return pwpath.split(p).length },
|
|
// XXX not sure if this is needed...
|
|
//number: function(p){
|
|
// return p
|
|
// .split(/[^\d]+/g)
|
|
// .join(' ') },
|
|
},
|
|
sort: function(paths, ...by){
|
|
var that = this
|
|
paths =
|
|
(paths.includes('*')
|
|
|| paths.includes('**')) ?
|
|
this.match(paths).iter()
|
|
: paths
|
|
// natural compare arrays...
|
|
var nsplit = function(e){
|
|
return typeof(e) == 'string' ?
|
|
e.split(/(\d+)/g)
|
|
.map(function(e){
|
|
var i = parseInt(e)
|
|
return isNaN(i) ?
|
|
e
|
|
: i })
|
|
: e }
|
|
var ncmp = function(a, b){
|
|
// skip the equal part...
|
|
var i = 0
|
|
while(a[i] != null
|
|
&& b[i] != null
|
|
&& a[i] == b[i]){
|
|
i++ }
|
|
return (a[i] == null
|
|
&& b[i] == null) ?
|
|
0
|
|
: (a[i] == null
|
|
|| a[i] < b[i]) ?
|
|
-1
|
|
: (b[i] == null
|
|
|| a[i] > b[i]) ?
|
|
1
|
|
: 0 }
|
|
var _async = false
|
|
return paths
|
|
// pre-fetch and prep all the needed data...
|
|
// XXX we can try and make this lazy and only get the data
|
|
// we need when we need it (in sort)...
|
|
// ...not sure if this is worth it...
|
|
.map(function(p, i){
|
|
var d
|
|
var res = []
|
|
// NOTE: this is the first and only instance so far where
|
|
// using let is simpler than doing this below:
|
|
// function(cmp){
|
|
// return (d = d ?? that.get(p))
|
|
// .then(function(data){
|
|
// return data[cmp] }) }.bind(this, cmp)
|
|
// ..still not sure if this is worth the special case...
|
|
for(let cmp of by){
|
|
cmp =
|
|
// ignore '-'/reverse
|
|
(typeof(cmp) == 'string'
|
|
&& cmp[0] == '-') ?
|
|
cmp.slice(1)
|
|
: cmp
|
|
res.push(
|
|
nsplit(
|
|
(cmp == 'path'
|
|
|| cmp == 'location') ?
|
|
p
|
|
: cmp in that.__sort_method__ ?
|
|
that.__sort_method__[cmp].call(that, p)
|
|
// async attr...
|
|
: typeof(cmp) == 'string' ?
|
|
// NOTE: we only get page data once per page...
|
|
(d = d ?? that.get(p))
|
|
.then(function(data){
|
|
return data[cmp] })
|
|
: null)) }
|
|
_async = _async || !!d
|
|
return d ?
|
|
// wait for data to resolve...
|
|
Promise.all([p, i, Promise.all(res)])
|
|
: [p, i, res] })
|
|
// NOTE: if one of the sort attrs is async we need to wrap the
|
|
// whole thing in a promise...
|
|
.run(function(){
|
|
return _async ?
|
|
Promise.all(this).iter()
|
|
: this })
|
|
.sort(function([a, ia, ca], [b, ib, cb]){
|
|
for(var [i, cmp] of by.entries()){
|
|
var res =
|
|
typeof(cmp) == 'string' ?
|
|
((ca[i] == null
|
|
&& cb[i] == null) ?
|
|
0
|
|
// natural cmp...
|
|
: (ca[i] instanceof Array
|
|
&& cb[i] instanceof Array) ?
|
|
ncmp(ca[i], cb[i])
|
|
: ca[i] instanceof Array ?
|
|
ncmp(ca[i], [cb[i]])
|
|
: cb[i] instanceof Array ?
|
|
ncmp([ca[i]], cb[i])
|
|
// normal indexed cmp...
|
|
: (cb[i] == null
|
|
|| ca[i] < cb[i]) ?
|
|
-1
|
|
: (ca[i] == null
|
|
|| ca[i] > cb[i]) ?
|
|
1
|
|
: 0)
|
|
: typeof(cmp) == 'function' ?
|
|
cmp(a, b)
|
|
: 0
|
|
// reverse...
|
|
res = cmp[0] == '-' ?
|
|
res * -1
|
|
: res
|
|
// got a non equal...
|
|
if(res != 0){
|
|
return res } }
|
|
// keep positions if all comparisons are equal...
|
|
return ia - ib })
|
|
.map(function([p]){
|
|
return p }) },
|
|
|
|
//
|
|
// .exists(<path>)
|
|
// -> <promise>
|
|
// -> <normalized-path>
|
|
// -> false
|
|
//
|
|
exists: function(path){
|
|
var {path, args} =
|
|
pwpath.splitArgs(
|
|
pwpath.sanitize(path, 'string'))
|
|
return Promise.awaitOrRun(
|
|
this.paths,
|
|
function(paths){
|
|
return paths.includes(path) ?
|
|
pwpath.joinArgs(path, args)
|
|
: undefined }) },
|
|
|
|
// find the closest existing alternative path...
|
|
find: function(path, strict=false){
|
|
var {path, args} = pwpath.splitArgs(path)
|
|
args = pwpath.joinArgs('', args)
|
|
return Promise.awaitOrRun(
|
|
this.names,
|
|
function(names){
|
|
// build list of existing page candidates...
|
|
var pages = new Set(
|
|
pwpath.names(path)
|
|
.map(function(name){
|
|
return names[name] ?? [] })
|
|
.flat())
|
|
// select accessible candidate...
|
|
for(var p of pwpath.paths(path, !!strict)){
|
|
if(pages.has(p)){
|
|
return p+args }
|
|
p = p[0] == '/' ?
|
|
p.slice(1)
|
|
: '/'+p
|
|
if(pages.has(p)){
|
|
return p+args } } }) },
|
|
//
|
|
// Resolve page for path
|
|
// .match(<path>)
|
|
// -> <path>
|
|
//
|
|
// Match paths (non-strict mode)
|
|
// .match(<pattern>)
|
|
// .match(<pattern>, false)
|
|
// -> [<path>, ...]
|
|
// -> []
|
|
//
|
|
// Match pages (paths in strict mode)
|
|
// .match(<pattern>, true)
|
|
// -> [<path>, ...]
|
|
// -> []
|
|
//
|
|
// In strict mode the trailing star in the pattern will only match
|
|
// actual existing pages, while in non-strict mode the pattern will
|
|
// match all sub-paths.
|
|
//
|
|
// Handled path arguments:
|
|
// all
|
|
// sort=<methods>
|
|
// sortnewlast (default)
|
|
// sortnewfirst
|
|
//
|
|
__match_args__: {
|
|
//
|
|
// <tag-name>: function(value, args){
|
|
// // setup state if needed and pre-check...
|
|
// // ...
|
|
// return no_check_needed ?
|
|
// false
|
|
// // path predicate...
|
|
// : function(path){
|
|
// if( path_matches ){
|
|
// return true } } },
|
|
//
|
|
// NOTE: handlers are run in order of definition.
|
|
//
|
|
tags: async function(tags){
|
|
tags = typeof(tags) == 'string' ?
|
|
this.parseTags(tags)
|
|
: false
|
|
tags && await this.tags
|
|
return tags
|
|
&& function(path){
|
|
// tags -> skip untagged pages...
|
|
var t = this.tags.paths[path]
|
|
if(!t){
|
|
return false }
|
|
for(var tag of tags){
|
|
if(!t || !t.has(tag)){
|
|
return false } }
|
|
return true } },
|
|
search: async function(search){
|
|
search = search
|
|
&& new Set(await this.search(search))
|
|
return search instanceof Set
|
|
&& function(path){
|
|
// nothing found or not in match list...
|
|
if(search.size == 0
|
|
|| !search.has(path)){
|
|
return false }
|
|
return true } },
|
|
// XXX EXPERIMENTAL...
|
|
// XXX add page/page-size???
|
|
offset: function(offset){
|
|
offset = parseInt(offset)
|
|
return !!offset
|
|
&& function(path){
|
|
offset--
|
|
return !(offset >= 0) } },
|
|
count: function(count){
|
|
count = parseInt(count)
|
|
return !!count
|
|
&& function(path){
|
|
count--
|
|
return !!(count >= 0) } },
|
|
},
|
|
__match_args: async function(args){
|
|
var that = this
|
|
var predicates = []
|
|
for(var [key, gen] of Object.entries(this.__match_args__ ?? {})){
|
|
var p = await gen.call(this, args[key], args)
|
|
p && predicates.push(p) }
|
|
return predicates.length > 0 ?
|
|
function(path){
|
|
for(var p of predicates){
|
|
if(!p.call(that, path)){
|
|
return false } }
|
|
return true }
|
|
: undefined },
|
|
match: async function(path, strict=false){
|
|
var that = this
|
|
// pattern match * / **
|
|
if(path.includes('*')
|
|
|| path.includes('**')){
|
|
var {path, args} = pwpath.splitArgs(path)
|
|
path = pwpath.sanitize(path)
|
|
|
|
var all = args.all
|
|
var sort = args.sort
|
|
var newlast =
|
|
args.sortnewlast
|
|
?? !(args.sortnewfirst
|
|
// default is sortnewlast...
|
|
?? false)
|
|
var test = await this.__match_args(args)
|
|
args = pwpath.joinArgs('', args)
|
|
|
|
var order = (await this.metadata(path) ?? {}).order || []
|
|
|
|
// NOTE: we are matching full paths only here so leading and
|
|
// trainling '/' are optional...
|
|
var pattern = new RegExp(`^\\/?`
|
|
+RegExp.quoteRegExp(path)
|
|
// pattern: **
|
|
.replace(/\\\*\\\*/g, '(.*)')
|
|
// pattern: *
|
|
// NOTE: we are prepping the leading '.' of a pattern
|
|
// dir for hidden tests...
|
|
.replace(/(^|\\\/+)(\\\.|)([^\/]*)\\\*/g, '$1$2($3[^\\/]*)')
|
|
+'(?=[\\/]|$)', 'g')
|
|
return [...(await this.paths)
|
|
// NOTE: we are not using .filter(..) here as wee
|
|
// need to keep parts of the path only and not
|
|
// return the whole thing...
|
|
.reduce(function(res, p){
|
|
// skip metadata paths...
|
|
if(p.includes('*')){
|
|
return res }
|
|
// check path: stage 1
|
|
var m = [...p.matchAll(pattern)]
|
|
var visible = m.length > 0
|
|
&& (!all ?
|
|
// test if we need to hide things....
|
|
m.reduce(function(res, m){
|
|
return res === false ?
|
|
res
|
|
: !/(^\.|[\\\/]\.)/.test(m[1])
|
|
}, true)
|
|
: true)
|
|
// args...
|
|
// NOTE: this needs to be between path checking
|
|
// stages as we need to skip paths depending
|
|
// on the all argument...
|
|
if(visible
|
|
&& test
|
|
&& !test(p)){
|
|
return res }
|
|
// check path: stage 2
|
|
visible
|
|
&& (m = m[0])
|
|
&& (!strict
|
|
|| m[0] == p)
|
|
&& res.add(
|
|
// normalize the path elements...
|
|
m[0][0] == '/' ?
|
|
m[0].slice(1)
|
|
: m[0])
|
|
return res }, new Set())]
|
|
// handle live sort...
|
|
.run(function(){
|
|
return (sort && sort !== true) ?
|
|
that
|
|
.sort(this, ...sort.split(/\s*[,\s]+/g))
|
|
:this
|
|
.sortAs(order,
|
|
newlast ?
|
|
'head'
|
|
: 'tail') })
|
|
.map(function(p){
|
|
return p+args })}
|
|
// direct search...
|
|
return this.find(path, strict) },
|
|
//
|
|
// .resolve(<path>)
|
|
// -> <path>
|
|
//
|
|
// .resolve(<pattern>)
|
|
// -> [<path>, ...]
|
|
// -> []
|
|
//
|
|
// This is like .match(..) for non-pattern paths and paths ending
|
|
// with '/'; When patterns end with a non-pattern then match the
|
|
// basedir and add the basename to each resulting path, e.g.:
|
|
// .match('/*/tree')
|
|
// -> ['System/tree']
|
|
// .resolve('/*/tree')
|
|
// -> ['System/tree', 'Dir/tree', ...]
|
|
//
|
|
// XXX should this be used by .get(..) instead of .match(..)???
|
|
resolve: async function(path, strict){
|
|
// pattern match * / **
|
|
if(path.includes('*')
|
|
|| path.includes('**')){
|
|
var p = pwpath.splitArgs(path)
|
|
var args = pwpath.joinArgs('', p.args)
|
|
p = pwpath.split(p.path)
|
|
var tail = []
|
|
while(!p.at(-1).includes('*')){
|
|
tail.unshift(p.pop()) }
|
|
tail = tail.join('/')
|
|
if(tail.length > 0){
|
|
return (await this.match(
|
|
p.join('/') + args,
|
|
strict))
|
|
.map(function(p){
|
|
var {path, args} = pwpath.splitArgs(p)
|
|
return pwpath.joinArgs(pwpath.join(path, tail), args) }) } }
|
|
// direct...
|
|
return this.match(path, strict) },
|
|
//
|
|
// Resolve page
|
|
// .get(<path>)
|
|
// -> <value>
|
|
//
|
|
// Resolve pages (non-strict mode)
|
|
// .get(<pattern>)
|
|
// .get(<pattern>, false)
|
|
// -> [<value>, .. ]
|
|
//
|
|
// Get pages (strict mode)
|
|
// .get(<pattern>, true)
|
|
// -> [<value>, .. ]
|
|
//
|
|
// In strict mode this will not try to resolve pages and will not
|
|
// return pages at paths that do not explicitly exist.
|
|
//
|
|
// XXX should this call actions???
|
|
// XXX should this return a map for pattern matches???
|
|
__get__: function(key){
|
|
return this.data[key] },
|
|
get: async function(path, strict=false, energetic=false){
|
|
var that = this
|
|
path = pwpath.sanitize(path, 'string')
|
|
var path = pwpath.splitArgs(path).path
|
|
path = path.includes('*')
|
|
&& (energetic == true ?
|
|
await this.find(path)
|
|
: await this.isEnergetic(path))
|
|
|| await this.resolve(path, strict)
|
|
//*/
|
|
return path instanceof Array ?
|
|
// XXX should we return matched paths???
|
|
Promise.iter(path)
|
|
.map(function(p){
|
|
// NOTE: p can match a non existing page at this point,
|
|
// this can be the result of matching a/* in a a/b/c
|
|
// and returning a a/b which can be undefined...
|
|
return that.get(p, strict) })
|
|
: (await this.__get__(path)
|
|
?? ((this.next || {}).get
|
|
&& this.next.get(path, strict))) },
|
|
|
|
isEnergetic: async function(path){
|
|
var p = await this.find(path)
|
|
return !!(await this.get(p, true) ?? {}).energetic
|
|
&& p },
|
|
|
|
//
|
|
// Get metadata...
|
|
// .metadata(<path>)
|
|
// -> <metadata>
|
|
// -> undefined
|
|
//
|
|
// Set metadata...
|
|
// .metadata(<path>, <data>[, <mode>])
|
|
// .update(<path>, <data>[, <mode>])
|
|
//
|
|
// Delete metadata...
|
|
// .delete(<path>)
|
|
//
|
|
// NOTE: .metadata(..) is the same as .data but supports pattern paths
|
|
// and does not try to acquire a target page.
|
|
// NOTE: setting/removing metadata is done via .update(..) / .delete(..)
|
|
// NOTE: this uses .__get__(..) internally...
|
|
metadata: async function(path, ...args){
|
|
path = pwpath.splitArgs(path).path
|
|
// set...
|
|
if(args.length > 0){
|
|
return this.update(path, ...args) }
|
|
// get...
|
|
path = await this.exists(path)
|
|
return path
|
|
&& (await this.__get__(path)
|
|
?? (this.next
|
|
&& await this.next.metadata(path)))
|
|
|| undefined },
|
|
|
|
// NOTE: deleting and updating only applies to explicit matching
|
|
// paths -- no page acquisition is performed...
|
|
// NOTE: edit methods are local-only...
|
|
// NOTE: if .__update__ and .__delete__ are set to null/false this
|
|
// will quietly go into read-only mode...
|
|
// XXX do we copy the data here or modify it????
|
|
__update__: async function(key, data, mode='update'){
|
|
this.data[key] = data },
|
|
//
|
|
// Unindexed update...
|
|
// .__update(<path>, <data>, 'unindexed')
|
|
// .__update(<path>, <data>, 'unindexed', <mode>)
|
|
// -> <data>
|
|
//
|
|
__update: async function(path, data, mode='update'){
|
|
// handle unindexed mode...
|
|
var index = true
|
|
if(mode == 'unindexed'){
|
|
index = false
|
|
mode = arguments[3]
|
|
?? 'update' }
|
|
// read-only...
|
|
if(this.__update__ == null){
|
|
return data }
|
|
var exists = await this.exists(path)
|
|
path = exists
|
|
|| pwpath.sanitize(path, 'string')
|
|
path = pwpath.splitArgs(path).path
|
|
data = data instanceof Promise ?
|
|
await data
|
|
: data
|
|
data =
|
|
typeof(data) == 'function' ?
|
|
data
|
|
: Object.assign(
|
|
{
|
|
__proto__: data.__proto__,
|
|
ctime: Date.now(),
|
|
},
|
|
(mode == 'update' && exists) ?
|
|
await this.__get__(path)
|
|
: {},
|
|
data,
|
|
{mtime: Date.now()})
|
|
await this.__update__(path, data, mode)
|
|
index
|
|
&& this.index('update', path, data, mode)
|
|
return data },
|
|
// XXX can we do a blanket .index('update', ...) here??
|
|
// ...currently this will mess up caches between .next/.substores
|
|
// and the top level store to an inconsistent state...
|
|
// ...this could be a sign of problems with index -- needs more
|
|
// tought...
|
|
update: types.event.Event('update',
|
|
async function(handler, path, data, mode='update'){
|
|
handler(false)
|
|
var res = await this.__update(...[...arguments].slice(1))
|
|
handler()
|
|
return this }),
|
|
|
|
__delete__: async function(path){
|
|
delete this.data[path] },
|
|
__delete: async function(path, mode='normal'){
|
|
// read-only...
|
|
if(this.__delete__ == null){
|
|
return this }
|
|
path = pwpath.splitArgs(path).path
|
|
path = await this.exists(path)
|
|
if(typeof(path) == 'string'){
|
|
await this.__delete__(path)
|
|
this.index('remove', path) }
|
|
return this },
|
|
delete: types.event.Event('delete',
|
|
async function(handler, path){
|
|
handler(false)
|
|
var res = await this.__delete(path)
|
|
handler()
|
|
return res }),
|
|
|
|
// XXX NEXT might be a good idea to have an API to move pages from
|
|
// current store up the chain...
|
|
|
|
// load/json protocol...
|
|
//
|
|
// The .load(..) / .json(..) methods have two levels of implementation:
|
|
// - generic
|
|
// uses .update(..) and .paths/.get(..) and is usable as-is
|
|
// in any store adapter implementing the base protocol.
|
|
// - batch
|
|
// implemented via .__batch_load__(..) and .__batch_json__(..)
|
|
// methods and can be adapter specific.
|
|
//
|
|
// NOTE: the generic level does not care about the nested stores
|
|
// and other details, as it uses the base API and will produce
|
|
// full and generic result regardless of actual store topology.
|
|
// NOTE: implementations of the batch level need to handle nested
|
|
// stores correctly.
|
|
// XXX not sure if we can avoid this at this stage...
|
|
// NOTE: care must be taken with inheriting the batch protocol methods
|
|
// as they take precedence over the generic protocol. It is
|
|
// recommended to either overload them or simply assign null or
|
|
// undefined to them when inheriting from a non-base-store.
|
|
//__batch_load__: function(data){
|
|
// // ...
|
|
// return this },
|
|
load: async function(...data){
|
|
var input = {}
|
|
for(var e of data){
|
|
input = {...input, ...e} }
|
|
// batch loader (optional)...
|
|
if(this.__batch_load__){
|
|
this.__batch_load__(input)
|
|
// one-by-one loader...
|
|
} else {
|
|
for(var [path, value] of Object.entries(input)){
|
|
this.update(path, value) } }
|
|
//this.__update(path, value) } }
|
|
return this },
|
|
// NOTE: this will not serialize functions...
|
|
//__batch_json__: function(){
|
|
// // ...
|
|
// return json },
|
|
json: async function(options={}){
|
|
if(options === true){
|
|
options = {stringify: true} }
|
|
var {stringify, keep_funcs} = options
|
|
// batch...
|
|
if(this.__batch_json__){
|
|
var res = this.__batch_json__(stringify)
|
|
// generic...
|
|
} else {
|
|
var res = {}
|
|
for(var path of await this.paths){
|
|
var page = await this.get(path)
|
|
if(keep_funcs
|
|
|| typeof(page) != 'function'){
|
|
res[path] = page } } }
|
|
return (stringify
|
|
&& typeof(res) != 'string') ?
|
|
JSON.stringify(res, options.replacer, options.space)
|
|
: res },
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
// Meta-Store
|
|
//
|
|
// Extends BaseStore to handle other stores as pages. i.e. sub-paths can
|
|
// be handled by nested stores.
|
|
//
|
|
|
|
// XXX see inside...
|
|
var metaProxy =
|
|
function(name, pre, post){
|
|
var func = function(path, ...args){
|
|
var that = this
|
|
var _do = function(path){
|
|
var res
|
|
var p = that.substore(path)
|
|
if(p){
|
|
// XXX can this be strict in all cases???
|
|
res = that.substores[p][name](
|
|
path.slice(path.indexOf(p)+p.length),
|
|
...args) }
|
|
return res
|
|
?? object.parentCall(MetaStore[name], that, ...arguments) }
|
|
|
|
path = pre ?
|
|
pre.call(this, path, ...args)
|
|
: path
|
|
|
|
var res = path instanceof Promise ?
|
|
path.then(_do)
|
|
: _do(path)
|
|
|
|
return post ?
|
|
(res instanceof Promise ?
|
|
res.then(function(res){
|
|
return post.call(that, res, path, ...args) })
|
|
: post.call(this, res, path, ...args))
|
|
: res }
|
|
Object.defineProperty(func, 'name', {value: name})
|
|
return func }
|
|
|
|
// XXX not sure about the name...
|
|
// XXX should this be a mixin???
|
|
var MetaStore =
|
|
module.MetaStore = {
|
|
__proto__: BaseStore,
|
|
|
|
//
|
|
// Format:
|
|
// {
|
|
// <path>: <store>,
|
|
// ...
|
|
// }
|
|
//
|
|
substores: undefined,
|
|
|
|
substore: function(path){
|
|
path = pwpath.sanitize(path, 'string')
|
|
if(path in (this.substores ?? {})){
|
|
return path }
|
|
var store = Object.keys(this.substores ?? {})
|
|
// normalize store paths to the given path...
|
|
.filter(function(p){
|
|
return path.startsWith(p)
|
|
// only keep whole path elements...
|
|
// NOTE: this prevents matching 'a/b' with 'a/bbb', for example.
|
|
&& (path[p.length] == null
|
|
|| path[p.length] == '/'
|
|
|| path[p.length] == '\\')})
|
|
.sort(function(a, b){
|
|
return a.length - b.length })
|
|
.pop()
|
|
return store == path ?
|
|
// the actual store is not stored within itself...
|
|
undefined
|
|
: store },
|
|
getstore: function(path){
|
|
return (this.substores ?? {})[this.substore(path)] },
|
|
isStore: function(path){
|
|
if(!this.substores){
|
|
return false }
|
|
path = pwpath.sanitize(path, 'string')
|
|
// XXX do we need this???
|
|
return !!this.substores[path]
|
|
|| !!this.substores['/'+ path] },
|
|
|
|
// NOTE: we are using level2 API here to enable mixing this with
|
|
// store adapters that can overload the level1 API to implement
|
|
// their own stuff...
|
|
|
|
// XXX INDEX...
|
|
__paths_merge__: async function(data){
|
|
var that = this
|
|
var stores = await Promise.iter(
|
|
Object.entries(this.substores ?? {})
|
|
.map(function([path, store]){
|
|
return store.paths
|
|
.iter()
|
|
.map(function(s){
|
|
return pwpath.join(path, s) }) }))
|
|
.flat()
|
|
return object.parentCall(MetaStore.__paths_merge__, this, ...arguments)
|
|
.iter()
|
|
.concat(stores) },
|
|
__paths_isvalid__: function(t){
|
|
if(this.substores){
|
|
// match substore list...
|
|
var cur = Object.keys(this.substores ?? {})
|
|
var prev = this.__paths_substores ?? cur ?? []
|
|
if(prev.length != cur.length
|
|
|| (new Set([...cur, ...prev])).size != cur.length){
|
|
return false }
|
|
// check timestamps...
|
|
for(var {__paths_modified} of Object.values(this.substores ?? {})){
|
|
if(__paths_modified > t){
|
|
return false } } }
|
|
return object.parentCall(MetaStore.__paths_isvalid__, this, ...arguments) },
|
|
|
|
exists: metaProxy('exists',
|
|
//async function(path){
|
|
// return this.resolve(path) },
|
|
null,
|
|
function(res, path){
|
|
var s = this.substore(path)
|
|
return typeof(res) != 'string' ?
|
|
(this.next ?
|
|
this.next.exists(path)
|
|
: res)
|
|
//res
|
|
: s ?
|
|
pwpath.join(s, res)
|
|
: res }),
|
|
get: async function(path, strict=false){
|
|
path = await this.resolve(path, strict)
|
|
if(path == undefined){
|
|
return }
|
|
var res
|
|
var p = this.substore(path)
|
|
if(p){
|
|
res = await this.substores[p].get(
|
|
path.slice(path.indexOf(p)+p.length),
|
|
true) }
|
|
return res
|
|
?? object.parentCall(MetaStore.get, this, ...arguments) },
|
|
// XXX can't reach .next on get but will cheerfully mess things up
|
|
// on set (creating a local page)...
|
|
// ...should copy and merge...
|
|
metadata: metaProxy('metadata'),
|
|
// NOTE: we intentionally do not delegate to .next here...
|
|
__update: async function(path, data, mode='update'){
|
|
data = data instanceof Promise ?
|
|
await data
|
|
: data
|
|
// add substore...
|
|
if(object.childOf(data, BaseStore)){
|
|
path = pwpath.sanitize(path, 'string')
|
|
//data.index('clear')
|
|
;(this.substores = this.substores ?? {})[path] = data
|
|
return data }
|
|
// add to substore...
|
|
var p = this.substore(path)
|
|
if(p){
|
|
// XXX should this call .__update(..) ???
|
|
// ...if yes, how do we trigger the substore event, if no
|
|
// then how do we nut trigger the event if needed???
|
|
return this.substores[p].__update(
|
|
// trim path...
|
|
path.slice(path.indexOf(p)+p.length),
|
|
...[...arguments].slice(1))
|
|
return data }
|
|
// add local...
|
|
return object.parentCall(MetaStore.__update, this, ...arguments) },
|
|
// NOTE: this fully overloads the .update(..) events and duplicates
|
|
// it's functionality because we need to handle the .__update(..)
|
|
// call differently for .substores here and in .__update(..)...
|
|
update: types.event.Event('update',
|
|
async function(handler, path, data, mode='update'){
|
|
handler(false)
|
|
// add to substore...
|
|
var p = this.substore(path)
|
|
if(p){
|
|
var res = await this.substores[p].update(
|
|
// trim path...
|
|
path.slice(path.indexOf(p)+p.length),
|
|
...[...arguments].slice(2))
|
|
// local...
|
|
} else {
|
|
var res = await this.__update(...[...arguments].slice(1)) }
|
|
handler()
|
|
return this }),
|
|
// XXX Q: how do we delete a substore???
|
|
// XXX need to call .__cache_remove(..) here if we did not super-call...
|
|
__delete: metaProxy('__delete'),
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// XXX not used...
|
|
var cacheProxy = function(name){
|
|
var func = function(path, ...args){
|
|
var cache = (this.root ?? this).cache
|
|
return cache[path]
|
|
?? (cache[path] =
|
|
object.parentCall(CachedStore[name], this, ...arguments)) }
|
|
Object.defineProperty(func, 'name', {value: name})
|
|
return func }
|
|
|
|
// XXX should this be a level-1 or level-2???
|
|
// XXX make this a mixin...
|
|
// XXX add cache invalidation strategies...
|
|
// - timeout
|
|
// - count
|
|
// XXX BROKEN...
|
|
var CachedStore =
|
|
module.CachedStore = {
|
|
__proto__: MetaStore,
|
|
|
|
__cache: undefined,
|
|
get cache(){
|
|
return (this.__cache = this.__cache ?? {}) },
|
|
set cache(value){
|
|
this.__cache = value },
|
|
|
|
clearCache: function(){
|
|
this.cache = {}
|
|
return this },
|
|
|
|
exists: function(path){
|
|
return (path in this.cache ?
|
|
path
|
|
: false)
|
|
|| object.parentCall(CachedStore.exists, this, ...arguments) },
|
|
// XXX this sometimes caches promises...
|
|
get: async function(path){
|
|
return this.cache[path]
|
|
?? (this.cache[path] =
|
|
await object.parentCall(CachedStore.get, this, ...arguments)) },
|
|
__update: async function(path, data){
|
|
var that = this
|
|
delete this.cache[path]
|
|
var res = object.parentCall(CachedStore.__update, this, ...arguments)
|
|
// re-cache in the background...
|
|
res.then(async function(){
|
|
that.cache[path] = await that.get(path) })
|
|
return res },
|
|
/* XXX
|
|
metadata: async function(path, data){
|
|
if(data){
|
|
// XXX this is wrong -- get merged data...
|
|
this.cache[path] = data
|
|
return object.parentCall(CachedStore.metadata, this, ...arguments)
|
|
} else {
|
|
return this.cache[path]
|
|
?? (this.cache[path] =
|
|
await object.parentCall(CachedStore.metadata, this, ...arguments)) } },
|
|
//*/
|
|
__delete: async function(path){
|
|
delete this.cache[path]
|
|
return object.parentCall(CachedStore.__delete, this, ...arguments) },
|
|
}
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
var Store =
|
|
module.Store =
|
|
MetaStore
|
|
//CachedStore
|
|
|
|
|
|
|
|
/**********************************************************************
|
|
* vim:set ts=4 sw=4 nowrap : */ return module })
|