refactoring...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2022-08-24 23:13:24 +03:00
parent 6e62ef782f
commit c2ee30502b
4 changed files with 298 additions and 116 deletions

View File

@ -1237,7 +1237,9 @@ object.Constructor('Page', BasePage, {
var state = {depends} var state = {depends}
var data = { render_root: this } var data = { render_root: this }
return this.get(path, data) return this.get(path, data)
.parse(this.get(tpl, data).raw, state) }).call(this) }, .parse(
this.get('/'+tpl, data).raw,
state) }).call(this) },
set text(value){ set text(value){
this.__update__({text: value}) }, this.__update__({text: value}) },
//this.onTextUpdate(value) }, //this.onTextUpdate(value) },
@ -1368,10 +1370,10 @@ var wikiword = require('./dom/wikiword')
var pWikiPageElement = var pWikiPageElement =
module.pWikiPageElement = module.pWikiPageElement =
/* XXX CACHE... // XXX CACHE...
object.Constructor('pWikiPageElement', Page, {
/*/
object.Constructor('pWikiPageElement', CachedPage, { object.Constructor('pWikiPageElement', CachedPage, {
/*/
object.Constructor('pWikiPageElement', Page, {
//*/ //*/
dom: undefined, dom: undefined,
@ -1381,8 +1383,11 @@ object.Constructor('pWikiPageElement', CachedPage, {
wikiword: wikiword.wikiWordText, wikiword: wikiword.wikiWordText,
}, },
//__clone_constructor__: Page, // XXX CACHE
__clone_constructor__: CachedPage, __clone_constructor__: CachedPage,
/*/
__clone_constructor__: Page,
//*/
__clone_proto: undefined, __clone_proto: undefined,
get __clone_proto__(){ get __clone_proto__(){

View File

@ -41,6 +41,23 @@ module = {
'/System', '/System',
], ],
/*/ XXX NORMCACHE...
__normalized_cache_threshold: 100,
__normalized_cache_size: 4096,
__normalized_cache: undefined,
get _normalized_cache(){
var norm = this.__normalized =
this.__normalized
?? new Set()
// trim to size...
var l = norm.size
var lim = this.__normalized_cache_size ?? 1000
var t = this.__normalized_cache_threshold ?? 100
if(l > lim){
norm = this.__normalized = new Set([...norm].slice(Math.max(l - lim - t, t))) }
return norm },
//*/
// Path utils... // Path utils...
// //
// Path can be in one of two formats: // Path can be in one of two formats:
@ -50,9 +67,10 @@ module = {
// NOTE: trailing/leading '/' are represented by '' at end/start of // NOTE: trailing/leading '/' are represented by '' at end/start of
// path list... // path list...
normalize: function(path='.', format='auto'){ normalize: function(path='.', format='auto'){
/*/ XXX RENORMALIZE... /*/ XXX NORMCACHE...
// do not re-normalize... if(typeof(path) == 'string'
if(path.normalized && format != 'array'){ && format != 'array'
&& this._normalized_cache.has(path)){
return path } return path }
//*/ //*/
format = format == 'auto' ? format = format == 'auto' ?
@ -86,24 +104,28 @@ module = {
// NOTE: the last '>>' will be retained... // NOTE: the last '>>' will be retained...
: res.push(e) : res.push(e)
return res }, []) return res }, [])
return format == 'string' ? /*/ XXX NORMCACHE...
var res = format == 'string' ?
// special case: root -> keep '/'
((root
&& path.length == 1
&& path[0] == '') ?
('/'+ path.join('/'))
: path.join('/'))
: path
typeof(res) == 'string'
&& this._normalized_cache.add(res)
return res },
/*/
return format == 'string' ?
// special case: root -> keep '/' // special case: root -> keep '/'
/*/ XXX RENORMALIZE...
Object.assign(
new String((root
&& path.length == 1
&& path[0] == '') ?
('/'+ path.join('/'))
: path.join('/')),
{normalized: true})
/*/
((root ((root
&& path.length == 1 && path.length == 1
&& path[0] == '') ? && path[0] == '') ?
('/'+ path.join('/')) ('/'+ path.join('/'))
: path.join('/')) : path.join('/'))
//*/
: path }, : path },
//*/
split: function(path){ split: function(path){
return this.normalize(path, 'array') }, return this.normalize(path, 'array') },
join: function(...parts){ join: function(...parts){
@ -237,6 +259,15 @@ module = {
for(var page of [...this.ALTERNATIVE_PAGES]){ for(var page of [...this.ALTERNATIVE_PAGES]){
yield* this.paths(path.concat(page), seen) }} }, yield* this.paths(path.concat(page), seen) }} },
names: function(path='/'){
path = this.normalize(path, 'string')
var name = path == '/' ?
this.ROOT_PAGE
: this.basename(path)
return name == '' ?
this.ALTERNATIVE_PAGES.slice()
: [name, ...this.ALTERNATIVE_PAGES] },
// XXX EXPERIMENTAL... // XXX EXPERIMENTAL...
// //

View File

@ -13,6 +13,44 @@ var types = require('ig-types')
var pwpath = require('../path') var pwpath = require('../path')
//---------------------------------------------------------------------
//
// cached(<name>, <update>[, ...<args>])
// cached(<name>, <get>, <update>[, ...<args>])
// -> <func>
//
// NOTE: in the first case (no <get>) the first <args> item can not be
// a function...
//
// XXX better introspection???
var cached =
module.cached =
function(name, get, update, ...args){
name = `__${name}_cache`
if(typeof(update) != 'function'){
args.unshift(update)
update = get
get = null }
return update instanceof types.AsyncFunction ?
async function(){
var cache = this[name] =
this[name]
?? await update.call(this)
return get ?
get.call(this, cache, ...arguments)
: cache }
: function(){
var cache = this[name] =
this[name]
?? update.call(this)
return get ?
get.call(this, cache, ...arguments)
: cache } }
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// Store... // Store...
@ -68,33 +106,70 @@ module.BaseStore = {
// XXX might be a good idea to cache this... // XXX might be a good idea to cache this...
__paths__: async function(){ __paths__: async function(){
return Object.keys(this.data) }, return Object.keys(this.data) },
//* XXX uncached...
// local paths...
__paths: cached('paths', async function(){
return this.__paths__() }),
// XXX should this also be cached???
paths: async function(local=false){ paths: async function(local=false){
return this.__paths__() return this.__paths()
.iter() .iter()
// XXX NEXT
.concat((!local && (this.next || {}).paths) ? .concat((!local && (this.next || {}).paths) ?
this.next.paths() this.next.paths()
: []) }, : []) },
/*/
__paths_cache_timeout: 1000, // local names...
__paths_cache_timer: undefined, __names: cached('names', async function(){
__paths_cache: undefined, return this.__paths()
paths: async function(local=false){ .iter()
this.__paths_cache_timer = .reduce(function(res, path){
this.__paths_cache_timer var n = pwpath.basename(path)
?? setTimeout(function(){ if(!n.includes('*')){
delete this.__paths_cache_timer (res[n] = res[n] ?? []).push(path) }
delete this.__paths_cache return res }, {}) }),
}.bind(this), this.__paths_cache_timeout ?? 1000) // XXX should this also be cached???
return this.__paths_cache names: async function(local=false){
|| this.__paths__() return {
.iter() ...(!local && (this.next || {}).names ?
// XXX NEXT await this.next.names()
.concat((!local && (this.next || {}).paths) ? : {}),
this.next.paths() ...await this.__names(),
: []) }, } },
//*/
__cache_add: function(path){
if(this.__paths_cache){
this.__paths_cache.includes(path)
|| this.__paths_cache.push(path) }
if(this.__names_cache){
var name = pwpath.basename(path)
var names = (this.__names_cache[name] =
this.__names_cache[name]
?? [])
names.includes(path)
|| names.push(path) }
return this },
__cache_remove: function(path){
if(this.__paths_cache){
var paths = this.__paths_cache
paths.splice(
paths.indexOf(
paths.includes(path) ?
path
: path[0] == '/' ?
path.slice(1)
: '/'+path),
1) }
if(this.__names_cache){
var name = pwpath.basename(path)
var names = (this.__names_cache[name] =
this.__names_cache[name]
?? [])
var i = names.indexOf(path)
i >= 0
&& names.splice(i, 1)
if(names.length == 0){
delete this.__names_cache[name] } }
return this },
// //
// .exists(<path>) // .exists(<path>)
@ -107,7 +182,7 @@ module.BaseStore = {
&& path }, && path },
exists: async function(path){ exists: async function(path){
path = pwpath.normalize(path, 'string') path = pwpath.normalize(path, 'string')
return (await this.__exists__(path, 'string')) return (await this.__exists__(path))
// NOTE: all paths at this point and in store are // NOTE: all paths at this point and in store are
// absolute, so we check both with the leading // absolute, so we check both with the leading
// '/' and without it to make things a bit more // '/' and without it to make things a bit more
@ -127,10 +202,30 @@ module.BaseStore = {
// normalize the output... // normalize the output...
|| false }, || false },
// find the closest existing alternative path... // find the closest existing alternative path...
// XXX CACHED....
find: async function(path, strict=false){
// build list of existing page candidates...
var names = await this.names()
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 }
p = p[0] == '/' ?
p.slice(1)
: '/'+p
if(pages.has(p)){
return p } } },
/*/
find: async function(path, strict=false){ find: async function(path, strict=false){
for(var p of pwpath.paths(path, !!strict)){ for(var p of pwpath.paths(path, !!strict)){
if(p = await this.exists(p)){ if(p = await this.exists(p)){
return p } } }, return p } } },
//*/
// //
// Resolve page for path // Resolve page for path
// .match(<path>) // .match(<path>)
@ -167,7 +262,14 @@ module.BaseStore = {
.replace(/\*\*/g, '.*') .replace(/\*\*/g, '.*')
.replace(/(?<=^|[\\\/]+|[^.])\*/g, '[^\\/]*') .replace(/(?<=^|[\\\/]+|[^.])\*/g, '[^\\/]*')
}(?=[\\\\\/]|$)`) }(?=[\\\\\/]|$)`)
/*/ XXX CACHED....
var name = pwpath.basename(path)
return [...(name.includes('*') ?
await this.paths()
: await (this.names())[name])
/*/
return [...(await this.paths()) return [...(await this.paths())
//*/
// NOTE: we are not using .filter(..) here as wee // NOTE: we are not using .filter(..) here as wee
// need to keep parts of the path only and not // need to keep parts of the path only and not
// return the whole thing... // return the whole thing...
@ -194,7 +296,11 @@ module.BaseStore = {
// //
// This is like .match(..) for non-pattern paths and paths ending // This is like .match(..) for non-pattern paths and paths ending
// with '/'; When patterns end with a non-pattern then match the // with '/'; When patterns end with a non-pattern then match the
// basedir and add the basename to each resulting path... // 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(..)??? // XXX should this be used by .get(..) instead of .match(..)???
// XXX EXPERIMENTAL // XXX EXPERIMENTAL
@ -307,11 +413,13 @@ module.BaseStore = {
ctime: Date.now(), ctime: Date.now(),
}, },
(mode == 'update' && exists) ? (mode == 'update' && exists) ?
await this.get(path) await this.__get__(path)
: {}, : {},
data, data,
{mtime: Date.now()}) {mtime: Date.now()})
await this.__update__(path, data, mode) await this.__update__(path, data, mode)
// XXX CACHED
this.__cache_add(path)
return this }, return this },
__delete__: async function(path){ __delete__: async function(path){
delete this.data[path] }, delete this.data[path] },
@ -320,8 +428,10 @@ module.BaseStore = {
if(this.__delete__ == null){ if(this.__delete__ == null){
return this } return this }
path = await this.exists(path) path = await this.exists(path)
path if(path){
&& await this.__delete__(path) await this.__delete__(path)
// XXX CACHED
this.__cache_remove(path) }
return this }, return this },
// XXX NEXT might be a good idea to have an API to move pages from // XXX NEXT might be a good idea to have an API to move pages from
@ -398,13 +508,6 @@ module.BaseStore = {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//
// XXX stores to experiment with:
// - cache
// - fs
// - PouchDB
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Meta-Store // Meta-Store
// //
@ -412,58 +515,49 @@ module.BaseStore = {
// be handled by nested stores. // be handled by nested stores.
// //
// XXX might be a good idea to normalize args...
var metaProxy = var metaProxy =
function(meth, drop_cache=false, post){ function(name, pre, post){
var target = meth.replace(/__/g, '')
if(typeof(drop_cache) == 'function'){
post = drop_cache
drop_cache = false }
var func = async function(path, ...args){ var func = async function(path, ...args){
var store = this.substore(path) path = pre ?
await pre.call(this, path, ...args)
: path
var res = var p = this.substore(path)
store == null ? if(p){
object.parentCall(MetaStore[meth], this, path, ...args) var res = this.substores[p][name](
: this.data[store][target]( path.slice(path.indexOf(p)+p.length),
// NOTE: we are normalizing for root/non-root paths... ...args)
path.slice(path.indexOf(store)+store.length), } else {
...args) var res = object.parentCall(MetaStore[name], this, ...arguments) }
if(drop_cache){ return post ?
delete this.__substores } post.call(this, await res, path, ...args)
post : res }
&& (res = post.call(this, await res, store, path, ...args)) Object.defineProperty(func, 'name', {value: name})
return res}
Object.defineProperty(func, 'name', {value: meth})
return func } return func }
// XXX this gets stuff from .data, can we avoid this???
// ...this can restrict this to being in-memory...
// XXX not sure about the name... // XXX not sure about the name...
// XXX should this be a mixin??? // XXX should this be a mixin???
var MetaStore = var MetaStore =
module.MetaStore = { module.MetaStore = {
__proto__: BaseStore, __proto__: BaseStore,
//data: undefined, //
// Format:
// {
// <path>: <store>,
// ...
// }
//
substores: undefined,
__substores: undefined,
get substores(){
return this.__substores
?? (this.__substores = Object.entries(this.data)
.filter(function([path, value]){
return object.childOf(value, BaseStore) })
.map(function([path, _]){
return path })) },
// XXX do we need to account for trailing '/' here??? // XXX do we need to account for trailing '/' here???
substore: function(path){ substore: function(path){
path = pwpath.normalize(path, 'string') path = pwpath.normalize(path, 'string')
if(this.substores.includes(path)){ if(path in (this.substores ?? {})){
return path } return path }
var root = path[0] == '/' var root = path[0] == '/'
var store = this.substores var store = Object.keys(this.substores ?? {})
// normalize store paths to the given path... // normalize store paths to the given path...
.filter(function(p){ .filter(function(p){
return path.startsWith( return path.startsWith(
@ -478,45 +572,87 @@ module.MetaStore = {
undefined undefined
: store }, : store },
getstore: function(path){ getstore: function(path){
return this.data[this.substore(path)] }, return (this.substores ?? {})[this.substore(path)] },
// XXX do we need to account for trailing '/' here??? // XXX do we need to account for trailing '/' here???
isStore: function(path){ isStore: function(path){
if(!this.substores){
return false }
path = pwpath.normalize(path, 'string') path = pwpath.normalize(path, 'string')
path = path[0] == '/' ? path = path[0] == '/' ?
path.slice(1) path.slice(1)
: path : path
return this.substores.includes(path) return !!this.substores[path]
|| this.substores.includes('/'+ path) }, || !!this.substores['/'+ path] },
// XXX this depends on .data having keys... // NOTE: we are using level2 API here to enable mixing this with
__paths__: async function(){ // store adapters that can overload the level1 API to implement
// their own stuff...
paths: async function(){
var that = this var that = this
var data = this.data var stores = await Promise.iter(
//return Object.keys(data) Object.entries(this.substores ?? {})
return Promise.iter(Object.keys(data) .map(function([path, store]){
.map(function(path){ return store.paths()
return object.childOf(data[path], BaseStore) ? .iter()
data[path].paths() .map(function(s){
.iter() return pwpath.join(path, s) }) }))
.map(function(s){ .flat()
return pwpath.join(path, s) }) return object.parentCall(MetaStore.paths, this, ...arguments)
: path })) .iter()
.flat() }, .concat(stores) },
// XXX revise... names: async function(){
__exists__: metaProxy('__exists__', var that = this
// normalize path... var res = await object.parentCall(MetaStore.names, this, ...arguments)
function(res, store, path){ await Promise.all(Object.entries(this.substores ?? {})
return (store && res) ? .map(async function([path, store]){
path return Object.entries(await store.names())
: res }), .map(function([name, paths]){
__get__: metaProxy('__get__'), res[name] = (res[name] ?? [])
__delete__: metaProxy('__delete__', true), .concat(paths
// XXX BUG: this does not create stuff in sub-store... .map(function(s){
__update__: metaProxy('__update__', true), return pwpath.join(path, s) })) }) }))
return res },
exists: metaProxy('exists',
//async function(path){
// return this.resolve(path) },
null,
function(res, path){
var s = this.substore(path)
return res == false ?
res
: s ?
pwpath.join(s, res)
: res }),
get: metaProxy('get',
async function(path){
return this.resolve(path) }),
metadata: metaProxy('metadata'),
update: async function(path, data, mode='update'){
data = data instanceof Promise ?
await data
: data
// add substore...
if(object.childOf(data, BaseStore)){
;(this.substores = this.substores ?? {})[path] = data
return this }
// add to substore...
var p = this.substore(path)
if(p){
this.substores[p].update(
// trim path...
path.slice(path.indexOf(p)+p.length),
...[...arguments].slice(1))
return this }
// add local...
return object.parentCall(MetaStore.update, this, ...arguments) },
// XXX Q: how do we delete a substore???
delete: metaProxy('delete'),
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX might be a fun idea to actually use this as a backend for BaseStore... // XXX might be a fun idea to actually use this as a backend for BaseStore...

View File

@ -1,6 +1,16 @@
/********************************************************************** /**********************************************************************
* *
* *
* XXX BUG:
* await pwiki.get('/* /path').text -> '' (wrong)
* XXX NORMCACHE .normalize(..) cache normalized strings...
* ...seems to have little impact...
* XXX MATCH limit candidates to actual page name matches -- this will
* limit the number of requests to actual number of pages with that
* name...
* e.g. when searching for xxx/tree the only "tree" available is
* System/tree, and if it is overloaded it's now a question of
* picking one out of two and not out of tens generated by .paths()
* XXX CACHE match pattern paths -- to catch page creation... * XXX CACHE match pattern paths -- to catch page creation...
* 1) explicit subpath matching -- same as .match(..) * 1) explicit subpath matching -- same as .match(..)
* 2) identify recursive patterns -- same as ** * 2) identify recursive patterns -- same as **