lots of tweaks and fixes...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2022-08-04 19:47:08 +03:00
parent 7ce3e6f8bc
commit e7a9610d81
32 changed files with 21775 additions and 68 deletions

View File

@ -13,19 +13,25 @@ LOCAL_MODULES := \
node_modules/ig-actions/actions.js \
node_modules/ig-features/features.js
EXT_MODULES := \
$(wildcard node_modules/pouchdb/dist/*) \
$(wildcard node_modules/jszip/dist/*) \
$(wildcard node_modules/showdown/dist/*)
POUCH_DB := \
$(wildcard node_modules/pouchdb/dist/*)
ext-lib/pouchdb.js: node_modules $(POUCH_DB)
cp $(POUCH_DB) ext-lib/
lib/types: node_modules
mkdir -p $@
cp node_modules/ig-types/*js $@
bootstrap.js: scripts/bootstrap.js $(BOOTSTRAP_FILES)
node $<
.PHONY: bootstrap
bootstrap: bootstrap.js
@ -34,8 +40,9 @@ node_modules:
npm install
dev: node_modules ext-lib/pouchdb.js $(LOCAL_MODULES) bootstrap
dev: node_modules lib/types $(EXT_MODULES) $(LOCAL_MODULES) bootstrap
cp $(LOCAL_MODULES) lib/
cp $(EXT_MODULES) ext-lib/
clean:

View File

@ -52,6 +52,10 @@ pwiki.store.update('@pouch', {
})
// XXX
typeof(Bootstrap) != 'undefined'
&& pwiki.store.load(Bootstrap)
/**********************************************************************

11577
ext-lib/jszip.js Executable file

File diff suppressed because it is too large Load Diff

14
ext-lib/jszip.min.js vendored

File diff suppressed because one or more lines are too long

5157
ext-lib/showdown.js Executable file

File diff suppressed because it is too large Load Diff

1
ext-lib/showdown.js.map Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
ext-lib/showdown.min.js.map Executable file

File diff suppressed because one or more lines are too long

View File

@ -29,12 +29,49 @@ var WIKIWORD_PATTERN =
'\\\\?\\[[^\\]]+\\]',
].join('|') +')', 'g')
// XXX REVISE...
var setWikiWords =
module.setWikiWords =
function(text, show_brackets=true, skip){
skip = skip ?? []
skip = skip instanceof Array ?
skip
: [skip]
return text
// set new...
.replace(
WIKIWORD_PATTERN,
function(l){
// check if WikiWord is escaped...
if(l[0] == '\\'){
return l.slice(1) }
var path = l[0] == '[' ?
l.slice(1, -1)
: l
var i = [].slice.call(arguments).slice(-2)[0]
// XXX HACK check if we are inside a tag...
var rest = text.slice(i+1)
if(rest.indexOf('>') < rest.indexOf('<')){
return l }
return skip.indexOf(l) < 0 ?
('<a '
+'class="wikiword" '
+'href="#'+ path +'" '
+'bracketed="'+ (show_brackets && l[0] == '[' ? 'yes' : 'no') +'" '
//+'onclick="event.preventDefault(); go($(this).attr(\'href\').slice(1))" '
+'>'
+ (!!show_brackets ? path : l)
+'</a>')
: l })}
module.wikiWord =
Filter(
{quote: 'quote-wikiword'},
function(source){
// XXX
return source })
return setWikiWords(source) })
module.quoteWikiWord =
function(source){
// XXX

View File

@ -8,6 +8,7 @@
/*********************************************************************/
var base = require('./base')
var showdown = require('showdown')
//---------------------------------------------------------------------
@ -16,8 +17,12 @@ module.markdown =
base.Filter(
{quote: 'quote-markdown'},
function(source){
// XXX
return source })
var converter = new showdown.Converter({
strikethrough: true,
tables: true,
tasklists: true,
})
return converter.makeHtml(source) })
module.quoteMarkdown =
function(source){

View File

@ -217,7 +217,11 @@ function(obj){
// NOTE: we just created func(..) so no need to sanitize it, the
// only potential vector of atack (AFAIK) here is name and
// that is checked above...
Object.defineProperty(func, 'name', {value: name})
func = eval('('+
func
.toString()
.replace(/function\(/, `function ${name}(`) +')')
//Object.defineProperty(func, 'name', {value: name})
/* XXX NAME...
//func.name = name
func.name != name
@ -916,28 +920,15 @@ function Constructor(name, a, b, c){
obj.__init__ instanceof Function
&& obj.__init__(...arguments)
return obj }
// constructor naming...
// rename the consructor...
// NOTE: we are not using:
// Object.defineProperty(_constructor, 'name', { value: name })
// because this does not affect the name displayed by the Chrome
// DevTools. FF does not seem to care about either version of code...
Object.defineProperty(_constructor, 'name', {value: name})
/* XXX NAME...
//_constructor.name = name
// just in case the browser/node refuses to change the name, we'll make
// them a different offer ;)
// NOTE: it is not possible to abstract this eval(..) into something
// like renameFunction(..) as reconstructing the function will
// lose it's closure that we depend on here...
// NOTE: this eval(..) should not be a risk as its inputs are
// static and never infuenced by external inputs...
_constructor.name != name
&& (_constructor = eval('('+
// because this does not affect the name displayed by the DevTools.
_constructor = eval('('+
_constructor
.toString()
.replace(/Constructor/g, name) +')'))
//*/
.replace(/Constructor/g, name) +')')
// set .toString(..)...
// NOTE: this test is here to enable mixinFlat(..) to overwrite
// .toString(..) below...
@ -1309,6 +1300,8 @@ Constructor('Mixin', {
// NOTE: we do not need to configure this any more, .defineProperty(..)
// merges the descriptor into the original keeping any values not
// explicitly overwritten...
// XXX is this effective???
// ...will this show up in DevTools???
Object.defineProperty(this, 'name', { value: name })
// create/merge .data...
this.data = this.constructor.mixinFlat({},

546
lib/types/Array.js Executable file
View File

@ -0,0 +1,546 @@
/**********************************************************************
*
*
*
* XXX move .zip(..) here from diff.js
* XXX do we need .at(..) / .to(..) methods here and in Map/Set/...???
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
var stoppable = require('ig-stoppable')
var generator = require('./generator')
/*********************************************************************/
// NOTE: this is used in a similar fashion to Python's StopIteration...
var STOP =
module.STOP =
stoppable.STOP
//---------------------------------------------------------------------
// Mixins...
// Wrap .map(..) / .filter(..) / .reduce(..) / .. to support STOP...
//
// NOTE: internally these are implemented as for-of loops (./generator.js)
var stoppableList = function(iter){
return function(func){
return [...this.iter()[iter](...arguments)] } }
var stoppableValue = function(iter, no_return=false){
return function(func){
var res = this.iter()[iter](...arguments)
return no_return ?
undefined
: res } }
// Equivalent to .map(..) / .filter(..) / .reduce(..) that process the
// contents in chunks asynchronously...
//
// .mapChunks(func)
// .mapChunks(chunk_size, func)
// .mapChunks([item_handler, chunk_handler])
// .mapChunks(chunk_size, [item_handler, chunk_handler])
// -> promise(list)
//
// .filterChunks(func)
// .filterChunks(chunk_size, func)
// .filterChunks([item_handler, chunk_handler])
// .filterChunks(chunk_size, [item_handler, chunk_handler])
// -> promise(list)
//
// .reduceChunks(func, res)
// .reduceChunks(chunk_size, func, res)
// .reduceChunks([item_handler, chunk_handler], res)
// .reduceChunks(chunk_size, [item_handler, chunk_handler], res)
// -> promise(res)
//
//
// chunk_handler(chunk, result, offset)
//
//
// chunk_size can be:
// 20 - chunk size
// '20' - chunk size
// '20C' - number of chunks
//
//
// STOP can be thrown in func or chunk_handler at any time to
// abort iteration, this will resolve the promise.
//
//
// The main goal of this is to not block the runtime while processing a
// very long array by interrupting the processing with a timeout...
//
// XXX should these return a partial result on STOP?
// XXX add generators:
// .map(..) / .filter(..) / .reduce(..)
// ...the basis here should be the chunks, i.e. each cycle should
// go through a chunk...
// ...the mixin can be generic, i.e. applicable to Array, and
// other stuff...
// XXX add time-based chunk iteration...
var makeChunkIter = function(iter, wrapper){
wrapper = wrapper
|| function(res, func, array, e){
return func.call(this, e[1], e[0], array) }
return function(size, func, ...rest){
var that = this
var args = [...arguments]
size = (args[0] instanceof Function
|| args[0] instanceof Array) ?
(this.CHUNK_SIZE || 50)
: args.shift()
size = typeof(size) == typeof('str') ?
// number of chunks...
(size.trim().endsWith('c') || size.trim().endsWith('C') ?
Math.round(this.length / (parseInt(size) || 1)) || 1
: parseInt(size))
: size
var postChunk
func = args.shift()
;[func, postChunk] = func instanceof Array ? func : [func]
rest = args
// special case...
// no need to setTimeout(..) if smaller than size...
if(this.length <= size){
try {
// handle iteration...
var res = this[iter](func, ...rest)
// handle chunk...
postChunk
&& postChunk.call(this, this, res, 0)
return Promise.all(res)
// handle STOP...
} catch(err){
if(err === STOP){
return Promise.resolve()
} else if( err instanceof STOP){
return Promise.resolve(err.value) }
throw err } }
var res = []
var _wrapper = wrapper.bind(this, res, func, this)
return new Promise(function(resolve, reject){
var next = function(chunks){
setTimeout(function(){
var chunk, val
try {
// handle iteration...
res.push(
val = (chunk = chunks.shift())
[iter](_wrapper, ...rest))
// handle chunk...
postChunk
&& postChunk.call(that,
chunk.map(function([i, v]){ return v }),
val,
chunk[0][0])
// handle STOP...
} catch(err){
if(err === STOP){
return resolve()
} else if( err instanceof STOP){
return resolve(err.value) }
throw err }
// stop condition...
chunks.length == 0 ?
resolve(res.flat(2))
: next(chunks) }, 0) }
next(that
// split the array into chunks...
.reduce(function(res, e, i){
var c = res.slice(-1)[0]
c.length >= size ?
// initial element in chunk...
res.push([[i, e]])
// rest...
: c.push([i, e])
return res }, [[]])) }) } }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var ArrayMixin =
module.ArrayMixin =
object.Mixin('ArrayMixin', 'soft', {
STOP: object.STOP,
// zip(array, array, ...)
// -> [[item, item, ...], ...]
//
// zip(func, array, array, ...)
// -> [func(i, [item, item, ...]), ...]
//
zip: function(func, ...arrays){
var i = arrays[0] instanceof Array ?
0
: arrays.shift()
if(func instanceof Array){
arrays.splice(0, 0, func)
func = null }
// build the zip item...
// NOTE: this is done this way to preserve array sparseness...
var s = arrays
.reduce(
function(res, a, j){
//a.length > i
i in a
&& (res[j] = a[i])
return res },
new Array(arrays.length))
return arrays
// check that at least one array is longer than i...
.reduce(function(res, a){
return Math.max(res, i, a.length) }, 0) > i ?
// collect zip item...
[func ? func(i, s) : s]
// get next...
.concat(this.zip(func, i+1, ...arrays))
// done...
: [] },
// XXX not sure about the handler API here yet...
iter: function*(lst=[], handler=undefined){
yield* lst.iter(
...(handler ?
[handler]
: [])) },
})
var ArrayProtoMixin =
module.ArrayProtoMixin =
object.Mixin('ArrayProtoMixin', 'soft', {
// A faster version of .indexOf(..)
//
// NOTE: this is not faster when looking for an item not in this,
// for some reason the native .includes(..) and .indexOf(..)
// search for non existant elements about an order of magnirude
// faster than if it existed...
// ...the funny thing is that at least on Crome .lastIndexOf(..)
// is about as fast as this for an item in about the same relative
// location...
// NOTE: this will get depricated as soon as JS redoes its .indexOf(..)
index: function(value){
for(var i = 0; i < this.length && this[i] !== value; i++){}
return i == this.length ? -1 : i },
// first/last element access short-hands...
//
// .first()
// .last()
// -> elem
//
// .first(value)
// .last(value)
// -> array
//
// NOTE: setting a value will overwrite an existing first/last value.
// NOTE: for an empty array both .first(..)/.last(..) will return undefined
// when getting a value and set the 0'th value when setting...
// NOTE: decided to keep these as methods and not props because methods
// have one advantage: they can be chained
// ...while you can't chain assignment unless you wrap it in .run(..)
first: function(value){
return arguments.length > 0 ?
((this[0] = value), this)
: this[0]},
last: function(value){
return arguments.length > 0 ?
((this[Math.max(this.length - 1, 0)] = value), this)
: this[this.length - 1]},
// Roll left/right (in-place)...
//
// NOTE: to .rol(..) left just pass a negative n value...
// NOTE: we can't use ...[..] for sparse arrays as the will expand undefined
// inplace of empty positions, this is thereason the .splice(..)
// implementation was replaced by a less clear (but faster)
// .copyWithin(..) version...
rol: function(n=1){
var l = this.length
n = (n >= 0 ?
n
: l - n)
% l
if(n != 0){
this.length += n
this.copyWithin(l, 0, n)
this.splice(0, n) }
return this },
// Compact a sparse array...
//
// NOTE: this will not compact in-place.
compact: function(){
return this
.filter(function(){ return true }) },
// Remove sprse slots form start/end/both ends of array...
//
trim: function(){
var l = this.length
var i = 0
while(!(i in this) && i < l){ i++ }
var j = 0
while(!(l-j-1 in this) && j < l){ j++ }
return this.slice(i, j == 0 ? l : -j) },
trimStart: function(){
var l = this.length
var i = 0
while(!(i in this) && i < l){ i++ }
return this.slice(i) },
trimEnd: function(){
var l = this.length
var j = 0
while(!(l-j-1 in this) && j < l){ j++ }
return this.slice(0, j == 0 ? l : -j) },
// like .length but for sparse arrays will return the element count...
get len(){
// NOTE: if we don't do .slice() here this can count array
// instance attributes...
// NOTE: .slice() has an added menifit here of removing any
// attributes from the count...
return Object.keys(this.slice()).length },
// Return a new array with duplicate elements removed...
//
// NOTE: order is preserved...
unique: function(normalize){
return normalize ?
[...new Map(this
.map(function(e){
return [normalize(e), e] }))
.values()]
// NOTE: we are calling .compact() here to avoid creating
// undefined items from empty slots in sparse arrays...
: [...new Set(this.compact())] },
tailUnique: function(normalize){
return this
.slice()
.reverse()
.unique(normalize)
.reverse() },
// Compare two arrays...
//
// NOTE: this is diffectent from Object.match(..) in that this compares
// self to other (internal) while match compares two entities
// externally.
// XXX not sure if we need the destinction in name, will have to
// come back to this when refactoring diff.js -- all three
// have to be similar...
cmp: function(other){
if(this === other){
return true }
if(this.length != other.length){
return false }
for(var i=0; i<this.length; i++){
if(this[i] != other[i]){
return false } }
return true },
// Compare two Arrays as sets...
//
// NOTE: this will ignore order and repeating elments...
setCmp: function(other){
return this === other
|| (new Set([...this, ...other])).length
== (new Set(this)).length },
// Sort as the other array...
//
// Sort as array placing the sorted items at head...
// .sortAs(array)
// .sortAs(array, 'head')
// -> sorted
//
// Sort as array placing the sorted items at tail...
// .sortAs(array, 'tail')
// -> sorted
//
// This will sort the intersecting items in the head keeping the rest
// of the items in the same relative order...
//
// NOTE: if an item is in the array multiple times only the first
// index is used...
//
// XXX should this extend/patch .sort(..)???
// ...currently do not see a clean way to do this without
// extending and replacing Array or directly re-wrapping
// .sort(..)...
sortAs: function(other, place='head'){
place = place == 'tail' ? -1 : 1
// NOTE: the memory overhead here is better than the time overhead
// when using .indexOf(..)...
other = other.toMap()
var orig = this.toMap()
return this.sort(function(a, b){
var i = other.get(a)
var j = other.get(b)
return i == null && j == null ?
orig.get(a) - orig.get(b)
: i == null ?
place
: j == null ?
-place
: i - j }) },
/*/ XXX EXPERIMENTAL...
// ...if this is successful then merge this with .sortAs(..)
sort: function(cmp){
return (arguments.length == 0 || typeof(cmp) == 'function') ?
object.parentCall(ArrayProtoMixin.data.sort, this, ...arguments)
: this.sortAs(...arguments) },
//*/
// Same as .sortAs(..) but will not change indexes of items not in other...
//
// Example:
// ['a', 3, 'b', 1, 2, 'c']
// .inplaceSortAs([1, 2, 3, 3]) // -> ['a', 1, 'b', 2, 3, 'c']
//
inplaceSortAs: function(other){
// sort only the intersection...
var sorted = this
.filter(function(e){
return other.includes(e) })
.sortAs(other)
// "zip" the sorted items back into this...
this.forEach(function(e, i, l){
other.includes(e)
&& (l[i] = sorted.shift()) })
return this },
// Convert an array to object...
//
// Format:
// {
// <item>: <index>,
// ...
// }
//
// NOTE: items should be strings, other types will get converted to
// strings and thus may mess things up.
// NOTE: this will forget repeating items...
// NOTE: normalize will slow things down...
toKeys: function(normalize){
return normalize ?
this.reduce(function(r, e, i){
r[normalize(e, i)] = i
return r }, {})
: this.reduce(function(r, e, i){
r[e] = i
return r }, {}) },
// Convert an array to a map...
//
// This is similar to Array.prototype.toKeys(..) but does not
// restrict value type to string.
//
// Format:
// Map([
// [<item>, <index>],
// ...
// ])
//
// NOTE: this will forget repeating items...
// NOTE: normalize will slow things down...
toMap: function(normalize){
return normalize ?
this
.reduce(function(m, e, i){
m.set(normalize(e, i), i)
return m }, new Map())
: this
.reduce(function(m, e, i){
m.set(e, i)
return m }, new Map()) },
// XXX would be nice for this to use the instance .zip(..) in recursion...
// ...this might be done by reversign the current implementation, i.e.
// for instance .zip(..) to be the main implementation and for
// Array.zip(..) to be a proxy to that...
zip: function(func, ...arrays){
return func instanceof Array ?
this.constructor.zip(this, func, ...arrays)
: this.constructor.zip(func, this, ...arrays) },
// get iterator over array...
//
// Array.iter()
// Array.iter([ .. ])
// -> iterator
//
// array.iter()
// -> iterator
//
// XXX should this take an argument and be like map??
// XXX this should handle throwing STOP!!!
// ...might also ne a good idea to isolate the STOP mechanics
// into a spearate module/package...
iter: stoppable(function*(handler=undefined){
if(handler){
var i = 0
for(var e of this){
var res = handler.call(this, e, i++)
// treat non-iterables as single elements...
if(typeof(res) == 'object'
&& Symbol.iterator in res){
yield* res
} else {
yield res } }
} else {
yield* this }}),
// Stoppable iteration...
//
// NOTE: internally these are generators...
smap: stoppableList('map'),
sfilter: stoppableList('filter'),
sreduce: stoppableValue('reduce'),
sreduceRight: stoppableValue('reduceRight'),
sforEach: stoppableValue('map', true),
// Chunk iteration...
//
CHUNK_SIZE: 50,
mapChunks: makeChunkIter('map'),
filterChunks: makeChunkIter('map',
function(res, func, array, e){
return !!func.call(this, e[1], e[0], array) ? [e[1]] : [] }),
reduceChunks: makeChunkIter('reduce',
function(total, func, array, res, e){
return func.call(this,
total.length > 0 ?
total.pop()
: res,
e[1], e[0], array) }),
})
ArrayMixin(Array)
ArrayProtoMixin(Array.prototype)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

147
lib/types/Date.js Executable file
View File

@ -0,0 +1,147 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
var DateMixin =
module.DateMixin =
object.Mixin('DateMixin', 'soft', {
timeStamp: function(...args){
return (new this()).getTimeStamp(...args) },
fromTimeStamp: function(ts){
return (new this()).setTimeStamp(ts) },
// convert string time period to milliseconds...
str2ms: function(str, dfl){
dfl = dfl || 'ms'
// number -- dfl unit...
if(typeof(str) == typeof(123)){
var val = str
str = dfl
// 00:00:00:00:000 format...
} else if(str.includes(':')){
var units = str.split(/\s*:\s*/g).reverse()
// parse units...
var ms = units[0].length == 3 ?
parseFloat(units.shift() || 0)
: 0
var [s=0, m=0, h=0, d=0] = units
// merge...
return ((((parseFloat(d || 0)*24
+ parseFloat(h || 0))*60
+ parseFloat(m || 0))*60
+ parseFloat(s || 0))*1000 + ms)
// 00sec format...
} else {
var val = parseFloat(str)
str = str.trim()
// check if a unit is given...
str = str == val ?
dfl
: str }
// NOTE: this is a small hack to avoid overcomplicating the
// pattern to still match the passed dfl unit...
str = ' '+str
var c =
(/[^a-z](m(illi)?(-)?s(ec(ond(s)?)?)?)$/i.test(str)
|| /^([0-9]*\.)?[0-9]+$/.test(str) ) ?
1
: /[^a-z]s(ec(ond(s)?)?)?$/i.test(str) ?
1000
: /[^a-z]m(in(ute(s)?)?)?$/i.test(str) ?
1000*60
: /[^a-z]h(our(s)?)?$/i.test(str) ?
1000*60*60
: /[^a-z]d(ay(s)?)?$/i.test(str) ?
1000*60*60*24
: null
return c ?
val * c
: NaN },
isPeriod: function(str){
return !isNaN(this.str2ms(str)) },
isDateStr: function(str){
return !isNaN(new Date(str).valueOf()) },
})
// XXX should this be flat???
var DateProtoMixin =
module.DateProtoMixin =
object.Mixin('DateProtoMixin', 'soft', {
toShortDate: function(show_ms){
return ''
+ this.getFullYear()
+'-'+ ('0'+(this.getMonth()+1)).slice(-2)
+'-'+ ('0'+this.getDate()).slice(-2)
+' '+ ('0'+this.getHours()).slice(-2)
+':'+ ('0'+this.getMinutes()).slice(-2)
+':'+ ('0'+this.getSeconds()).slice(-2)
+ (show_ms ?
':'+(('000'+this.getMilliseconds()).slice(-3))
: '') },
getTimeStamp: function(show_ms){
return ''
+ this.getFullYear()
+ ('0'+(this.getMonth()+1)).slice(-2)
+ ('0'+this.getDate()).slice(-2)
+ ('0'+this.getHours()).slice(-2)
+ ('0'+this.getMinutes()).slice(-2)
+ ('0'+this.getSeconds()).slice(-2)
+ (show_ms ?
('000'+this.getMilliseconds()).slice(-3)
: '') },
setTimeStamp: function(ts){
ts = ts.replace(/[^0-9]*/g, '')
this.setFullYear(ts.slice(0, 4))
this.setMonth(ts.slice(4, 6)*1-1)
this.setDate(ts.slice(6, 8))
this.setHours(ts.slice(8, 10))
this.setMinutes(ts.slice(10, 12))
this.setSeconds(ts.slice(12, 14))
this.setMilliseconds(ts.slice(14, 17) || 0)
return this },
})
//---------------------------------------------------------------------
// NOTE: repatching a date should not lead to any side effects as this
// does not add any state...
// NOTE: this is done differently as there are contexts where there may
// be multiple Date objects in different contexts (nw/electron/..)
var patchDate =
module.patchDate =
function(date){
date = date || Date
DateMixin(date)
DateProtoMixin(date.prototype)
return date }
//---------------------------------------------------------------------
// patch the root date...
patchDate()
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

18
lib/types/Function.js Executable file
View File

@ -0,0 +1,18 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var AsyncFunction =
module.AsyncFunction =
(async function(){}).constructor
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

70
lib/types/Map.js Executable file
View File

@ -0,0 +1,70 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
var MapProtoMixin =
module.MapProtoMixin =
object.Mixin('MapProtoMixin', 'soft', {
iter: function*(){
for(var e of this){
yield e } },
// NOTE: we do not touch .__keys here as no renaming is ever done...
//
// XXX this essentially rewrites the whole map, is there a faster/better
// way to do this???
// ...one way would be to decouple order from the container, i.e.
// store the order in a separate attr/prop but this would require
// a whole new set of ordered "type" that would overload every single
// iteration method, not sure if this is a good idea untill we
// reach a state whe JS "shuffles" (index-orders) its containers
// (a-la Python)
sort: function(keys){
keys = (typeof(keys) == 'function'
|| keys === undefined) ?
[...this.keys()].sort(keys)
: keys
var del = this.delete.bind(this)
var set = this.set.bind(this)
new Set([...keys, ...this.keys()])
.forEach(function(k){
if(this.has(k)){
var v = this.get(k)
del(k)
set(k, v) } }.bind(this))
return this },
replaceKey: function(old, key, ordered=true){
if(!this.has(old)){
return this }
if(ordered){
var order = [...this.keys()]
order[order.lastIndexOf(old)] = key }
// replace...
var value = this.get(old)
this.delete(old)
this.set(key, value)
ordered
&& this.sort(order)
return this },
})
MapProtoMixin(Map.prototype)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

101
lib/types/Object.js Executable file
View File

@ -0,0 +1,101 @@
/**********************************************************************
*
*
*
* XXX shoule we add these from object to Object?
* - .parent(..)
* - .parentProperty(..)
* - .parentCall(..)
* - .parentOf(..)
* - .childOf(..)
* - .related(..)
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
require('object-run')
var object = require('ig-object')
/*********************************************************************/
var ObjectMixin =
module.ObjectMixin =
object.Mixin('ObjectMixin', 'soft', {
// stuff from object.js...
deepKeys: object.deepKeys,
match: object.match,
matchPartial: object.matchPartial,
/* XXX not yet sure about these...
// XXX EXPERIMENTAL...
parent : object.parent,
parentProperty: object.parentProperty,
parentCall: object.parentCall,
parentOf: object.parentOf,
childOf: object.childOf,
related: object.related,
//*/
// Make a copy of an object...
//
// This will:
// - create a new object linked to the same prototype chain as obj
// - copy obj own state
//
// NOTE: this will copy prop values and not props...
copy: function(obj, constructor){
return Object.assign(
constructor == null ?
Object.create(obj.__proto__)
: constructor(),
obj) },
// Make a full key set copy of an object...
//
// NOTE: this will copy prop values and not props...
// NOTE: this will not deep-copy the values...
flatCopy: function(obj, constructor){
return Object.deepKeys(obj)
.reduce(
function(res, key){
res[key] = obj[key]
return res },
constructor == null ?
//Object.create(obj.__proto__)
{}
: constructor()) },
// XXX for some reason neumric keys do not respect order...
// to reproduce:
// Object.keys({a:0, x:1, 10:2, 0:3, z:4, ' 1 ':5})
// // -> ["0", "10", "a", "x", "z", " 1 "]
// ...this is the same across Chrome and Firefox...
sort: function(obj, keys){
keys = (typeof(keys) == 'function'
|| keys === undefined) ?
[...Object.keys(obj)].sort(keys)
: keys
new Set([...keys, ...Object.keys(obj)])
.forEach(function(k){
if(k in obj){
var v = Object.getOwnPropertyDescriptor(obj, k)
delete obj[k]
Object.defineProperty(obj, k, v) } })
return obj },
})
ObjectMixin(Object)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

798
lib/types/Promise.js Executable file
View File

@ -0,0 +1,798 @@
/**********************************************************************
*
* This defines the following extensions to Promise:
*
* Promise.iter(seq)
* <promise>.iter()
* Iterable promise object.
* Similar to Promise.all(..) but adds basic iterator API.
*
* Promise.interactive(handler)
* Interactive promise object.
* This adds a basic message passing API to the promise.
*
* Promise.cooperative()
* Cooperative promise object.
* Exposes the API to resolve/reject the promise object
* externally.
*
* <promise>.as(obj)
* Promise proxy.
* Proxies the methods available from obj to promise value.
*
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
//var generator = require('./generator')
//---------------------------------------------------------------------
// Iterable promise...
//
// Like Promise.all(..) but adds ability to iterate through results
// via generators .map(..)/.reduce(..) and friends...
//
// NOTE: the following can not be implemented here:
// .splice(..) - can't both modify and return
// a result...
// .pop() / .shift() - can't modify the promise, use
// .first() / .last() instead.
// [Symbol.iterator]() - needs to be sync and we can't
// know the number of elements to
// return promises before the whole
// iterable promise is resolved.
// NOTE: we are not using async/await here as we need to control the
// type of promise returned in cases where we know we are returning
// an array...
// NOTE: there is no point in implementing a 1:1 version of this that
// would not support element expansion/contraction as it would only
// simplify a couple of methods that are 1:1 (like .map(..) and
// .some(..)) while methods like .filter(..) will throw everything
// back to the complex IterablePromise...
//
// XXX how do we handle errors/rejections???
// ...mostly the current state is OK, but need more testing...
// XXX add support for async generators...
//
var iterPromiseProxy =
module.iterPromiseProxy =
function(name){
return function(...args){
return this.constructor(
this.then(function(lst){
return lst[name](...args) })) } }
var promiseProxy =
module.promiseProxy =
function(name){
return async function(...args){
return (await this)[name](...args) } }
var IterablePromise =
module.IterablePromise =
object.Constructor('IterablePromise', Promise, {
get STOP(){
return Array.STOP },
}, {
// packed array...
//
// Holds promise state.
//
// Format:
// [
// <non-array-value>,
// [ <value> ],
// <promise>,
// ...
// ]
//
// This format has several useful features:
// - concatenating packed lists results in a packed list
// - adding an iterable promise (as-is) into a packed list results
// in a packed list
//
// NOTE: in general iterable promises are implicitly immutable, so
// it is not recomended to ever edit this in-place...
// NOTE: we are not isolating or "protecting" any internals to
// enable users to responsibly extend the code.
__packed: null,
// low-level .__packed handlers/helpers...
//
// NOTE: these can be useful for debugging and extending...
//
// pack and oprionally transform/handle an array (sync)...
//
// NOTE: if 'types/Array' is imported this will support throwing STOP
// from the handler.
// Due to the async nature of promises though the way stops are
// handled may be unpredictable -- the handlers can be run out
// of order, as the nested promises resolve and thus throwing
// stop will stop the handlers not yet run and not the next
// handlers in sequence.
// XXX EXPEREMENTAL: STOP...
// XXX add support for async generators...
// ...an async generator is not "parallel", i.e. intil one
// returned promise is resolved the generator blocks (will not
// advance)...
// ...can we feed out a results one by one???
__pack: function(list, handler=undefined){
var that = this
// handle iterable promise list...
if(list instanceof IterablePromise){
return this.__handle(list.__packed, handler) }
// handle promise list...
if(list instanceof Promise){
return list.then(function(list){
return that.__pack(list, handler) }) }
// do the work...
// NOTE: packing and handling are mixed here because it's faster
// to do them both on a single list traverse...
var handle = !!handler
handler = handler
?? function(elem){
return [elem] }
//* XXX EXPEREMENTAL: STOP...
var stoppable = false
var stop = false
var map = 'map'
var pack = function(){
return [list].flat()
[map](function(elem){
return elem && elem.then ?
(stoppable ?
// stoppable -- need to handle stop async...
elem
.then(function(res){
return !stop ?
handler(res)
: [] })
// NOTE: we are using .catch(..) here
// instead of directly passing the
// error handler to be able to catch
// the STOP from the handler...
.catch(handleSTOP)
// non-stoppable...
: elem.then(handler))
: elem instanceof Array ?
handler(elem)
// NOTE: we keep things that do not need protecting
// from .flat() as-is...
: !handle ?
elem
: handler(elem) }) }
// pack (stoppable)...
if(!!this.constructor.STOP){
stoppable = true
map = 'smap'
var handleSTOP = function(err){
stop = err
if(err === that.constructor.STOP
|| err instanceof that.constructor.STOP){
return 'value' in err ?
err.value
: [] }
throw err }
try{
return pack()
}catch(err){
return handleSTOP(err) } }
// pack (non-stoppable)...
return pack() },
/*/
return [list].flat()
.map(function(elem){
return elem && elem.then ?
//that.__pack(elem, handler)
elem.then(handler)
: elem instanceof Array ?
handler(elem)
// NOTE: we keep things that do not need protecting
// from .flat() as-is...
: !handle ?
elem
: handler(elem) }) },
//*/
// transform/handle packed array (sync)...
__handle: function(list, handler=undefined){
var that = this
if(typeof(list) == 'function'){
handler = list
list = this.__packed }
if(!handler){
return list }
// handle promise list...
if(list instanceof Promise){
return list.then(function(list){
return that.__handle(list, handler) }) }
// do the work...
// NOTE: since each section of the packed .__array is the same
// structure as the input we'll use .__pack(..) to handle
// them, this also keeps all the handling code in one place.
//* XXX EXPEREMENTAL: STOP...
var map = !!this.constructor.STOP ?
'smap'
: 'map'
return list[map](function(elem){
/*/
return list.map(function(elem){
//*/
return elem instanceof Array ?
that.__pack(elem, handler)
: elem instanceof Promise ?
that.__pack(elem, handler)
//.then(function(elem){
.then(function([elem]){
return elem })
: [handler(elem)] })
.flat() },
// unpack array (async)...
__unpack: async function(list){
list = list
?? this.__packed
// handle promise list...
return list instanceof Promise ?
this.__unpack(await list)
// do the work...
: (await Promise.all(list))
.flat() },
// iterator methods...
//
// These will return a new IterablePromise instance...
//
// NOTE: these are different to Array's equivalents in that the handler
// is called not in the order of the elements but rather in order
// of promise resolution...
// NOTE: index of items is unknowable because items can expand and
// contract depending on handlers (e.g. .filter(..) can remove
// items)...
map: function(func){
return this.constructor(this,
function(e){
var res = func(e)
return res instanceof Promise ?
res.then(function(e){
return [e] })
: [res] }) },
filter: function(func){
return this.constructor(this,
function(e){
var res = func(e)
var _filter = function(elem){
return res ?
[elem]
: [] }
return res instanceof Promise ?
res.then(_filter)
: _filter(e) }) },
// NOTE: this does not return an iterable promise as we can't know
// what the user reduces to...
// NOTE: the items can be handled out of order because the nested
// promises can resolve in any order...
// NOTE: since order of execution can not be guaranteed there is no
// point in implementing .reduceRight(..) in the same way
// (see below)...
reduce: function(func, res){
return this.constructor(this,
function(e){
res = func(res, e)
return [] })
.then(function(){
return res }) },
// XXX .chain(..) -- see generator.chain(..)
flat: function(depth=1){
return this.constructor(this,
function(e){
return (depth > 1
&& e != null
&& e.flat) ?
e.flat(depth-1)
: depth != 0 ?
e
: [e] }) },
reverse: function(){
var lst = this.__packed
return this.constructor(
lst instanceof Promise ?
lst.then(function(elems){
return elems instanceof Array ?
elems.slice()
.reverse()
: elems })
: lst
.map(function(elems){
return elems instanceof Array ?
elems.slice()
.reverse()
: elems instanceof Promise ?
elems.then(function(elems){
return elems.reverse() })
: elems })
.reverse(),
'raw') },
// NOTE: the following methods can create an unresolved promise from
// a resolved promise...
concat: function(other){
var that = this
var cur = this.__pack(this)
var other = this.__pack(other)
return this.constructor(
// NOTE: we need to keep things as exposed as possible, this
// is why we're not blanketing all the cases with
// Promise.all(..)...
(cur instanceof Promise
&& other instanceof Promise) ?
[cur, other]
: cur instanceof Promise ?
[cur, ...other]
: other instanceof Promise ?
[...cur, other]
: [...cur, ...other],
'raw') },
push: function(elem){
return this.concat([elem]) },
unshift: function(elem){
return this.constructor([elem])
.concat(this) },
// proxy methods...
//
// These require the whole promise to resolve to trigger.
//
// An exception to this would be .at(0)/.first() and .at(-1)/.last()
// that can get the target element if it's accessible.
//
// NOTE: methods that are guaranteed to return an array will return
// an iterable promise (created with iterPromiseProxy(..))...
//
at: async function(i){
var list = this.__packed
return ((i != 0 && i != -1)
|| list instanceof Promise
// XXX not sure if this is correct...
|| list.at(i) instanceof Promise) ?
(await this).at(i)
// NOTE: we can only reason about first/last explicit elements,
// anything else is non-deterministic...
: list.at(i) instanceof Promise ?
[await list.at(i)].flat().at(i)
: list.at(i) instanceof Array ?
list.at(i).at(i)
: list.at(i) },
first: function(){
return this.at(0) },
last: function(){
return this.at(-1) },
// NOTE: unlike .reduce(..) this needs the parent fully resolved
// to be able to iterate from the end.
// XXX is it faster to do .reverse().reduce(..) ???
reduceRight: promiseProxy('reduceRight'),
// NOTE: there is no way we can do a sync generator returning
// promises for values because any promise in .__packed makes
// the value count/index non-deterministic...
sort: iterPromiseProxy('sort'),
slice: iterPromiseProxy('slice'),
entries: iterPromiseProxy('entries'),
keys: iterPromiseProxy('keys'),
values: iterPromiseProxy('values'),
indexOf: promiseProxy('indexOf'),
lastIndexOf: promiseProxy('lastIndexOf'),
includes: promiseProxy('includes'),
//
// .find(<func>)
// .find(<func>, 'value')
// -> <promise>(<value>)
//
// .find(<func>, 'result')
// -> <promise>(<result>)
//
// .find(<func>, 'bool')
// -> <promise>(<bool>)
//
// NOTE: this is slightly different to Array's .find(..) in that it
// accepts the result value enabling returning both the value
// itself ('value', default), the test function's result
// ('result') or true/false ('bool') -- this is added to be
// able to distinguish between the undefined as a stored value
// and undefined as a "nothing found" result.
// NOTE: I do not get how essentially identical methods .some(..)
// and .find(..) got added to JS's Array...
// the only benefit is that .some(..) handles undefined values
// stored in the array better...
// NOTE: this will return the result as soon as it's available but
// it will not stop the created but unresolved at the time
// promises from executing, this is both good and bad:
// + it will not break other clients waiting for promises
// to resolve...
// - if no clients are available this can lead to wasted
// CPU time...
find: async function(func, result='value'){
var that = this
// NOTE: not using pure await here as this is simpler to actually
// control the moment the resulting promise resolves without
// the need for juggling state...
return new Promise(function(resolve, reject){
var resolved = false
that.map(function(elem){
var res = func(elem)
if(res){
resolved = true
resolve(
result == 'bool' ?
true
: result == 'result' ?
res
: elem)
// XXX EXPEREMENTAL: STOP...
// NOTE: we do not need to throw STOP here
// but it can prevent some overhead...
if(that.constructor.STOP){
throw that.constructor.STOP } } })
.then(function(){
resolved
|| resolve(
result == 'bool' ?
false
: undefined) }) }) },
findIndex: promiseProxy('findIndex'),
// NOTE: this is just a special-case of .find(..)
some: async function(func){
return this.find(func, 'bool') },
every: promiseProxy('every'),
join: async function(){
return [...(await this)]
.join(...arguments) },
// this is defined globally as Promise.prototype.iter(..)
//
// for details see: PromiseMixin(..) below...
//iter: function(handler=undefined){ ... },
// promise api...
//
// Overload .then(..), .catch(..) and .finally(..) to return a plain
// Promise instnace...
//
// NOTE: .catch(..) and .finally(..) are implemented through .then(..)
// so we do not need to overload those...
// NOTE: this is slightly different from .then(..) in that it can be
// called without arguments and return a promise wrapper. This can
// be useful to hide special promise functionality...
then: function (onfulfilled, onrejected){
var p = new Promise(
function(resolve, reject){
Promise.prototype.then.call(this,
// NOTE: resolve(..) / reject(..) return undefined so
// we can't pass them directly here...
function(res){
resolve(res)
return res },
function(res){
reject(res)
return res }) }.bind(this))
return arguments.length > 0 ?
p.then(...arguments)
: p },
// constructor...
//
// Promise.iter([ .. ])
// -> iterable-promise
//
// Promise.iter([ .. ], handler)
// -> iterable-promise
//
//
// handler(e)
// -> [value, ..]
// -> []
// -> <promise>
//
//
// NOTE: element index is unknowable until the full list is expanded
// as handler(..)'s return value can expand to any number of
// items...
// XXX we can make the index a promise, then if the client needs
// the value they can wait for it...
// ...this may be quite an overhead...
//
//
// Special cases useful for extending this constructor...
//
// Set raw .__packed without any pre-processing...
// Promise.iter([ .. ], 'raw')
// -> iterable-promise
//
// Create a rejected iterator...
// Promise.iter(false)
// -> iterable-promise
//
//
// NOTE: if 'types/Array' is imported this will support throwing STOP,
// for more info see notes for .__pack(..)
// XXX EXPEREMENTAL: STOP...
__new__: function(_, list, handler){
// instance...
var promise
var obj = Reflect.construct(
IterablePromise.__proto__,
[function(resolve, reject){
// NOTE: this is here for Promise compatibility...
if(typeof(list) == 'function'){
return list.call(this, ...arguments) }
// initial reject...
if(list === false){
return reject() }
promise = {resolve, reject} }],
IterablePromise)
// populate new instance...
if(promise){
// handle/pack input data...
if(handler != 'raw'){
list = list instanceof IterablePromise ?
this.__handle(list.__packed, handler)
: this.__pack(list, handler) }
Object.defineProperty(obj, '__packed', {
value: list,
enumerable: false,
})
// handle promise state...
this.__unpack(list)
.then(function(list){
promise.resolve(list) })
.catch(promise.reject) }
return obj },
})
//---------------------------------------------------------------------
// Interactive promise...
//
// Adds ability to send messages to the running promise.
//
var InteractivePromise =
module.InteractivePromise =
object.Constructor('InteractivePromise', Promise, {
// XXX do we need a way to remove handlers???
__message_handlers: null,
send: function(...args){
var that = this
;(this.__message_handlers || [])
.forEach(function(h){ h.call(that, ...args) })
return this },
then: IterablePromise.prototype.then,
//
// Promise.interactive(handler)
// -> interacive-promise
//
// handler(resolve, reject, onmessage)
//
// onmessage(func)
//
//
__new__: function(_, handler){
var handlers = []
var onmessage = function(func){
// remove all handlers...
if(func === false){
var h = (obj == null ?
handlers
: (obj.__message_handlers || []))
h.splice(0, handlers.length)
// remove a specific handler...
} else if(arguments[1] === false){
var h = (obj == null ?
handlers
: (obj.__message_handlers || []))
h.splice(h.indexOf(func), 1)
// register a handler...
} else {
var h = obj == null ?
// NOTE: we need to get the handlers from
// .__message_handlers unless we are not
// fully defined yet, then use the bootstrap
// container (handlers)...
// ...since we can call onmessage(..) while
// the promise is still defined there is no
// way to .send(..) until it returns a promise
// object, this races here are highly unlikely...
handlers
: (obj.__message_handlers =
obj.__message_handlers ?? [])
handlers.push(func) } }
var obj = Reflect.construct(
InteractivePromise.__proto__,
!handler ?
[]
: [function(resolve, reject){
return handler(resolve, reject, onmessage) }],
InteractivePromise)
Object.defineProperty(obj, '__message_handlers', {
value: handlers,
enumerable: false,
// XXX should this be .configurable???
configurable: true,
})
return obj },
})
//---------------------------------------------------------------------
// Cooperative promise...
//
// A promise that can be resolved/rejected externally.
//
// NOTE: normally this has no internal resolver logic...
//
var CooperativePromise =
module.CooperativePromise =
object.Constructor('CooperativePromise', Promise, {
__handlers: null,
get isSet(){
return this.__handlers === false },
set: function(value, resolve=true){
// can't set twice...
if(this.isSet){
throw new Error('.set(..): can not set twice') }
// bind to promise...
if(value && value.then && value.catch){
value.then(handlers.resolve)
value.catch(handlers.reject)
// resolve with value...
} else {
resolve ?
this.__handlers.resolve(value)
: this.__handlers.reject(value) }
// cleanup and prevent setting twice...
this.__handlers = false
return this },
then: IterablePromise.prototype.then,
__new__: function(){
var handlers
var resolver = arguments[1]
var obj = Reflect.construct(
CooperativePromise.__proto__,
[function(resolve, reject){
handlers = {resolve, reject}
// NOTE: this is here to support builtin .then(..)
resolver
&& resolver(resolve, reject) }],
CooperativePromise)
Object.defineProperty(obj, '__handlers', {
value: handlers,
enumerable: false,
writable: true,
})
return obj },
})
//---------------------------------------------------------------------
// XXX EXPEREMENTAL...
var ProxyPromise =
module.ProxyPromise =
object.Constructor('ProxyPromise', Promise, {
then: IterablePromise.prototype.then,
__new__: function(context, other, nooverride=false){
var proto = 'prototype' in other ?
other.prototype
: other
var obj = Reflect.construct(
ProxyPromise.__proto__,
[function(resolve, reject){
context.then(resolve)
context.catch(reject) }],
ProxyPromise)
// populate...
// NOTE: we are not using object.deepKeys(..) here as we need
// the key origin not to trigger property getters...
var seen = new Set()
nooverride = nooverride instanceof Array ?
new Set(nooverride)
: nooverride
while(proto != null){
Object.entries(Object.getOwnPropertyDescriptors(proto))
.forEach(function([key, value]){
// skip overloaded keys...
if(seen.has(key)){
return }
// skip non-functions...
if(typeof(value.value) != 'function'){
return }
// skip non-enumerable except for Object.prototype.run(..)...
if(!(key == 'run'
&& Object.prototype.run === value.value)
&& !value.enumerable){
return }
// do not override existing methods...
if(nooverride === true ?
key in obj
: nooverride instanceof Set ?
nooverride.has(key)
: nooverride){
return }
// proxy...
obj[key] = promiseProxy(key) })
proto = proto.__proto__ }
return obj },
})
//---------------------------------------------------------------------
var PromiseMixin =
module.PromiseMixin =
object.Mixin('PromiseMixin', 'soft', {
iter: IterablePromise,
interactive: InteractivePromise,
cooperative: CooperativePromise,
})
PromiseMixin(Promise)
var PromiseProtoMixin =
module.PromiseProtoMixin =
object.Mixin('PromiseProtoMixin', 'soft', {
as: ProxyPromise,
iter: function(handler=undefined){
return IterablePromise(this, handler) },
})
PromiseProtoMixin(Promise.prototype)
/**********************************************************************
* vim:set ts=4 sw=4 nowrap : */ return module })

78
lib/types/RegExp.js Executable file
View File

@ -0,0 +1,78 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
var RegExpMixin =
module.RegExpMixin =
object.Mixin('RegExpMixin', 'soft', {
// Quote a string and convert to RegExp to match self literally.
quoteRegExp: function(str){
return str
.replace(/([\.\\\/\(\)\[\]\$\*\+\-\{\}\@\^\&\?\<\>])/g, '\\$1') }
})
var GROUP_PATERN =
//module.GROUP_PATERN = /(^\(|[^\\]\()/g
module.GROUP_PATERN = new RegExp([
'^\\(',
// non-escaped braces...
'[^\\\\]\\(',
// XXX ignore braces in ranges...
// XXX '\\[.*(.*\\]',
].join('|'))
// Pattern group introspection...
var RegExpProtoMixin =
module.RegExpProtoMixin =
object.Mixin('RegExpProtoMixin', 'soft', {
// Format:
// [
// {
// index: <index>,
// name: <name>,
// pattern: <string>,
// offset: <offset>,
// },
// ...
// ]
// XXX cache this...
get groups(){
this.toString()
.matchAll(GROUP_PATERN)
},
get namedGroups(){
return this.groups
.reduce(function(res, e){
e.name
&& (res[name] = e)
return res }, {}) },
get groupCount(){
return this.groups.length },
})
RegExpMixin(RegExp)
// XXX EXPEREMENTAL...
//RegExpProtoMixin(RegExp.prototype)
var quoteRegExp =
RegExp.quoteRegExp =
RegExp.quoteRegExp
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

122
lib/types/Set.js Executable file
View File

@ -0,0 +1,122 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
var SetProtoMixin =
module.SetProtoMixin =
object.Mixin('SetMixin', 'soft', {
iter: function*(){
for(var e of this){
yield e } },
// Set set operation shorthands...
unite: function(other=[]){
return new Set([...this, ...other]) },
intersect: function(other){
var test = other.has ?
'has'
: 'includes'
return new Set([...this]
.filter(function(e){
return other[test](e) })) },
subtract: function(other=[]){
other = new Set(other)
return new Set([...this]
.filter(function(e){
return !other.has(e) })) },
sort: function(values=[]){
values = (typeof(values) == 'function'
|| values === undefined) ?
[...this].sort(values)
: values
var del = this.delete.bind(this)
var add = this.add.bind(this)
new Set([...values, ...this])
.forEach(function(e){
if(this.has(e)){
del(e)
add(e) } }.bind(this))
return this },
replace: function(old, value, ordered=true){
// nothing to do...
if(!this.has(old) || old === value){
return this }
if(ordered){
var order = [...this]
// XXX is this fast enough???
order[order.lastIndexOf(old)] = value }
// replace...
this.delete(old)
this.add(value)
ordered
&& this.sort(order)
return this },
// NOTE: if index is <0 then the value is prepended to the set, if
// it's >=this.size then the value will be appended.
// if ordered is set to false in both cases the value is appended.
// XXX should this be implemented via .splice(..) ???
replaceAt: function(index, value, ordered=true){
// append...
if(index >= this.size){
this.add(value)
return this }
// prepend...
if(index < 0){
index = 0
var order = [, ...this]
// replace...
} else {
var order = [...this]
var old = order[index]
// nothing to do...
if(old === value){
return this } }
ordered
&& (order[index] = value)
// replace...
this.has(old)
&& this.delete(old)
this.add(value)
ordered
&& this.sort(order)
return this },
// XXX do we need this???
// ...should this be enough???
// new Set([...set].splice(..))
splice: function(from, count, ...items){
var that = this
var order = [...this]
var removed = order.splice(...arguments)
// update the set...
removed.forEach(this.delete.bind(this))
items.forEach(this.add.bind(this))
this.sort(order)
return removed },
})
SetProtoMixin(Set.prototype)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

43
lib/types/String.js Executable file
View File

@ -0,0 +1,43 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
var StringProtoMixin =
module.StringProtoMixin =
object.Mixin('StringProtoMixin', 'soft', {
capitalize: function(){
return this == '' ?
this
: this[0].toUpperCase() + this.slice(1) },
// Indent a block of text...
//
// .indent(<width>)
// .indent(<str>)
// -> <str>
//
indent: function(indent){
indent = typeof(indent) == typeof('str') ?
indent
: ' '.repeat(indent)
return indent + this.split(/\n/).join('\n'+ indent) },
})
StringProtoMixin(String.prototype)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

19
lib/types/_module.js Executable file
View File

@ -0,0 +1,19 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
/*********************************************************************/
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

211
lib/types/containers.js Executable file
View File

@ -0,0 +1,211 @@
/**********************************************************************
*
*
*
***********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
require('./Map')
var object = require('ig-object')
/*********************************************************************/
var UniqueKeyMap =
module.UniqueKeyMap =
object.Constructor('UniqueKeyMap', Map, {
// Format:
// Map([
// [ <elem>, Set([
// <original-name>,
// ...
// ]) ],
// ...
// ])
//
__keys_index: null,
get __keys(){
return (this.__keys_index =
this.__keys_index || new Map()) },
// Format:
// Map([
// [<unique-key>, <orig-key>],
// ...
// ])
//
__reverse_index: null,
get __reverse(){
return (this.__reverse_index =
this.__reverse_index || new Map()) },
// Pattern to be used to generate unique key...
//
__key_pattern__: '$KEY ($COUNT)',
// If true then a value can not be stored under the same key more
// than once...
//
// Example:
// var u = UniqueKeyMap()
// u.set('x', 123)
// // if .__unique_key_value__ is true this will have no effect,
// // otherwise 123 will be stored under 'x (1)'
// u.set('x', 123)
//
__unique_key_value__: false,
// helpers...
//
originalKey: function(key){
return this.__reverse.get(key) },
uniqieKey: function(key){
var n = key
var i = 0
while(this.has(n)){
i++
n = this.__key_pattern__
.replace(/\$KEY/, key)
.replace(/\$COUNT/, i) }
return n },
keysOf: function(elem, mode='original'){
// get unique keys...
if(mode == 'unique'){
return this
.entries()
.reduce(function(res, [k, e]){
e === elem
&& res.push(k)
return res }, []) }
// get keys used to set the values...
return [...(this.__keys.get(elem) || [])] },
// NOTE: this will never overwrite a key's value, to overwrite use .reset(..)
set: function(key, elem, return_key=false){
// index...
var names
this.__keys.set(elem,
names = this.__keys.get(elem) || new Set())
// key/elem already exists...
if(this.__unique_key_value__
&& names.has(key)){
return return_key ?
key
: this }
names.add(key)
// add the elem with the unique name...
var n
var res = object.parentCall(
UniqueKeyMap.prototype,
'set',
this,
n = this.uniqieKey(key),
elem)
// reverse index...
this.__reverse.set(n, key)
return return_key ?
n
: res },
// NOTE: this will never generate a new name...
reset: function(key, elem){
// rewrite...
if(this.has(key)){
// remove old elem/key from .__keys...
var o = this.originalKey(key)
var s = this.__keys.get(this.get(key))
s.delete(o)
s.size == 0
&& this.__keys.delete(this.get(key))
// add new elem/key to .__keys...
var n
this.__keys.set(elem, (n = this.__keys.get(elem) || new Set()))
n.add(o)
return object.parentCall(UniqueKeyMap.prototype, 'set', this, key, elem)
// add...
} else {
return this.set(key, elem) } },
delete: function(key){
var s = this.__keys.get(this.get(key))
if(s){
// XXX will this delete if key is with an index???
//s.delete(key)
s.delete(this.originalKey(key))
this.__reverse.delete(key)
s.size == 0
&& this.__keys.delete(this.get(key)) }
return object.parentCall(UniqueKeyMap.prototype, 'delete', this, key) },
// NOTE: this maintains the item order. This is done by rewriting
// items in sequence, this may be slow and trigger lots of write
// observer callbacks. to avoid this use .unOrderedRename(..)
// XXX do not see how can we avoid rewriting the map if we want to
// keep the order...
orderedRename: function(from, to, return_key=false){
var keys = [...this.keys()]
// rename the element...
var e = this.get(from)
this.delete(from)
var n = this.set(to, e, true)
// keep order...
keys.splice(keys.indexOf(from), 1, n)
this.sort(keys)
return return_key ?
n
: this },
// NOTE: the renamed item is appended to the map...
unorderedRename: function(from, to, return_key=false){
var e = this.get(from)
this.delete(from)
return this.set(to, e, return_key) },
__unorderd_rename__: false,
rename: function(from, to, return_key=false){
return this.__unorderd_writes__ ?
this.unorderedRename(...arguments)
: this.orderedRename(...arguments) },
})
//---------------------------------------------------------------------
// XXX should this be a map???
// XXX make two variants: ordered and unordered...
// ...the ordered variant should have the same API as an array...
var ObjectWithAutoKeys =
module.ObjectWithAutoKeys =
object.Constructor('ObjectWithAutoKeys', {
__last_key: null,
__new_key__: function(value){
return (this.__last_key = (this.__last_key || -1) + 1) },
//
// .push(value)
// -> key
//
// .push(value, ..)
// -> [key, ..]
//
push: function(...values){
var that = this
var res = values
.map(function(value){
that[that.__new_key__(value)] = value })
return values.length == 1 ?
res.pop()
: res },
})
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

382
lib/types/event.js Executable file
View File

@ -0,0 +1,382 @@
/**********************************************************************
*
*
*
* XXX need ability to extend event to implement proxy events...
* ...i.e. .on(..) / ... get called on one object but the handler
* bound on a different object via a proxy event method...
* XXX is types/events the right place for this???
* XXX should we have .pre/.post events???
* XXX should we propogate event handling to parent/overloaded events???
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
// Event method wrappers...
var EventCommand =
module.EventCommand =
object.Constructor('EventCommand', {
name: null,
__init__: function(name, data={}){
Object.assign(this, data, {name}) },
})
module.TRIGGER = module.EventCommand('TRIGGER')
// Create an "eventful" method...
//
// The resulting method can be either called directly or via .trigger(..).
// Handlrs can be bound to it via .on(..) and unbound via .off(..) and
// calling it will trigger the handlers either after the user func(..)
// return or when the user calles the passed handler(..) function.
//
// Eventful(name[, options])
// -> method
//
// Eventful(name, func[, options])
// -> method
//
//
// Trigger the event...
// method(...args)
// -> ..
//
//
// func(handle, ...args)
// -> ..
//
//
// trigger event handlers...
// handle()
// handle(true)
// -> true
// -> false
//
// trigger event handlers and overload handler arguments...
// handle(true, ...)
// -> true
// -> false
//
// prevent event handlers from triggering...
// handle(false)
// -> undefined
//
//
//
// Special case: EventCommand...
//
// EventCommand instance can be passed as the first argument of method,
// in this case the event function will get it but the event handlers
// will not...
// This is done to be able to externally pass commands to event methods
// that get handled in a special way by the function but not passed to
// the event handlers...
//
// method(<event-command>, ...args)
// -> ..
//
// func(handle, <event-command>, ...args)
// -> ..
//
//
//
// NOTE: calling handle(false) will exiplicitly disable calling the
// handlers for that call...
var Eventful =
module.Eventful =
object.Constructor('Eventful', {
handlerLocation: 'context',
name: null,
func: null,
toString: function(){
return this.func ?
`${this.constructor.name} `
+(this.func.toString()
.replace(/^(function[^(]*\()[^,)]*, ?/, '$1'))
: `${this.constructor.name} function ${this.name}(){}` },
__event_handlers__: null,
bind: function(context, handler){
var handlers =
// local...
this.handlerLocation == 'method' ?
(this.__event_handlers__ = this.__event_handlers__ || [])
// context (default)...
//: (context.__event_handlers__ == null ?
: !context.hasOwnProperty('__event_handlers__') ?
Object.defineProperty(context, '__event_handlers__', {
value: {[this.name]: (handlers = [])},
enumerable: false,
configurable: true,
writable: true,
})
&& handlers
: (context.__event_handlers__[this.name] =
context.__event_handlers__[this.name] || [])
// add handler...
handlers.push(handler)
return this },
unbind: function(context, handler){
var handlers =
this.handlerLocation == 'method' ?
method.__event_handlers__
//: (context.__event_handlers__ || {})[this.name]) || []
: context.hasOwnProperty('__event_handlers__') ?
(context.__event_handlers__ || {})[this.name] || []
: []
handlers.splice(0, handlers.length,
...handlers.filter(function(h){
return h !== handler
&& h.__event_original_handler !== handler }))
return this },
__call__: function(context, ...args){
var that = this
var handlers =
this.handlerLocation == 'method' ?
(this.__event_handlers__ || [])
: []
// context (default)...
// NOTE: these are allways called...
handlers = handlers
//.concat((context.__event_handlers__ || {})[this.name] || [])
.concat(context.hasOwnProperty('__event_handlers__') ?
(context.__event_handlers__ || {})[this.name] || []
: [])
// NOTE: this will stop event handling if one of the handlers
// explicitly returns false...
// NOTE: if the user does not call handle() it will be called
// after the event action is done but before it returns...
// NOTE: to explicitly disable calling the handlers func must
// call handle(false)
var did_handle = false
var handle = function(run=true, ...alt_args){
did_handle = true
var a = (run === true
&& arguments.length > 1) ?
alt_args
: args
a = a[0] instanceof EventCommand ?
a.slice(1)
: a
return run ?
handlers
.reduce(function(res, handler){
return res === true
&& handler.call(context, that.name, ...a) !== false }, true)
: undefined }
// call...
var res = this.func ?
this.func.call(context, handle, ...args)
: undefined
// call the handlers if the user either didn't call handle()
// or explicitly called handle(false)...
!did_handle
&& handle()
return res },
__init__: function(name, func, options={}){
options = func && typeof(func) != 'function' ?
func
: options
Object.assign(this, options)
Object.defineProperty(this, 'name', { value: name })
func
&& typeof(func) == 'function'
&& Object.defineProperty(this, 'func', {
value: func,
enumerable: false,
}) },
})
// Extends Eventful(..) adding ability to bind events via the
// resulting method directly by passing it a function...
//
// Event(name[, options])
// -> method
//
// Event(name, func[, options])
// -> method
//
//
// Bind handler...
// method(handler)
// -> this
//
// Unbind handler...
// method(handler, false)
// -> this
//
// Trigger handlers...
// method(...args)
// -> this
//
//
// func(handle, ...args)
//
//
// Special case:
//
// Force trigger event...
// method(TRIGGER, ...args)
// -> this
//
// This will pass args to the event action regardless whether the first
// arg is a function or not...
var Event =
module.Event =
object.Constructor('Event', Eventful, {
toString: function(){
return this.orig_func ?
'Event '
+this.orig_func.toString()
.replace(/^(function[^(]*\()[^,)]*, ?/, '$1')
: `Event function ${this.name}(){}`},
__call__: function(context, ...args){
// NOTE: when the first arg is an event command this will
// fall through to calling the action...
typeof(args[0]) == 'function' ?
// add handler...
this.bind(context, args[0])
// call the action...
: object.parentCall(Event.prototype.__call__, this, context, ...args)
// XXX workaround for above line -- remove when fully tested...
//: Eventful.prototype.__call__.call(this, context, ...args)
return context },
})
// Like Event(..) but produces an event method that can only be triggered
// via .trigger(name, ...), calling this is a no-op...
var PureEvent =
module.PureEvent =
object.Constructor('PureEvent', Event, {
toString: function(){
return `PureEvent ${this.name}(){}`},
__init__: function(name, options={}){
object.parentCall(PureEvent.prototype.__init__, this,
name,
function(handle, trigger){
trigger === module.TRIGGER
|| handle(false) }, options) },
})
//---------------------------------------------------------------------
// Mixins...
// XXX might be nice to add support to pre/post handlers...
// XXX still not sure about the builtin-local event control flow...
// XXX do we need to be able to force global handler???
var EventHandlerMixin =
module.EventHandlerMixin = object.Mixin('EventHandlerMixin', {
//__event_handlers__: null,
on: function(evt, func){
// event...
if(evt in this
&& this[evt].bind){
this[evt].bind(this, func)
// non-event...
} else {
//this.__event_handlers__ == null
!this.hasOwnProperty('__event_handlers__')
&& Object.defineProperty(this, '__event_handlers__', {
value: {},
enumerable: false,
configurable: true,
writable: true,
})
;(this.__event_handlers__[evt] =
this.__event_handlers__[evt] || [])
.push(func) }
return this },
one: function(evt, func){
var handler
this.on(evt,
handler = Object.assign(
function(handle, ...args){
this.off(evt, handler)
return func.call(this, handle, ...args) }.bind(this),
{__event_original_handler: func}))
return this },
// XXX do we need .off(evt, 'all')
off: function(evt, func){
// event...
if(evt in this
&& this[evt].unbind){
this[evt].unbind(this, func)
// non-event...
} else {
var handlers = this.__event_handlers__
&& (this.__event_handlers__[evt] || [])
handlers
&& handlers.splice(0, handlers.length,
...handlers.filter(function(h){
return h !== func
&& h.__event_original_handler !== func })) }
return this },
// XXX revise...
trigger: function(evt, ...args){
evt in this ?
// XXX add a better check...
this[evt](module.TRIGGER, ...args)
//: this.__event_handlers__
: this.hasOwnProperty('__event_handlers__')
&& (this.__event_handlers__[evt] || [])
.forEach(function(h){ h(evt, ...args) })
return this },
})
// NOTE: this can't be added via Object.assign(..), use object.mixinFlat(..)
// instead...
var EventDocMixin =
module.EventDocMixin = object.Mixin('EventDocMixin', {
get eventful(){
return object.deepKeys(this)
.filter(function(n){
// avoid triggering props...
return !object.values(this, n, true).next().value.get
// XXX this is too strict...
&& (this[n] || {}).constructor === Eventful}.bind(this)) },
get events(){
return object.deepKeys(this)
.filter(function(n){
// avoid triggering props...
return !object.values(this, n, true).next().value.get
// XXX this is too strict...
&& (this[n] || {}).constructor === Event }.bind(this)) },
})
var EventMixin =
module.EventMixin =
object.Mixin('EventMixin',
EventHandlerMixin,
EventDocMixin)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

622
lib/types/generator.js Executable file
View File

@ -0,0 +1,622 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
var stoppable = require('ig-stoppable')
/*********************************************************************/
// NOTE: this is used in a similar fashion to Python's StopIteration...
var STOP =
module.STOP =
stoppable.STOP
//---------------------------------------------------------------------
// The generator hierarchy in JS is a bit complicated.
//
// Consider the following:
//
// // this is the generator function (i.e. the constructor)
// var Iter = function*(lst){
// for(var e of lst){
// yield e }}
//
// // this is the generator instance (constructed instance)...
// var iter = Iter([1,2,3])
//
//
// In this module we need to add methods to be visible from either Iter
// or iter from the above example, so we need to access the prototypes
// of each of them.
// So, below we will define:
//
// Generator.prototype
// prototype of the generator constructors (i.e. Iter(..) from the
// above example)
//
// Generator.prototype.prototype
// generator instance prototype (i.e. iter for the above code)
//
//
// Also the following applies:
//
// iter instanceof Iter // -> true
//
// Iter instanceof Generator
//
//
// NOTE: there appears to be no way to test if iter is instance of some
// generic Generator...
//
//---------------------------------------------------------------------
var Generator =
module.Generator =
(function*(){}).constructor
var AsyncGenerator =
module.AsyncGenerator =
(async function*(){}).constructor
// base iterator prototypes...
var ITERATOR_PROTOTYPES = [
Array,
Set,
Map,
].map(function(e){
return (new e()).values().__proto__ })
//---------------------------------------------------------------------
// generic generator wrapper...
// helper...
var __iter =
module.__iter =
function*(lst=[]){
if(typeof(lst) == 'object'
&& Symbol.iterator in lst){
yield* lst
} else {
yield lst } }
// XXX updatae Array.js' version for compatibility...
// XXX DOCS!!!
var iter =
module.iter =
Generator.iter =
stoppable(function(lst=[]){
// handler -> generator-constructor...
if(typeof(lst) == 'function'){
// we need to be callable...
var that = this instanceof Function ?
this
// generic root generator...
: module.__iter
return function*(){
yield* that(...arguments).iter(lst) } }
// no handler -> generator instance...
return module.__iter(lst) })
// NOTE: we need .iter(..) to both return generators if passed an iterable
// and genereator constructos if passed a function...
iter.__proto__ = Generator.prototype
//---------------------------------------------------------------------
// Generator.prototype "class" methods...
//
// the following are effectively the same:
// 1) Wrapper
// var combined = function(...args){
// return someGenerator(...args)
// .filter(function(e){ ... })
// .map(function(e){ ... }) }
//
// combined( .. )
//
// 2) Static generator methods...
// var combined = someGenerator
// .filter(function(e){ ... })
// .map(function(e){ ... })
//
// combined( .. )
//
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Helpers...
//
// makeGenerator(<name>)
// makeGenerator(<name>, <handler>)
// -> <func>
//
// makeGenerator('async', <name>)
// makeGenerator('async', <name>, <handler>)
// -> <func>
//
//
// <func>(...args)
// -> <Generator>
//
// <Generator>(...inputs)
// -> <generator>
//
// <handler>(args, ...inputs)
// -> args
//
//
// XXX this needs to be of the correct type... (???)
// XXX need to accept generators as handlers...
var makeGenerator = function(name, pre){
var sync = true
if(name == 'async'){
sync = false
var [name, pre] = [...arguments].slice(1) }
return function(...args){
var that = this
return Object.assign(
// NOTE: the two branches here are identical, the only
// difference is the async keyword...
sync ?
function*(){
var a = pre ?
pre.call(this, args, ...arguments)
: args
yield* that(...arguments)[name](...a) }
: async function*(){
var a = pre ?
pre.call(this, args, ...arguments)
: args
yield* that(...arguments)[name](...a) },
{ toString: function(){
return [
that.toString(),
// XXX need to normalize args better...
`.${ name }(${ args.join(', ') })`,
].join('\n ') }, }) } }
// XXX do a better doc...
var makePromise = function(name){
return function(...args){
var that = this
return function(){
return that(...arguments)[name](func) } } }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var GeneratorMixin =
module.GeneratorMixin =
object.Mixin('GeneratorMixin', 'soft', {
STOP: object.STOP,
iter: module.iter,
gat: makeGenerator('gat'),
at: function(i){
var that = this
return Object.assign(
function(){
return that(...arguments).at(i) },
{ toString: function(){
return that.toString()
+ '\n .at('+ i +')'}, }) },
slice: makeGenerator('slice'),
flat: makeGenerator('flat'),
map: makeGenerator('map'),
filter: makeGenerator('filter'),
reduce: makeGenerator('reduce'),
reduceRight: makeGenerator('reduceRight'),
// non-generators...
//
toArray: function(){
var that = this
return Object.assign(
function(){
return that(...arguments).toArray() },
{ toString: function(){
return that.toString()
+ '\n .toArray()'}, }) },
gpop: makeGenerator('gpop'),
pop: function(){
var that = this
return Object.assign(
function(){
//return that(...arguments).toArray().pop() },
return that(...arguments).pop() },
{ toString: function(){
return that.toString()
+ '\n .pop()'}, }) },
push: makeGenerator('push'),
gshift: makeGenerator('gshift'),
shift: function(){
var that = this
return Object.assign(
function(){
//return that(...arguments).toArray().shift() },
return that(...arguments).shift() },
{ toString: function(){
return that.toString()
+ '\n .shift()'}, }) },
unshift: makeGenerator('unshift'),
// promises...
//
then: makePromise('then'),
catch: makePromise('catch'),
finally: makePromise('finally'),
// combinators...
//
chain: makeGenerator('chain'),
concat: makeGenerator('concat',
// initialize arguments...
function(next, ...args){
return next
.map(function(e){
return (e instanceof Generator
|| typeof(e) == 'function') ?
e(...args)
: e }) }),
//zip: makeGenerator('zip'),
enumerate: makeGenerator('enumerate'),
// XXX should this have a .gjoin(..) companion...
join: function(){
var args = [...arguments]
var that = this
return Object.assign(
function(){
//return that(...arguments).toArray().shift() },
return that(...arguments).join(...args) },
{ toString: function(){
return that.toString()
+ '\n .join()'}, }) },
})
var GeneratorProtoMixin =
module.GeneratorProtoMixin =
object.Mixin('GeneratorProtoMixin', 'soft', {
// XXX use module.iter(..) ???
iter: stoppable(function*(handler){
if(handler){
var i = 0
for(var elem of this){
var res = handler.call(this, elem, i)
// expand iterables...
if(typeof(res) == 'object'
&& Symbol.iterator in res){
yield* res
// as-is...
} else {
yield res }}
// no handler...
} else {
yield* this } }),
//*/
at: function(i){
return this.gat(i).next().value },
// XXX this needs the value to be iterable... why???
gat: function*(i){
// sanity check...
if(i < 0){
throw new Error('.gat(..): '
+'generator index can\'t be a negative value.')}
for(var e of this){
if(i-- == 0){
yield e
return } } },
// NOTE: this is different from Array's .slice(..) in that it does not
// support negative indexes -- this is done because there is no way
// to judge the length of a generator until it is fully done...
slice: function*(from=0, to=Infinity){
// sanity check...
if(from < 0 || to < 0){
throw new Error('.slice(..): '
+'generator form/to indexes can\'t be negative values.')}
var i = 0
for(var e of this){
// stop at end of seq...
if(i >= to){
return }
// only yield from from...
if(i >= from){
yield e }
i++ } },
// XXX do we need a version that'll expand generators???
flat: function*(depth=1){
if(depth == 0){
return this }
for(var e of this){
// expand array...
if(e instanceof Array){
for(var i=0; i < e.length; i++){
if(depth <= 1){
yield e[i]
} else {
yield* typeof(e[i].flat) == 'function' ?
e[i].flat(depth-1)
: e[i] } }
// item as-is...
} else {
yield e } } },
// NOTE: if func is instanceof Generator then it's result (iterator)
// will be expanded...
// NOTE: there is no point to add generator-handler support to either
// .filter(..) or .reduce(..)
map: stoppable(
function*(func){
var i = 0
if(func instanceof Generator){
for(var e of this){
yield* func(e, i++, this) }
} else {
for(var e of this){
yield func(e, i++, this) } } }),
filter: stoppable(function*(func){
var i = 0
try{
for(var e of this){
if(func(e, i++, this)){
yield e } }
// normalize the stop value...
} catch(err){
if(err instanceof STOP){
if(!err.value){
throw STOP }
err.value = e }
throw err } }),
reduce: stoppable(function(func, res){
var i = 0
for(var e of this){
res = func(res, e, i++, this) }
return res }),
greduce: function*(func, res){
yield this.reduce(...arguments) },
pop: function(){
return [...this].pop() },
// XXX this needs the value to be iterable...
gpop: function*(){
yield [...this].pop() },
push: function*(value){
yield* this
yield value },
shift: function(){
return this.next().value },
// XXX this needs the value to be iterable...
gshift: function*(){
yield this.next().value },
unshift: function*(value){
yield value
yield* this },
// non-generators...
//
toArray: function(){
return [...this] },
// promises...
//
then: function(onresolve, onreject){
var that = this
var p = new Promise(
function(resolve){
resolve([...that]) })
p = (onresolve || onreject) ?
p.then(...arguments)
: p
return p },
catch: function(func){
return this.then().catch(func) },
finally: function(func){
return this.then().finally(func) },
// combinators...
//
chain: function*(...next){
yield* next
.reduce(function(cur, next){
return next(cur) }, this) },
concat: function*(...next){
yield* this
for(var e of next){
yield* e } },
// XXX EXPERIMENTAL...
/* XXX not sure how to do this yet...
tee: function*(...next){
// XXX take the output of the current generator and feed it into
// each of the next generators... (???)
},
zip: function*(...items){
// XXX
},
//*/
enumerate: function*(){
var i = 0
for(var e of this){
yield [i++, e] } },
join: function(){
return [...this]
.join(...arguments) },
})
GeneratorMixin(Generator.prototype)
GeneratorProtoMixin(Generator.prototype.prototype)
// Extend base iterators...
ITERATOR_PROTOTYPES
.forEach(function(proto){
GeneratorProtoMixin(proto) })
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX EXPERIMENTAL...
var AsyncGeneratorMixin =
module.AsyncGeneratorMixin =
object.Mixin('AsyncGeneratorMixin', 'soft', {
// XXX TEST...
iter: makeGenerator('async', 'iter'),
map: makeGenerator('async', 'map'),
filter: makeGenerator('async', 'filter'),
reduce: makeGenerator('async', 'reduce'),
})
var AsyncGeneratorProtoMixin =
module.AsyncGeneratorProtoMixin =
object.Mixin('AsyncGeneratorProtoMixin', 'soft', {
// promise...
//
// NOTE: this will unwind the generator...
// XXX create an iterator promise???
// XXX should we unwind???
then: function(resolve, reject){
var that = this
var p = new Promise(async function(_resolve, _reject){
var res = []
for await(var elem of that){
res.push(elem) }
_resolve(res) })
p = (resolve || reject) ?
p.then(...arguments)
: p
return p },
catch: function(func){
return this.then().catch(func) },
finally: function(){
return this.then().finally(func) },
// XXX might be a good idea to use this approach above...
iter: stoppable(async function*(handler=undefined){
var i = 0
if(handler){
for await(var e of this){
var res = handler.call(this, e, i++)
if(typeof(res) == 'object'
&& Symbol.iterator in res){
yield* res
} else {
yield res } }
} else {
yield* this } }),
map: async function*(func){
yield* this.iter(function(elem, i){
return [func.call(this, elem, i)] }) },
filter: async function*(func){
yield* this.iter(function(elem, i){
return func.call(this, elem, i) ?
[elem]
: [] }) },
// NOTE: there is not much point in .reduceRight(..) in an async
// generator as we'll need to fully unwind it then go from the
// end...
reduce: async function(func, state){
this.iter(function(elem, i){
state = func.call(this, state, elem, i)
return [] })
return state },
// XXX TEST...
chain: async function*(...next){
yield* next
.reduce(function(cur, next){
return next(cur) }, this) },
flat: async function*(){
for await(var e of this){
if(e instanceof Array){
yield* e
} else {
yield e }}},
concat: async function*(other){
yield* this
yield* other },
push: async function*(elem){
yield* this
yield elem },
unsift: async function*(elem){
yield elem
yield* this },
join: async function(){
return [...(await this)]
.join(...arguments) },
// XXX
// slice -- not sure if we need this...
// ...
})
AsyncGeneratorMixin(AsyncGenerator.prototype)
AsyncGeneratorProtoMixin(AsyncGenerator.prototype.prototype)
//---------------------------------------------------------------------
// Generators...
// NOTE: step can be 0, this will repeat the first element infinitely...
var range =
module.range =
function*(from, to, step){
if(to == null){
to = from
from = 0 }
step = step ?? (from > to ? -1 : 1)
while(step > 0 ?
from < to
: from > to){
yield from
from += step } }
var repeat =
module.repeat =
function*(value=true, stop){
while( typeof(stop) == 'function' && stop(value) ){
yield value } }
var produce =
module.produce =
stoppable(function*(func){
while(true){
yield func() } })
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

45
lib/types/main.js Executable file
View File

@ -0,0 +1,45 @@
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
// Extend built-in types...
require('./Object')
require('./Array')
require('./Set')
require('./Map')
require('./String')
require('./RegExp')
require('./Promise')
module.patchDate = require('./Date').patchDate
// Additional types...
module.containers = require('./containers')
module.func = require('./Function')
module.generator = require('./generator')
module.event = require('./event')
module.runner = require('./runner')
// Shorthands...
module.STOP = object.STOP
// frequently used stuff...
module.AsyncFunction = module.func.AsyncFunction
module.Generator = module.generator.Generator
module.AsyncGenerator = module.generator.AsyncGenerator
// XXX doc...
module.iter = module.generator.iter
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

975
lib/types/runner.js Executable file
View File

@ -0,0 +1,975 @@
/**********************************************************************
*
*
*
*
* XXX would be helpful to define a task api...
* task('abort') vs. task.abort(), task state, ...etc.
* then define Task and TaskQueue(Queue) and extended api to:
* - task state introspection
* - stop/resume tasks (or task queue?)
* - serialize tasks
* - ...
* would be nice to make the task just a slightly extended or better
* defined function/generator, ideally to make them interchangable...
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
require('./Array')
require('./Promise')
var events = require('./event')
/*********************************************************************/
module.STOP = object.STOP
module.SKIP = {doc: 'skip queue item',}
//---------------------------------------------------------------------
// Queue...
//
// A means to manage execution of large-ish number of small tasks...
//
// A queue is a list of async functions that get executed in order and
// not more than .pool_size can run at a time, i.e. new tasks get
// started only only when tasks in the running pool either finish or
// release their spot in the pool.
//
// XXX would be nice to:
// - nest queues -- DONE
// - chain queues
// - weave queues -- a-la generator's .map(..) etc.
// XXX need to configure to run a specific amount of jobs on each start...
// XXX document the Queue({handler: e => e*e}, 1,2,3,4) use-case...
// ...this is essentially a .map(..) variant...
var Queue =
module.Queue =
object.Constructor('Queue', Array, {
// create a running queue...
//
runTasks: function(...tasks){
if(typeof(tasks[0]) != 'function'
&& !(tasks[0] instanceof Queue)
&& typeof(tasks[0].finally) != 'function'){
var [options, ...tasks] = arguments }
return this(
Object.assign({},
options || {},
{ state: 'running' }),
...tasks) },
// Create a handler queue...
//
// Queue.handle(func, ...data)
// Queue.handle(options, func, ...data)
// -> queue
//
// NOTE: func(..) should be compatible with .handler(..) instance method...
// NOTE: this is a shorthand for:
// Queue({handler: func, ...}, ...data)
handle: function(handler, ...data){
// NOTE: this is a simpler test than in .runTasks(..) above because
// here we are expecting a function as the first arg in the
// general case while above a non-task is the exception..
if(typeof(handler) != 'function'){
var [options, handler, ...data] = arguments }
return this(
Object.assign({},
options || {},
{handler}),
...data) },
}, events.EventMixin('flat', {
// Config...
//
// Number of tasks to be running at the same time...
pool_size: 8,
// Number of tasks to run before letting go of the exec frame...
pause_after_sync: 4,
// XXX
auto_start: false,
// Start synchronously...
//
// NOTE: this affects the start only, all other timeouts apply as-is...
sync_start: false,
catch_errors: true,
// If true, stop after queue is depleted...
auto_stop: false,
// NOTE: for long running queues this may get quite big...
collect_results: true,
// Sub-queue handling mode...
//
// This can be:
// 'wait' - wait fot the sun-queue to stop
// 'unwind' - run sub-task and requeue parent
//
// XXX do we need this???
// XXX should the nested queue decide??? ...how???
sub_queue: 'unwind',
// If true only add unique items to queue...
// XXX not implemented yet...
// ...how should this be done?
// - keep a set of seen elements and check against it?
// - check against the queue contents?
unique_items: false,
// Timeouts...
//
// Time to wait when pool is full...
// if 'auto', wait the average task time * .busy_timeout_scale.
// XXX revise defaults...
busy_timeout: 50,
//busy_timeout: 'auto',
busy_timeout_scale: 5,
// Time to wait between checks for new tasks in an empty queue...
poling_timeout: 200,
// Time to pause after a set of .pause_after_sync sync tasks...
pause_timeout: 0,
// Runtime statistics...
//
// To disable set to false
//
// NOTE: this, if true, will get replaced with the stats...
runtime_stats: true,
//
// This can be:
// 'running'
// 'stopped'
//
__state: null,
get state(){
return this.__state
|| 'stopped' },
set state(value){
if(value == 'running'){
this.start()
} else if(value == 'stopped'){
this.stop()
} else if(value == 'aborted'){
this.abort() } },
// events/actions - state transitions...
//
// NOTE: to start synchronously call .start(true), this will not
// affect further operation...
//
// XXX would be nice to run a specific number of tasks and stop...
// XXX might be a good idea to let the user set .__wait_for_items...
// XXX should we wait for items on empty?
__wait_for_items: null,
start: events.Event('start', function(handle, sync){
// first start -- wait for items...
if('__state' in this){
this.__wait_for_items = true }
// can't start while running...
if(this.__state == 'running' || this.__state == 'aborted'){
return handle(false) }
this.__state = 'running'
// XXX if empty start polling...
this.__run_tasks__(sync) }),
stop: events.Event('stop', function(handle){
// can't stop while not running...
if(this.state == 'stopped' || this.state == 'aborted'){
return handle(false) }
this.__state = 'stopped' }),
// events...
//
// .tasksAdded(func(evt, [task, ..]))
// .taskStarting(func(evt, task))
// .taskCompleted(func(evt, task, res))
// .queueEmpty(func(evt))
//
tasksAdded: events.PureEvent('tasksAdded'),
taskStarting: events.PureEvent('taskStarting'),
taskCompleted: events.PureEvent('taskCompleted'),
taskFailed: events.PureEvent('taskFailed'),
queueEmpty: events.PureEvent('queueEmpty'),
// Runner API...
//
// Run the given task type...
// .handler(task[, next])
// -> STOP
// -> STOP(value)
// -> SKIP
// -> queue
// -> promise
// -> func
// -> ...
//
// NOTE: this intentionally does not handle results as that whould
// require this to also handle events, and other runtime stuff...
// ...to add a new task/result type either handle the non-standard
// result here or wrap it into a standard return value like a
// promise...
handler: function(task, next){
return typeof(task) == 'function' ?
task()
: (task instanceof Queue
&& this.sub_queue == 'unwind') ?
(task.runTask(next), task)
: (task instanceof Queue
&& this.sub_queue == 'wait') ?
task.start()
: task },
//
// Hanlde 'running' state (async)...
// .__run_tasks__()
// -> this
//
// NOTE: .sync_start affects only the first run...
// NOTE: we do not store the exec results...
// NOTE: not intended for direct use and will likely have no effect
// if called directly...
//
// XXX will this be collected by the GC if it is polling???
__running: null,
__run_tasks__: function(sync){
var that = this
sync = sync == null ?
this.sync_start
: sync == 'async' ?
false
: !!sync
var run = function(){
var c = 0
var pause = this.pause_after_sync
var running = this.__running || []
// run queue...
while(this.length > 0
&& this.state == 'running'
// do not exceed pool size...
&& running.length < this.pool_size
// do not run too many sync tasks without a break...
&& (pause == null
|| c < pause)){
var p = running.length
delete this.__wait_for_items
this.runTask(this.__run_tasks__.bind(this))
// NOTE: only count sync stuff that does not get added
// to the pool...
p == running.length
&& c++ }
// empty queue -> pole or stop...
//
// NOTE: we endup here in two cases:
// - the pool is full
// - the queue is empty
// NOTE: we do not care about stopping the timer when changing
// state as .__run_tasks__() will stop itself...
if(this.state == 'running'){
var timeout =
// idle -- empty queue...
this.length == 0 ?
this.poling_timeout
// busy poling -- pool full...
: c < pause ?
//this.busy_timeout
(this.runtime_stats && this.busy_timeout == 'auto' ?
(this.runtime_stats.avg_t || 50) * (this.busy_timeout_scale || 2)
: this.busy_timeout == 'auto' ?
50 * (this.busy_timeout_scale || 2)
: this.busy_timeout)
// pause -- let other stuff run...
: (this.pause_timeout || 0)
;(this.length == 0
&& this.auto_stop
&& !this.__wait_for_items) ?
// auto-stop...
this.__onempty__()
// pole / pause...
: timeout != null
&& setTimeout(
this.__run_tasks__.bind(this), timeout) } }.bind(this)
this.state == 'running'
&& (sync ?
run()
: setTimeout(run, 0))
return this },
__onempty__: function(){
var that = this
this.poling_timeout != null ?
// wait a bit then stop if still empty...
setTimeout(function(){
that.length > 0 ?
that.__run_tasks__()
: that.stop()
}, this.poling_timeout)
// stop now...
: this.stop()
return this },
// run one task from queue...
// NOTE: this does not care about .state...
// XXX revise error handling...
// XXX ABORT: added nested abort support...
__results: null,
runTask: function(next){
var that = this
var running = this.__running = this.__running || []
// can't run...
if(this.length == 0
|| running.length >= this.pool_size ){
return this }
// closure: running, task, res, stop, next...
var taskCompleted = function(){
// calculate runtime statistics...
if(that.runtime_stats){
var x = Date.now() - t0
var s = that.runtime_stats =
that.runtime_stats
|| {max_t: x, min_t: x, avg_t: x, count: 0}
s.max_t = Math.max(s.max_t, x)
s.min_t = Math.min(s.min_t, x)
var i = ++s.count
var a = s.avg_t
s.avg_t = a + (x - a)/i }
// report...
that.trigger('taskCompleted', task, res) }
var fail = {doc: 'fail runningDone(..)'}
var runningDone = function(mode){
running.splice(0, running.length,
// NOTE: there can be multiple occurrences of res...
...running
.filter(function(e){ return e !== res }))
mode === fail
|| taskCompleted()
!stop && next
&& next() }
var task = this.shift()
this.trigger('taskStarting', task)
var t0 = this.runtime_stats && Date.now()
// run...
// catch and pass errors to .taskFailed(...)
if(this.catch_errors){
var err
try {
var res = this.handler(task, next)
} catch(err){
this.trigger('taskFailed', task, err) }
// promise result...
// XXX is the err test here needed???
res
&& err === undefined
&& res.catch
&& res.catch(function(err){
that.trigger('taskFailed', task, err) })
// let errors rain...
} else {
var res = this.handler(task, next) }
// handle stop...
var stop = res === module.STOP
|| res instanceof module.STOP
res = res instanceof module.STOP ?
res.value
: res === module.STOP ?
undefined
: res
// collect results...
this.collect_results
&& res !== module.SKIP
&& (this.__results = this.__results || []).push(res)
// handle task results...
//
// queue -- as a set of tasks...
if(res instanceof Queue
&& this.sub_queue == 'unwind'){
if(res.length > 0){
this.push(res) }
taskCompleted()
// queue -- as a task...
} else if(res instanceof Queue
&& this.sub_queue == 'wait'){
if(res.state == 'stopped'){
taskCompleted()
} else {
running.push(res)
res.stop(function(){
// not fully done yet -- re-queue...
res.length > 0
&& that.push(res)
runningDone() }) }
// pool async (promise) task...
// XXX REVISE...
// XXX do we need to report errors???
} else if(typeof((res || {}).then) == 'function'
// one post handler is enough...
// XXX will this prevent some tasks from reporting???
&& !running.includes(res)){
running.push(res)
//res.finally(runningDone)
res.then(
runningDone,
...(this.catch_errors ?
[function(err){
runningDone(fail)
that.trigger('taskFailed', task, err) }]
// let errors propagate...
: []))
// func -> re-queue tasks...
} else if(typeof(res) == 'function'){
taskCompleted()
this.push(res)
// completed sync task...
} else {
taskCompleted() }
this.length == 0
&& this.trigger('queueEmpty')
stop
&& this.stop()
return res },
// helpers...
//
// move tasks to head/tail of queue resp.
prioritize: function(...tasks){
return this.sortAs(tasks) },
delay: function(...tasks){
return this.sortAs(tasks, true) },
// edit/add API...
//
// trigger .tasksAdded(..) on relevant methods...
//
// NOTE: adding tasks via the [..] notation will not trigger the
// event...
// NOTE: the events will not be triggered on no-op calls...
//
// XXX add methods that can shorten the queue (like .pop()/.shift()/..)
// to test and trigger .queueEmpty()
// ...this is not and will not be done on polling as that would
// introduce issues -- queue can change between task runs... (revise!)
push: function(...tasks){
var res = object.parentCall(Queue.prototype.push, this, ...tasks)
tasks.length > 0
&& this.trigger('tasksAdded', tasks)
return res },
unsift: function(...tasks){
var res = object.parentCall(Queue.prototype.unshift, this, ...tasks)
tasks.length > 0
&& this.trigger('tasksAdded', tasks)
return res },
splice: function(...args){
var l = this.length
var res = object.parentCall(Queue.prototype.splice, this, ...args)
var tasks = args.slice(2)
tasks.length > 0
&& this.trigger('tasksAdded', tasks)
// length changed...
l != 0 && this.length == 0
&& this.trigger('queueEmpty')
return res },
// shorthands...
//
// NOTE: this helps get around the argument number limitation in JS...
add: function(tasks){
// handle too large a number of args...
var MAX_ARGS = 10000
if(tasks.length > MAX_ARGS){
while(tasks.length > 0){
this.push(...tasks.splice(0, MAX_ARGS)) }
return this }
this.push(...tasks)
return this },
// NOTE: this will also clear the results cache...
clear: function(full=false){
full
&& (delete this.__results)
this.splice(0, this.length) },
// constructor argument handling...
//
// Queue()
// -> queue
//
// Queue(..,tasks)
// -> queue
//
// Queue(options)
// Queue(options, ..,tasks)
// -> queue
//
__init__: function(options){
// options...
if(!(this[0] instanceof Queue)
&& this[0] instanceof Object
&& typeof(this[0]) != 'function'
// XXX do we need this test???
&& typeof(this[0].finally) != 'function'){
Object.assign(this, this.shift()) }
this.length > 0
&& this.trigger('tasksAdded', [...this])
// see if we need to start...
this.__run_tasks__() },
}))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Like Queue(..) but adds terminal states and conversion to promises...
//
// XXX should this .freeze()??/
// XXX find a better name...
var FinalizableQueue =
module.FinalizableQueue =
object.Constructor('FinalizableQueue', Queue, {
auto_stop: true,
__onempty__: function(){
return this.trigger('done') },
done: events.Event('done', function(handle){
// abort only once...
if(this.state == 'aborted' || this.state == 'done'){
return handle(false) }
this.__state = 'done'
Object.freeze(this) }),
abort: events.Event('abort', function(handle){
// abort only once...
if(this.state == 'aborted' || this.state == 'done'){
return handle(false) }
this.__state = 'aborted'
Object.freeze(this) }),
// NOTE: each handler will get called once when the next time the
// queue is emptied...
promise: function(){
var that = this
return new Promise(function(resolve, reject){
that
.one('done', function(){
resolve(...(that.collect_results ?
[that.__results || []]
: [])) })
.one('abort', reject) }) },
then: function(onresolve, onreject){
return this.promise().then(...arguments) },
catch: function(onreject){
return this.promise().catch(...arguments) },
})
//---------------------------------------------------------------------
// Task manager...
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Helpres...
// Task ticket...
//
// This lets the client control the task object and receive messages
// from it.
//
// NOTE: this is not intended for direct use...
var TaskTicket =
// XXX do we let the user see this???
module.TaskTicket =
object.Constructor('TaskTicket', Promise, {
__data: null,
title: null,
task: null,
get state(){
return this.__data.state },
resolve: function(...args){
if(this.__data.state == 'pending'){
this.__data.state = 'resolved'
this.__data.resolve(...args) }
return this },
reject: function(...args){
if(this.__data.state == 'pending'){
this.__data.state = 'rejected'
this.__data.reject(...args) }
return this },
onmessage: function(msg, func){
this.__data.onmessage(
typeof(msg) == 'function' ?
msg
: function(m, ...args){
m == msg
&& func(...args) })
return this },
then: Promise.iter.prototype.then,
__new__: function(_, title, resolve, reject, onmessage, task){
var handlers
var resolver = arguments[1]
var obj = Reflect.construct(
TaskTicket.__proto__,
[function(resolve, reject){
handlers = {resolve, reject}
// NOTE: this is here to support builtin .then(..)
typeof(resolver) == 'function'
&& resolver(resolve, reject) }],
TaskTicket)
// if we got a resolver then it's an internal constructor we are
// not using (likely in base .then(..)) so there is no point in
// moving on...
// NOTE: this may be a potential source of bugs so we need to
// keep tracking this (XXX)
if(typeof(resolver) == 'function'){
return obj }
// bind this to external resolve/reject...
obj.then(
function(){
resolve(...arguments) },
function(){
reject(...arguments) })
// setup state...
obj.title = title
obj.task = task
Object.defineProperty(obj, '__data', {
value: {
resolve: handlers.resolve,
reject: handlers.reject,
onmessage,
state: 'pending',
},
enumerable: false,
})
return obj },
})
// NOTE: this is not intended for direct use...
var TaskMixin =
module.TaskMixin =
object.Mixin('TaskMixin', 'soft', {
// standard messages...
stop: function(){
this.send('stop', ...arguments) },
})
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Task manager...
//
// Externally manage/influence long running tasks...
//
// A task can be:
// - Promise.interactive(..)
// - Queue(..)
// - function(ticket, ..)
// - object supporting task protocol
//
//
// The task is controlled by passing messages, default messages include:
// - .stop(..)
//
//
// Task protocol:
// .then(..) - registers a completion handler (a-la Promise)
// .stop(..) - triggers a task to stop
//
//
// NOTE: we should keep the API here similar to Queue...
// ...but this is not a queue in principle (internal vs. external
// management) so we'll also need to keep them different enough to
// avoid confusion...
//
// XXX should a task manager have a pool size???
// ...if yes it would be fun to use the queue to manage the pool...
// XXX revise .abort(..)
var TaskManager =
module.TaskManager =
object.Constructor('TaskManager', Array, events.EventMixin('flat', {
__task_ticket__: TaskTicket,
__task_mixin__: TaskMixin,
// settings...
//
// if true start/end times will be set on the task:
// .time_started
// .time_ended
record_times: true,
// if true the task will be started sync before .Task(..) is returns..
//
// NOTE: this is not recommended as the default as this can block the
// manager...
sync_start: false,
//
// .titled(title)
// .titled(title, ..)
// -> manager
//
titled: function(title){
if(title == 'all' || title == '*'){
return this }
var titles = new Set([...arguments])
return this
.filter(function(task){
return titles.has(task.title)
|| titles.has(task.name) }) },
// actions...
//
// commands to test as methods...
// i.e. task.send(cmd, ...args) -> task[cmd](...args)
__send_commands__: [ 'stop', 'abort'],
send: function(title, ...args){
var that = this
if(title == 'all' || title == '*'){
;[...this].forEach(function(task){
'send' in task ?
task.send(...args)
: that.__send_commands__.includes(args[0]) ?
task[args[0]](...args.slice(1))
// XXX
: console.warn('.send(..): can\'t .send(..) to:', task)
})
return this }
return this.titled(
...(title instanceof Array) ?
title
: [title])
.send('all', ...args) },
// XXX should this be an event???
// the best way to go would be to proxy this to task-specific
// variants and register handlers on the tasks...
// ...should work with .on(..) / ... and other event methods...
stop: function(title='all'){
this.send(title, 'stop')
return this },
// XXX
abort: function(title='all'){
this.send(title, 'abort')
return this },
// events...
//
// XXX need to be able to bind to proxy events...
done: events.PureEvent('done'),
error: events.PureEvent('error'),
tasksDone: events.PureEvent('tasksDone'),
// Create/start a task...
//
// Create a task...
// .Task(task)
// .Task(title, task)
// -> task-handler
//
//
// Create a function task...
// .Task(func, ..)
// .Task(title, func, ..)
// -> task-handler
//
// func(ticket, ..)
//
//
// A task can be:
// - Promise.cooperative instance
// - Queue instance
// - function
// - Promise instance
//
// The task-manager is a Promise.interactive(..) instance with
// TaskMixin added.
//
// The ticket is a TaskTicket instance, see it for reference...
//
//
//
// We can also force a specific task to start sync/async regardless
// of the .sync_start setting:
//
// .Task('sync', task)
// .Task('sync', title, task)
// .Task(title, 'sync', task)
// -> task-handler
//
// .Task('async', task)
// .Task('async', title, task)
// .Task(title, 'async', task)
// -> task-handler
//
//
// sync/async start mode apply only to function tasks and tasks that
// have a .start() method like Queue's...
//
//
// NOTE: 'sync' more for a blocking task will block the task manager.
// NOTE: only function tasks accept args.
// NOTE: the task is started as soon as it is accepted.
// NOTE: tasks trigger events only on the task-manager instance that
// they were created in...
// XXX try and make all event handlers to be registered in the
// task itself and all the manager events just be proxies
// to tasks...
// ...this needs to work with all the event methods...
Task: function(title, task, ...args){
var that = this
var _args = [...arguments]
// parse args...
var sync_start = this.sync_start
if(title == 'sync' || title == 'async'){
;[sync_start, title, task, ...args] = _args
sync_start = sync_start == 'sync'
} else if(task == 'sync' || task == 'async'){
;[title, sync_start, task, ...args] = _args
sync_start = sync_start == 'sync' }
// anonymous task...
if(typeof(title) != typeof('str')){
;[task, ...args] = _args
title = null }
// normalize handler...
var run
var handler =
// queue...
// NOTE: queue is task-compatible...
task instanceof FinalizableQueue ?
task
// task protocol...
: task && task.then
&& (task.stop || task.send) ?
task
: this.__task_mixin__(
// interactive promise...
task instanceof Promise.interactive ?
task
// dumb promise -- will ignore all the messages...
// XXX should we complain about this???
: task instanceof Promise ?
Promise.interactive(
function(resolve, reject, onmsg){
// NOTE: since this is a promise, we can't
// stop it externally...
onmsg(function(msg){
msg == 'stop'
&& reject('stop') })
task.then(resolve, reject) })
// function...
: Promise.interactive(
function(resolve, reject, onmessage){
// NOTE: we need to start this a bit later hence
// we wrap this into run(..) and call it when
// the context is ready...
run = function(){
var res =
task(
that.__task_ticket__(title, resolve, reject, onmessage, handler),
...args)
// NOTE: if the client calls resolve(..) this
// second resolve(..) call has no effect,
// and the same is true with reject...
// XXX is double binding like this (i.e.
// ticket + .then()) a good idea???
res instanceof Promise
&& res.then(resolve, reject) } }))
// set handler title...
// NOTE: this will override the title of the handler if it was
// set before...
if(title){
handler.title
&& console.warn(
'TaskManager.Task(..): task title already defined:', handler.title,
'overwriting with:', title)
Object.assign(handler, {title}) }
this.push(handler)
this.record_times
&& (handler.time_started = Date.now())
// handle task manager state...
var cleanup = function(evt){
return function(res){
that.record_times
&& (handler.time_ended = Date.now())
that.splice(that.indexOf(handler), 1)
that.trigger(evt, task, res)
that.length == 0
&& that.trigger('tasksDone') } }
handler
.then(cleanup('done'), cleanup('error'))
// start...
var start = function(){
run ?
run()
: task.start ?
task.start()
: null }
// trigger task start...
sync_start ?
start()
: setTimeout(start, 0)
return handler },
}))
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

680
lib/types/test.js Executable file
View File

@ -0,0 +1,680 @@
#!/usr/bin/env node
/**********************************************************************
*
*
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var colors = require('colors')
var test = require('ig-test')
var object = require('ig-object')
var types = require('./main')
var promise = require('./Promise')
var containers = require('./containers')
var generator = require('./generator')
var events = require('./event')
var runner = require('./runner')
//---------------------------------------------------------------------
/*
var setups = test.Setups({
})
var modifiers = test.Modifiers({
})
var tests = test.Tests({
})
//*/
var cases = test.Cases({
// Object.js
//
Object: function(assert){
var o = Object.assign(
Object.create({
x: 111,
y: 222,
}), {
y: 333,
z: 444,
})
var oo = assert(Object.flatCopy(o), 'Object.flatCopy(..)')
assert(Object.match(oo, {x: 111, y: 333, z: 444}), 'Object.match(..)')
var k = ['z', 'x', 'y']
assert(Object.keys(Object.sort(oo, k)).cmp(k), 'Object.sort(,,)')
assert(Object.keys(Object.sort(oo)).cmp(k.slice().sort()), 'Object.sort(,,)')
var cmp = function(a, b){
return a == 'y' ?
1
: a == 'z' ?
-1
: 0 }
assert(Object.keys(Object.sort(oo, cmp)).cmp(k.slice().sort(cmp)), 'Object.sort(,,)')
},
// Array.js
// - flat (???)
// - includes (???)
// - first
// - last
// - compact
// - len
// - unique
// - tailUnique
// - cmp
// - setCmp
// - sortAs
// - mapChunks
// - filterChunks
// - reduceChunks
// - toKeys
// - toMap
Array: function(assert){
},
// Set.js
// - unite
// - intersect
// - subtract
// - sort
Set: function(assert){
},
// Map.js
// - sort
Map: function(assert){
},
Generator: function(assert){
},
String: function(assert){
assert(''.capitalize() == '')
assert('a'.capitalize() == 'A')
assert('abc'.capitalize() == 'Abc')
},
RegExp: function(assert){
},
IterablePromise: test.TestSet(function(){
var create = function(assert, value){
return {
input: value,
output: assert(Promise.iter(value), 'Promise.iter(', value, ')'),
} }
this.Setup({
empty: function(assert){
return create(assert, []) },
value: function(assert){
return create(assert, 123) },
array: function(assert){
return create(assert, [1, 2, 3]) },
nested_array: function(assert){
return create(assert, [1, 2, [3]]) },
promise_value: function(assert){
return create(assert, Promise.resolve(123)) },
promise_array: function(assert){
return create(assert, Promise.resolve([1, 2, 3])) },
promise_nested_array: function(assert){
return create(assert, Promise.resolve([1, 2, [3]])) },
array_mixed: function(assert){
return create(assert, [1, Promise.resolve(2), 3]) },
nested_array_mixed: function(assert){
return create(assert, [
1,
Promise.resolve(2),
[3],
Promise.resolve([4]),
]) },
promise_array_mixed: function(assert){
return create(assert, Promise.resolve([1, Promise.resolve(2), 3])) },
promise_nested_array_mixed: function(assert){
return create(assert, Promise.resolve([
1,
Promise.resolve(2),
[3],
Promise.resolve([4]),
])) },
})
this.Modifier({
nest: function(assert, setup){
setup.output = Promise.iter(setup.output)
return setup },
iter: function(assert, setup){
setup.output = setup.output.iter()
return setup },
map_asis: function(assert, setup){
setup.output = setup.output
.map(function(e){
return e })
return setup },
map_promise: function(assert, setup){
setup.output = setup.output
.map(function(e){
return Promise.resolve(e) })
return setup },
filter_all: function(assert, setup){
setup.output = setup.output
.filter(function(e){ return true })
return setup },
// XXX either the test is worng or something is broken...
filter_none: function(assert, setup){
return {
input: [],
output: setup.output
.filter(function(e){ return false }),
} },
/* XXX need tuning...
concat_basic: function(assert, {input, output}){
return {
input: [input].flat()
.concat(['a', 'b', 'c']),
output: output
.concat(['a', 'b', 'c']),
} },
concat_nested_array: function(assert, {input, output}){
return {
input: [input].flat()
.concat(['a', ['b'], 'c']),
output: output
.concat(['a', ['b'], 'c']),
} },
//*/
})
this.Test({
value: async function(assert, {input, output}){
var res = await output
assert(res instanceof Array, 'result is array')
input instanceof Array ?
// XXX this does not catch some errors -- map_promise specifically...
assert.array(res,
await Promise.all(input),
'array -> array')
: (input instanceof Promise && await input instanceof Array) ?
assert.array(res,
await input,
'promise array -> array')
: input instanceof Promise ?
assert.array(res,
[await input],
'promise value -> array')
: assert.array(res,
[input],
'value -> array') },
})
}),
Promise: async function(assert){
var p = assert(Promise.cooperative(), '.cooperative()')
assert(!p.isSet, 'promise unset')
var RESOLVE = 123
var then = false
var done = false
p.then(function(v){
then = assert(v == RESOLVE, '.then(..) handled') })
p.finally(function(){
done = assert(true, '.finally(..) handled') })
assert(!p.isSet, 'promise unset')
p.set(RESOLVE)
assert(p.isSet, 'promise set')
// allow the promise to finalize...
await p
assert(then, '.then(..): confirmed')
assert(done, '.done(..): confirmed')
// XXX
assert(await Promise.iter([1, Promise.resolve(2), [3]]), '.iter(..)')
assert.array(
await Promise.iter([1, 2, 3]),
[1, 2, 3],
'basic array')
assert.array(
await Promise.iter([1, Promise.resolve(2), 3]),
[1, 2, 3],
'promises as elements')
},
// Date.js
Date: function(assert){
var d = new Date()
var ts = assert(d.getTimeStamp(), '.getTimeStamp()')
var tsm = assert(d.getTimeStamp(true), '.getTimeStamp(true)')
var dd = assert(Date.fromTimeStamp(tsm), '.fromTimeStamp(..)')
var dds = assert(Date.fromTimeStamp(ts), '.fromTimeStamp(..)')
assert(d.toString() == dd.toString(), 'timestamp reversable')
assert(d.toShortDate() == dd.toShortDate(), '.toShortDate()')
assert(d.toShortDate() == dds.toShortDate(), '.toShortDate()')
assert(d.toShortDate(true) == dd.toShortDate(true), '.toShortDate(true)')
var a = Date.timeStamp()
var b = Date.timeStamp(true)
assert(a == Date.fromTimeStamp(a).getTimeStamp())
assert(a + '000' == Date.fromTimeStamp(a).getTimeStamp(true))
assert(b == Date.fromTimeStamp(b).getTimeStamp(true))
assert(Date.str2ms('1') == 1, 'Date.str2ms("1")')
assert(Date.str2ms(1) == 1, 'Date.str2ms(1)')
assert(Date.str2ms('1ms') == 1, 'Date.str2ms("1ms")')
assert(Date.str2ms('1s') == 1000, 'Date.str2ms("1s")')
assert(Date.str2ms('1m') == 60*1000, 'Date.str2ms("1m")')
assert(Date.str2ms('1h') == 60*60*1000, 'Date.str2ms("1h")')
assert(Date.str2ms('1d') == 24*60*60*1000, 'Date.str2ms("1d")')
assert(Date.str2ms('5 sec') == 5000, 'Date.str2ms("1 sec")')
assert(Date.str2ms('5 second') == 5000, 'Date.str2ms("1 second")')
assert(Date.str2ms('5 seconds') == 5000, 'Date.str2ms("1 seconds")')
assert(Date.str2ms('2hour') == 2*60*60*1000, 'Date.str2ms("1hour")')
assert(Date.str2ms('2 Hour') == 2*60*60*1000, 'Date.str2ms("1 Hour")')
assert(Date.str2ms('2 hours') == 2*60*60*1000, 'Date.str2ms("1 hours")')
assert(Number.isNaN(Date.str2ms('moo')), 'Date.str2ms("moo")')
assert(Number.isNaN(Date.str2ms('123 moo')), 'Date.str2ms("moo")')
},
// containers.js
// XXX move this out to a separate test set...
UniqueKeyMap: function(assert){
var a = assert(containers.UniqueKeyMap(), '')
var b = assert(containers.UniqueKeyMap([]), '')
var c = assert(containers.UniqueKeyMap([
['a', 111],
['b', 222],
['a', 333],
]), '')
assert(a.size == 0)
assert(b.size == 0)
assert(c.size == 3)
assert(c.get('a') == 111)
assert(c.has('a (1)'))
assert(c.get('a (1)') == 333)
var n
assert((n = c.set('a', 444, true)) == 'a (2)')
assert(c.get(n) == 444)
assert(c.reset(n, 555))
assert(c.get(n) == 555)
assert(c.delete('a (1)'))
assert(!c.has('a (1)'))
assert((n = c.set('a', 222, true)) == 'a (1)')
assert(c.keysOf(222).sort().cmp(['b', 'a'].sort()))
var k = [...c.keys()]
k[k.indexOf('a')] = 'b (1)'
assert((n = c.rename('a', 'b', true)) == 'b (1)')
// check order...
// XXX since we are using Array's .cmp(..) would be nice to be
// able to fail this test if that fails to test correctly...
// ...not sure how to do this though...
assert(k.cmp([...c.keys()]), '.rename(..) order')
},
})
//---------------------------------------------------------------------
var PromiseTests = test.TestSet()
test.Case('PromiseTests', PromiseTests)
PromiseTests.setups({
cooperative: function(assert){
return {
a: assert(Promise.cooperative(), '.cooperative()')
} },
})
PromiseTests.modifiers({
})
PromiseTests.tests({
})
//---------------------------------------------------------------------
// UniqueKeyMap testing...
var UniqueKeyMap = test.TestSet()
test.Case('UniqueKeyMap-new', UniqueKeyMap)
// XXX
UniqueKeyMap.setups({
empty: function(assert){
return {
value: assert(containers.UniqueKeyMap()),
// metadata...
size: 0,
}},
'empty-input': function(assert){
return {
value: assert(containers.UniqueKeyMap([])),
// metadata...
size: 0,
}},
// XXX non-empty input...
// XXX intersecting unput...
})
// XXX
UniqueKeyMap.modifiers({
set: function(assert, e, k='a', o){
o = o || {}
var n
var exists = e.value.has(k)
var expected = e.value.uniqieKey(k)
assert(n = e.value.set(k, o, true), '.set(..)')
// key update...
assert(n.startsWith(k), 'key prefix')
assert(n == expected, 'unexpected key')
exists
|| assert(n == k, 'key unexpectedly changed')
// size...
e.size += 1
assert(e.value.size == e.size, 'inc size')
return e },
reset: function(assert, e, k='a', o){
o = o || {}
var exists = e.value.has(k)
assert(e.value.reset(k, o))
assert(e.value.get(k) === o)
// size...
exists
|| (e.size += 1)
assert(e.value.size == e.size)
return e },
delete: function(assert, e, k='a'){
var exists = e.value.has(k)
assert(e.value.delete(k) == exists, '.delete(..)')
assert(!e.value.has(k), 'delete successful')
// size...
exists
&& (e.size -= 1)
assert(e.value.size == e.size)
return e },
'set-set': function(assert, e){
this.set(assert, e, 'x')
this.set(assert, e, 'y')
this.set(assert, e, 'x')
return e },
})
// XXX
UniqueKeyMap.tests({
consistent: function(assert, e){
assert(e.value.size == e.size, '.size')
assert([...e.value.keys()].length == e.value.size, '.keys() same size as .size')
return e }
})
//---------------------------------------------------------------------
// events...
var Events = test.TestSet()
test.Case('Events', Events)
// XXX test aborting handlers...
Events.cases({
base: function(assert){
var called = {}
// object with events...
var ObjWithEvents =
assert(
events.EventMixin('flat', {
// NOTE: we will also use virtual events later -- 'moo'
// and 'foo', these do not have to be defined to
// be usable...
// blank events...
bareEventBlank: assert(
events.Eventful('bareEventBlank'),
'.Eventful(..): blank'),
eventBlank: assert(
events.Event('eventBlank'),
'.Event(..): blank'),
// normal events...
bareEvent: assert(events.Eventful('bareEvent',
function(handle, ...args){
called['bareEvent-call'] = true
assert(handle(), '.Eventful(..) -> handle(..)')
return 'bareEvent'
}), '.Eventful(..)'),
event: assert(events.Event('event',
function(handle, ...args){
called['event-call'] = true
assert(handle(), '.Event(..) -> handle(..)')
}), '.Event(..)'),
}),
'object with event mixin created.')
// create an "instance"...
var obj = Object.create(ObjWithEvents)
// test event list...
assert.array(obj.events, ['event', 'eventBlank'], '.events')
assert.array(obj.eventful, ['bareEvent', 'bareEventBlank'], '.eventful')
// bind...
var bind = function(evt){
assert(obj.on(evt, function(evt, ...args){
called[evt +'-handle'] = true
}) === obj, 'bind: <obj-w-events>.on("'+ evt +'", ..)') }
;['moo',
...obj.events,
...obj.eventful]
.forEach(bind)
assert(obj.event(function(evt, ...args){
called['event-handle-2'] = true
}) === obj, 'bind: <obj-w-events>.event(<func>)')
// trigger
var trigger = function(evt, triggerd=true, handled=true){
var res = assert(obj.trigger(evt), 'trigger: <obj-w-events>.trigger("'+ evt +'")')
triggerd
&& !evt.endsWith('Blank')
&& assert(called[evt +'-call'], 'trigger: "'+ evt +'" event triggered')
handled
&& assert(called[evt +'-handle'], 'trigger: "'+ evt +'" event handled')
delete called[evt +'-call']
delete called[evt +'-handle']
return res }
var call = function(evt, triggered=true, handled=true){
var res = assert(obj[evt](), 'trigger: <obj-w-events>.'+ evt +'(..)')
triggered
&& !evt.endsWith('Blank')
&& assert(called[evt +'-call'], 'trigger: "'+ evt +'" event triggered')
handled
&& assert(called[evt +'-handle'], 'trigger: "'+ evt +'" event handled')
delete called[evt +'-call']
delete called[evt +'-handle']
return res }
trigger('foo', false, false)
trigger('moo', false)
obj.events
.forEach(function(e){
trigger(e)
call(e) })
assert(called['event-handle-2'], 'trigger: "event" event handled')
delete called['event-handle-2']
assert(call('event') === obj, '<obj-w-events>.event(..) return value.')
assert(call('bareEvent') == 'bareEvent', '<obj-w-events>.bareEvent(..) return value.')
// unbind: .one(..) / .off(..)
// XXX this is triggered twice for some reason...
obj.one('event', function(){
called['event-one-time-handler'] =
(called['event-one-time-handler'] || 0) + 1 })
obj
.event()
//.event()
//.event()
assert(called['event-one-time-handler'] == 1, '.one("event", ..) handler cleared.')
delete called['event-one-time-handler']
// special case...
obj.trigger('event', function(){ called['trigger-function-called'] = true })
assert(called['trigger-function-called'] === undefined, '.trigger(..) should not call it\'s args')
// XXX test passing args...
// XXX test different mode events...
// re-bind...
},
})
//---------------------------------------------------------------------
// events...
var Runner = test.TestSet()
test.Case('Runner', Runner)
// XXX test aborting handlers...
Runner.cases({
base: function(assert){
//var q = assert(runner.Queue({auto_stop: true}), 'Queue()')
var q = assert(runner.Queue(), 'Queue()')
// empty states...
assert(q.state == 'stopped', '.state: stopped')
q.start()
assert(q.state == 'running', '.state: running')
q.start()
assert(q.state == 'running', '.state: running')
q.stop()
assert(q.state == 'stopped', '.state: stopped')
var tasks_run = []
var a = function(){ tasks_run.push('a') }
var b = function(){ tasks_run.push('b') }
var c = function(){ tasks_run.push('c') }
q.push(a)
q.push(b)
q.push(c)
assert(q.length == 3, '.length is 3')
q.runTask()
q.runTask()
q.runTask()
assert.array(tasks_run, ['a', 'b', 'c'], 'all tasks run')
// XXX need to figure out how to test async...
tasks_run = []
var q = assert(runner.Queue({sync_start: true}), 'Queue({sync_start: true})')
q.push(a)
q.push(b)
q.push(c)
q.start()
assert.array(tasks_run, ['a', 'b', 'c'], 'all tasks run')
//console.log('\n>>>', q.state)
},
})
//---------------------------------------------------------------------
typeof(__filename) != 'undefined'
&& __filename == (require.main || {}).filename
&& test.run()
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

View File

@ -12,7 +12,8 @@
"jszip": "*",
"pouchdb": "^7.3.0",
"pouchdb-browser": "^7.3.0",
"requirejs": "*"
"requirejs": "*",
"showdown": "^2.1.0"
},
"disabled-dependencies": {
"peer": "*",

View File

@ -482,6 +482,7 @@ module.PAGE_NOT_FOUND = '404: PAGE NOT FOUND: $PATH'
// XXX PATH_VARS need to handle path variables...
// XXX filters (and macros?) should be features for simpler plugin handlng (???)
// XXX STUB filters...
// XXX rename to pWikiPage???
var Page =
module.Page =
object.Constructor('Page', BasePage, {
@ -526,6 +527,9 @@ object.Constructor('Page', BasePage, {
markdown: markdown.markdown,
'quote-markdown': markdown.quoteMarkdown,
text: function(source){
return `<pre>${source}</pre>` },
},
//
@ -1054,6 +1058,8 @@ var System =
module.System = {
// base templates...
//
// XXX revise this...
// ...need to be able to do: /some/path/_text
_text: {
text: '<macro src="." join="\n">@source(.)</macro>' },
NotFound: {

View File

@ -27,38 +27,95 @@
<style>
</style>
<script src="ext-lib/pouchdb.js"></script>
<!-- XXX do we need this??? -->
<script src="bootstrap.js"></script>
<!--script data-main="pwiki2" src="ext-lib/require.js"></script-->
<script src="ext-lib/require.js"></script>
<script>
var makeFallbacks =
function(paths, search=['lib']){
return Object.entries(paths)
.map(function([key, path]){
// package...
if(path.endsWith('/')){
return [key, path] }
return [
key,
[
path,
...search
.map(function(base){
return base +'/'+ path.split(/[\\\/]+/g).pop() }),
]
] })
.reduce(function(res, [key, value]){
res[key] = value
return res }, {}) }
require.config({
paths: {
...makeFallbacks({
'ig-doc': 'node_modules/ig-doc/doc',
'ig-stoppable': 'node_modules/ig-stoppable/stoppable',
'ig-object': 'node_modules/ig-object/object',
'ig-types': 'node_modules/ig-types/',
'object-run': 'node_modules/object-run/run',
'ig-object': 'node_modules/ig-object/object',
}),
'pouchdb': 'node_modules/pouchdb-browser/lib/index',
// packages...
'ig-types': [
'node_modules/ig-types',
'lib/types',
],
// external stuff...
...makeFallbacks({
'jszip': 'node_modules/jszip/dist/jszip',
'pouchdb': 'node_modules/pouchdb/dist/pouchdb',
'showdown': 'node_modules/showdown/dist/showdown',
}, ['ext-lib']),
},
packages: [
'ig-types',
]
})
var setup = function(){
require(['./browser'], function(m){
// start loading pWiki...
require(['./browser'], function(m){
window.pwiki = m.pwiki
})
}
// handle location.hash (both directions)
var _debounceHashChange = false
pwiki.onNavigate(async function(){
// update location.hash without retriggering...
_debounceHashChange = true
location.hash = this.path
setTimeout(function(){
_debounceHashChange = false }, 0)
// render...
document.querySelector('#pWiki').innerHTML = await this.text })
window.addEventListener('hashchange', function(evt){
evt.preventDefault()
if(_debounceHashChange){
return }
var [path, hash] = location.hash.slice(1).split('#')
path = path.trim() == '' ?
'/'
: path
pwiki.path = path })
// show current page...
pwiki.path = location.hash.slice(1)
})
</script>
<body onload="setup()">
<body>
pWiki2
<div class="wiki" />
<div id="pWiki" />
</body>
</html>

View File

@ -1,17 +1,28 @@
/**********************************************************************
*
*
* XXX wikiword filter seems to hang on /
* XXX do filters affect the whole page or only what comes after???
* XXX BUG: need to be able to affect the default render wrpaper...
* i.e.: /some/path vs. /some/path/_text vs. /some/path/_raw
*
*
* XXX ROADMAP:
* - run in browser
* - basics, loading -- DONE
* - test localStorage / sessionStorage -- DONE
* - test pouch -- DONE
* - render page
* - render page -- DONE
* - navigation
* - hash/anchor
* - hash/anchor -- DONE
* - service worker
* ...handle relative urls (???)
* - migrate bootstrap
* - test pwa
* - store topology
* - WikiWord
* - markdown -- DONE
* - service worker
* - pwa
* - archive old code
* - update docs
* - refactor and cleanup
@ -59,6 +70,7 @@
* TODO:
* - <page>.then() -- resolve when all pending write operations done ???
* - an async REPL???
* - custom element???
*
*
*

View File

@ -13,12 +13,7 @@ var types = require('ig-types')
var pwpath = require('../lib/path')
var base = require('../store/base')
// XXX HACK: trick requirejs to delay module loading...
var req = require
module.PouchDB =
typeof(PouchDB) != 'undefined' ?
PouchDB
: req('pouchdb')
var pouchdb = require('pouchdb')
//---------------------------------------------------------------------
@ -35,7 +30,7 @@ module.PouchDBStore = {
__data: undefined,
get data(){
return this.__data
?? (this.__data = new module.PouchDB(this.__path__)) },
?? (this.__data = new pouchdb.PouchDB(this.__path__)) },
set data(value){
this.__data = value },