mirror of
https://github.com/flynx/pWiki.git
synced 2025-10-28 09:30:07 +00:00
lots of tweaks and fixes...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
parent
7ce3e6f8bc
commit
e7a9610d81
15
Makefile
15
Makefile
@ -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:
|
||||
|
||||
@ -52,6 +52,10 @@ pwiki.store.update('@pouch', {
|
||||
})
|
||||
|
||||
|
||||
// XXX
|
||||
typeof(Bootstrap) != 'undefined'
|
||||
&& pwiki.store.load(Bootstrap)
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
|
||||
11577
ext-lib/jszip.js
Executable file
11577
ext-lib/jszip.js
Executable file
File diff suppressed because it is too large
Load Diff
14
ext-lib/jszip.min.js
vendored
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
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
1
ext-lib/showdown.js.map
Executable file
File diff suppressed because one or more lines are too long
5
ext-lib/showdown.min.js
vendored
5
ext-lib/showdown.min.js
vendored
File diff suppressed because one or more lines are too long
1
ext-lib/showdown.min.js.map
Executable file
1
ext-lib/showdown.min.js.map
Executable file
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
||||
@ -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){
|
||||
|
||||
@ -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
546
lib/types/Array.js
Executable 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
147
lib/types/Date.js
Executable 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
18
lib/types/Function.js
Executable 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
70
lib/types/Map.js
Executable 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
101
lib/types/Object.js
Executable 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
798
lib/types/Promise.js
Executable 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
78
lib/types/RegExp.js
Executable 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
122
lib/types/Set.js
Executable 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
43
lib/types/String.js
Executable 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
19
lib/types/_module.js
Executable 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
211
lib/types/containers.js
Executable 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
382
lib/types/event.js
Executable 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
622
lib/types/generator.js
Executable 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
45
lib/types/main.js
Executable 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
975
lib/types/runner.js
Executable 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
680
lib/types/test.js
Executable 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 })
|
||||
@ -12,7 +12,8 @@
|
||||
"jszip": "*",
|
||||
"pouchdb": "^7.3.0",
|
||||
"pouchdb-browser": "^7.3.0",
|
||||
"requirejs": "*"
|
||||
"requirejs": "*",
|
||||
"showdown": "^2.1.0"
|
||||
},
|
||||
"disabled-dependencies": {
|
||||
"peer": "*",
|
||||
|
||||
6
page.js
6
page.js
@ -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: {
|
||||
|
||||
81
pwiki2.html
81
pwiki2.html
@ -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>
|
||||
|
||||
18
pwiki2.js
18
pwiki2.js
@ -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???
|
||||
*
|
||||
*
|
||||
*
|
||||
|
||||
@ -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 },
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user