koru
Main koru module. Responsible for:
- Thread local storage
- Logging
- Dependency tracking and load/unload manager
- AppDir location
Methods
koru.buildPath(path)
Converts path to related build path of compiled resource.
Example
const koru = require("koru/main");koru.buildPath("models/library/book");// returns "models/library/.build/book"koru.buildPath("helper");// returns ".build/helper"
koru.onunload(moduleOrId, callback)
A wrapper around module#onUnload
.
Example
const koru = require("koru/main");const myModule = {id: 'myModule', onUnload: stub()}; const callback = {stop() {}}; koru.onunload(myModule, callback); assert.calledWith(myModule.onUnload, callback);
BTree
A Balanced Tree. Implemented using a Red-black tree.
Methods
constructor(compare=simpleCompare, unique=false)
Create an instance of BTree.
Parameters
[compare] | function | Function to test order of two keys (like
|
[unique] | boolean | if |
Example
const BTree = require("koru/btree");const simple = new BTree(); simple.add(5); simple.add(5); simple.add(1); assert.equals(Array.from(simple), [1, 5, 5]); const compCompare = (a, b) => { if (a === b) return 0; const ans = a.k1 - b.k1; return ans == 0 ? a.k2 - b.k2 : ans; }; const composite = new BTree(compCompare, true); composite.add({k1: 4, k2: 5, name: '45'}); composite.add({k1: 4, k2: 3, name: '43'}); assert.same( composite.add({k1: 4, k2: 3, name: 'rep'}).value.name, '43'); assert.equals(Array.from(composite).map((v) => v.name), ['43', '45']);
BTree#*[Symbol.iterator]()
iterate the tree in order
Parameters
Returns | Function |
Example
const BTree = require("koru/btree");const tree = new BTree(); insertNodes(tree, [123, 456]); assert.equals(Array.from(tree), [123, 456]); assert.equals(Array.from(tree), [123, 456]); const i = tree[Symbol.iterator](); tree.add(53); assert.equals(i.next().value, 53); assert.equals(Array.from(tree), [53, 123, 456]);
BTree#clear()
Remove all entries
Example
const BTree = require("koru/btree");const tree = new BTree(); tree.add(1); tree.add(2); tree.add(3); tree.clear(); assert.same(tree.firstNode, null); assert.same(tree.size, 0);
BTree#find(value)
Find a value in the tree.
Parameters
value | number | find entry with same keys as |
Returns | undefined / object |
|
Example
const BTree = require("koru/btree");const tree = new BTree(); insertNodes(tree, [100, 200, 50, 150, 250]); assert.same(tree.find(50), 50); assert.same(tree.find(49), undefined); assert.same(tree.find(150), 150);
BTree#findNode(value)
Find a node
in the tree that matches value
. Be careful with the node; don't insert it
into another tree or change its keys; call deleteNode first.
Parameters
value | number | contains the keys to find. |
Returns | undefined / object |
|
Example
const BTree = require("koru/btree");const tree = new BTree(); insertNodes(tree, [100, 200, 50, 150, 250]); assert.same(tree.findNode(50).value, 50); assert.same(tree.findNode(49), undefined); assert.same(tree.findNode(150).value, 150);
BTree#nodeFrom(value)
find node equal or greater than value
Example
const BTree = require("koru/btree");tree = new BTree(); insertNodes(tree, [100, 50, 20, 110, 120, 130, 95]); assert.same(tree.nodeFrom(35).value, 50); assert.same(tree.nodeFrom(95).value, 95); assert.same(tree.nodeFrom(5).value, 20); assert.same(tree.nodeFrom(200), null);
BTree#nodeTo(value)
find node equal or less than value
Example
const BTree = require("koru/btree");tree = new BTree(); insertNodes(tree, [100, 50, 20, 110, 120, 130, 95]); const n130 = tree.lastNode; assert.same(n130.value, 130); assert.same(tree.nodeTo(51).value, 50); assert.same(tree.nodeTo(200), n130); assert.same(tree.nodeTo(10), null); assert.same(tree.nodeTo(95).value, 95); assert.same(tree.nodeTo(105).value, 100);
Changes
Methods
Changes.applyAll(attrs, changes)
Apply all commands to an attributes object. Commands can have:
- a $match object which assert the supplied fields match the attribute
- a $partial object which calls applyPartial for each field; OR
- be a top level replacement value
Parameters
attrs | object | |
changes | object | |
Returns | object |
|
Example
const Changes = require("koru/changes");// matches top level const attrs = {foo: 1, bar: 'two'}; const changes = {$match: {foo: 1, bar: {md5: 'b8a9'}}}; refute.exception(() => {Changes.applyAll(attrs, changes)}); assert.equals(attrs, {foo: 1, bar: 'two'}); assert.equals(changes, {$match: {foo: 1, bar: {md5: 'b8a9'}}});// bad checksum const attrs = {foo: 1, bar: {md5: 'bad'}}; const changes = {$match: {foo: 1, bar: 'two'}}; assert.exception( () => {Changes.applyAll(attrs, changes)}, {error: 409, reason: {bar: 'not_match'}}, );// can apply undo const attrs = { foo: 1, bar: 2, baz: {bif: [1, 2, {bob: 'text'}]}, simple: [123], }; const changes = { foo: 2, $partial: { bar: ['$replace', 4], simple: 456, baz: [ 'bif.2.$partial', [ 'bip', 'new', 'bob.$partial', [ '$append', ' appended', ], ], ], }, }; const undo = Changes.applyAll(attrs, changes);assert.same(Changes.original(undo), changes); assert.equals(attrs, { foo: 2, bar: 4, baz: { bif: [1, 2, {bob: 'text appended', bip: 'new'}], }, simple: 456, }); assert.equals(undo, { foo: 1, simple: [123], $partial: { bar: ['$replace', 2], baz: [ 'bif.2.$partial', [ 'bob.$partial', [ '$patch', [-9, 9, null], ], 'bip', null, ], ], }, }); Changes.applyAll(attrs, undo); assert.equals(attrs, { foo: 1, bar: 2, baz: {bif: [1, 2, {bob: 'text'}]}, simple: [123], });const ary = [1, 2, 3], aa = {aa: 1, ary}; const b = [1, 2]; const orig = {a: aa, b, c: 3, nest: {foo: 'foo'}}; const changes = { a: {aa: 1, ab: 2, ary: [1, 4, 5, 6, 3]}, b: [1, 2], c: 4, nest: {foo: 'foo'}}; assert.equals(Changes.applyAll(orig, changes), {$partial: { a: ['ary.$partial', ['$patch', [1, 3, [2]]], 'ab', null]}, c: 3}); assert.equals(aa, {aa: 1, ab: 2, ary}); assert.equals(ary, [1, 4, 5, 6, 3]); assert.same(orig.a, aa);
Changes.applyOne(attrs, key, changes)
Apply only one attribute from changes
Example
const Changes = require("koru/changes");const attrs = {bar: 1, foo: 2, fuz: 3, fiz: 4}; const changes = {foo: null, fuz: undefined, fiz: 5, nit: 6}; Changes.applyOne(attrs, 'foo', changes); Changes.applyOne(attrs, 'fuz', changes); Changes.applyOne(attrs, 'fiz', changes); Changes.applyOne(attrs, 'nit', changes); assert.equals(attrs, {bar: 1, fiz: 5, nit: 6}); assert.equals(changes, {foo: 2, fuz: 3, fiz: 4, nit: m.null});
Changes.fieldDiff(field, from, to)
Example
const Changes = require("koru/changes");Changes.fieldDiff("foo", {_id: "t123"}, {fuz: "123"});Changes.fieldDiff("foo", {_id: "t123", foo: {one: 123, three: true, two: 'a string'}}, {foo: {one: 123, three: true, two: 'a string'}});
Changes.has(changes, field)
test if undo has changed field
Example
const Changes = require("koru/changes");Changes.has({foo: undefined}, "foo");// returns trueChanges.has({foo: false}, "foo");// returns trueChanges.has({foo: undefined}, "bar");// returns falseChanges.has({$partial: {foo: undefined}}, "foo");// returns trueChanges.has({$partial: {foo: undefined}}, "bar");// returns false
Changes.merge(to, from)
Merge one set of changes into another.
Parameters
to | object | the destination for the merges |
from | object | the source of the merges. Values may be assignment (not copied) to |
Returns | object |
|
Example
const Changes = require("koru/changes");const current = {title: 'The Bone', author: 'Kery Hulme', genre: ['Romance', 'Mystery']}; assert.same(Changes.merge(current, {$partial: { title: ['$append', ' People'], author: ['$patch', [3, 1, 'i']], genre: ['$remove', ['Romance'], '$add', ['Drama']], }}), current); assert.equals(current, { title: 'The Bone People', author: 'Keri Hulme', genre: ['Mystery', 'Drama'], });const current = {$partial: { title: ['$append', 'The Bone'], author: ['$prepend', 'Kery'], }}; Changes.merge(current, {$partial: { title: ['$append', ' People'], author: ['$patch', [3, 1, 'i']], genre: ['$add', ['Mystery']], }}); assert.equals(current, {$partial: { title: ['$append', 'The Bone', '$append', ' People'], author: ['$prepend', 'Kery', '$patch', [3, 1, 'i']], genre: ['$add', ['Mystery']], }});const current = {$partial: { title: ['$append', 'The Bone'], author: ['$prepend', 'Kery'], }}; Changes.merge(current, { title: 'The Bone People', genre: ['Mystery'], }); assert.equals(current, { title: 'The Bone People', genre: ['Mystery'], $partial: {author: ['$prepend', 'Kery']}});
Changes.nestedDiff(from, to, depth=0)
Create diff in partial format to the specified depth.
Parameters
from | object | |
to | object | |
[depth] | number | How deep to recurse subfields defaults to 0 |
Returns | Array | diff in partial format |
Example
const Changes = require("koru/changes");const was = {level1: {level2: {iLike: 'I like three', numbers: [2, 9, 11]}}}; const now = deepCopy(was); now.level1.level2.iLike = 'I like the rimu tree'; now.level1.level2.iAlsoLike = 'Tuis'; let changes = Changes.nestedDiff(was, now, 5); /** depth 5 **/ assert.equals(changes, [ 'level1.$partial', [ 'level2.$partial', [ 'iLike.$partial', ['$patch', [9, 0, 'e rimu t']], 'iAlsoLike', 'Tuis', ]]]); const wasCopy = deepCopy(was); Changes.applyAll({likes: wasCopy}, {$partial: {likes: changes}}); assert.equals(wasCopy, now); /** depth 2 **/ assert.equals(Changes.nestedDiff(was, now, 2), [ 'level1.$partial', ['level2.$partial', [ 'iLike', 'I like the rimu tree', 'iAlsoLike', 'Tuis', ]]]); /** depth 0 **/ assert.equals(Changes.nestedDiff(was, now), [ 'level1', {level2: { iLike: 'I like the rimu tree', iAlsoLike: 'Tuis', numbers: [2, 9, 11]}}]); { /** arrays and multiple entries **/ const now = deepCopy(was); now.level1.level2.numbers = [2, 3, 5, 7, 11]; now.level1.level2a = 'Another branch'; assert.equals(Changes.nestedDiff(was, now, 5), [ 'level1.$partial', [ 'level2.$partial', [ 'numbers.$partial', ['$patch', [1, 1, [3, 5, 7]]], ], 'level2a', 'Another branch', ]]); }
Changes.original(undo)
Reteries the original set of changes from an undo
. See applyAll
Example
const Changes = require("koru/changes");Changes.original({foo: 123});// returns {foo: 456}
MockCacheStorage
Methods
constructor()
Create an instance suitable for replacing window.caches
Example
const MockCacheStorage = require("koru/client/mock-cache-storage");const caches = new MockCacheStorage; refute(await caches.match(new Request('/index.js')));
Uncache
Uncache is used for development to uncache fixed assets or the service-worker and reload app if they are modified.
Methods
Uncache.start(assets={ '.build/index.html': '/', 'manifest.json': '/manifest.json', 'index.css': '/index.css', })
Start tracking fixed asset changes and service-worker.
Parameters
[assets] | object |
Example
const Uncache = require("koru/client/uncache");Uncache.start(); await cache.put(new Request('/'), {status: 200, body: 'code...'}); await koru.unload('i_do_not_exist'); refute.called(koru.reload); assert(await caches.match('/')); await koru.unload('.build/index.html'); // simulate server sending unload to client refute(await caches.match('/')); assert.called(koru.reload); Uncache.stop();Uncache.start({'public/my.css': '/public/my.css'}); await cache.put(new Request('/public/my.css'), {status: 200, body: 'css...'}); await koru.unload('public/my.css'); // simulate server sending unload to client refute(await caches.match('/public/my.css')); Uncache.stop();// service-worker Uncache.start(); stub(koru, 'unregisterServiceWorker'); await koru.unload('service-worker'); assert.called(koru.unregisterServiceWorker); await koru.unload('sw'); // this works as well assert.calledTwice(koru.unregisterServiceWorker);
AccSha256
This is useful for building a hash in small increments; it is not the same as hashing the entire string in one go.
Methods
AccSha256.add( text, hash = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19], )
Modify hash
using text
.
Parameters
text | string | The message to add to |
[hash] | Array | should be an array of 8 32bit integers. The default is the standard sha256 initial hash values. |
Returns | Array | the modified |
Example
const AccSha256 = require("koru/crypto/acc-sha256");const h = [1, 2, 3, 4, 5, 6, 7, 8]; assert.same(sut.add('hello world', h), h); assert.equals(h, [4138495084, 3973010320, 2777164054, 2207796612, 615005229, 3241153105, 1397076350, 2212452408]); assert.equals(sut.add('secret'), [ 733482323, 2065540067, 2345861985, 2860865158, 3185633997, 1902313206, 2724194683, 4113015387, ]);
AccSha256.toHex(hash)
Convert a hash
to a hex string.
Parameters
hash | Array | an array of 32bit integers. Usually produced from add. |
Returns | string | the hex equivalent of the |
Example
const AccSha256 = require("koru/crypto/acc-sha256");assert.same( sut.toHex([65535, 1, 15, 256, 0xffffffff, 10, 0, 0]), '0000ffff000000010000000f00000100ffffffff0000000a0000000000000000', ); const h = [1, 2, 3, 4, 5, 6, 7, 8]; sut.add('hello world', h); assert.same(sut.toHex(h), 'f6ac6c6ceccf5390a588291683984d8424a83c2dc13012515345b17e83df5838'); assert.same( sut.toHex(sut.add('\na bit more text\n\u{1}÷\0\n\n\n', h)), '4cb301ea6c1e975ad8130be3e660b5a9ccf9e28e514a73c63533f2690c866255', );
AccSha256.toId(text, hash)
Convert a string into an id hash
Parameters
text | string / Uint8Array / Uint32Array | |
[hash] | ||
Returns | string |
Example
const AccSha256 = require("koru/crypto/acc-sha256");assert.same(sut.toId('hello' + 'goodbye'), 'hef112kz6HMarjX36'); assert.same(sut.toId(''), '5aQFks5seW4uAZNtG'); assert.same(sut.toId('1'), 'RSaJD5Q8g5Jxp2s8M'); assert.same(sut.toId('hello'), '1fUIeDQxGXKCyEZbu');
CssLoader
css/loader allows dynamic replacement of css and less files when their contents change.
Methods
constructor(session)
Construct a css loader
Parameters
session | object | listen for load messages from this session |
Example
const CssLoader = require("koru/css/loader");new CssLoader(SessionBase(loader));
CssLoader#loadAll(dir)
Load all css and less files under dir
Parameters
dir | string |
Example
const CssLoader = require("koru/css/loader");const cssLoader = new CssLoader();cssLoader.loadAll("koru/css");
DirWatcher
Methods
constructor(dir, callback, callOnInit=false)
Watch recursively for file and directory changes
Parameters
dir | string | the top level directory to watch |
callback | function | called with the path that changed and a |
callOnInit | boolean | if true run the callback for every existing path found. unlinked. |
Example
const DirWatcher = require("koru/dir-watcher");const callback = stub((pathname, st) => { if (path.basename(pathname) === 'f1') { future.resolve(); } }); after(new DirWatcher(liveDir, callback, true)); await future.promise; assert.calledTwice(callback); assert.calledWith(callback, m(/^\/.*\.build\/test-watch-dir\/live\/d1$/), m((st) => st.isDirectory())); assert.calledWith(callback, m(/test-watch-dir.*f1$/), m((st) => ! st.isDirectory()));
DLinkedList
A double linked list. The list is iterable. Nodes can be deleted without needing this list.
Properties
#head | null / object | Retrieve the node that is the head of the list |
#tail | null / object | Retrieve the node that is the tail of the list |
Methods
constructor(listEmpty)
Make an instance of DLinkedList
Parameters
[listEmpty] | function | method will be called when the list becomes empty. |
Example
const DLinkedList = require("koru/dlinked-list");const listEmpty = stub(); const subject = new DLinkedList(listEmpty); const node1 = subject.add('value1'); const node2 = subject.add('value2'); node1.delete(); refute.called(listEmpty); node2.delete(); assert.called(listEmpty);
DLinkedList#add(value=null)
add value
to the end of the list.
Parameters
[value] | any-type | to add to the list |
Returns | object | a node that has the methods
|
Example
const DLinkedList = require("koru/dlinked-list");const subject = new DLinkedList(); const node1 = subject.add(1); const node2 = subject.add(2); assert.same(node1.value, 1); assert.same(node1, subject.head); assert.same(node2, subject.tail); assert.equals(Array.from(subject), [1, 2]); node1.delete(); assert.equals(Array.from(subject), [2]); subject.add(); assert.equals(Array.from(subject), [2, null]); node2.value = void 0; // undefined is coerced to null assert.equals(Array.from(subject), [null, null]);
DLinkedList#addFront(value=null)
add value
to the front of the list.
Parameters
[value] | any-type | to add to the list |
Returns | object | a node that has the methods
|
Example
const DLinkedList = require("koru/dlinked-list");const subject = new DLinkedList(); const node1 = subject.add(1); const node2 = subject.addFront(2); assert.same(node1.value, 1); assert.same(node1, subject.tail); assert.same(node2, subject.head); assert.equals(Array.from(subject), [2, 1]); node1.delete(); assert.equals(Array.from(subject), [2]); subject.addFront(); assert.equals(Array.from(subject), [null, 2]);
DLinkedList#clear()
clear all entries. (calls listEmpty
if present)
Example
const DLinkedList = require("koru/dlinked-list");const subject = new DLinkedList(); subject.add(1); subject.add(2); subject.clear(); assert.equals(Array.from(subject), []);
DLinkedList#forEach(callback)
visit each entry
Parameters
callback | function |
Example
const DLinkedList = require("koru/dlinked-list");const subject = new DLinkedList(); subject.add('a'); subject.add('b'); const ans = []; subject.forEach((v) => {ans.push(v)}); assert.equals(ans, ['a', 'b']);
DLinkedList#*nodes()
Return an iterator over the nodes added to the list. (add returns the node
)
Parameters
Returns | Function |
Example
const DLinkedList = require("koru/dlinked-list");const subject = new DLinkedList(); const f = () => {}; const exp = [ m.is(subject.add(1)), m.is(subject.add(f)), ]; subject.add('a'); const ans = []; for (const h of subject.nodes()) { ans.push(h); if (h.value === f) break; } assert.equals(ans, exp);
DLinkedList#*values()
Return an iterator over the values added to the list.
Parameters
Returns | Function |
Example
const DLinkedList = require("koru/dlinked-list");const subject = new DLinkedList(); const f = () => {}; subject.add(1), subject.add(f), subject.add('a'); const ans = []; for (const v of subject.values()) { ans.push(v); if (v === f) break; } assert.equals(ans, [1, m.is(f)]);
Dom
Utilities for interacting with the Document Object Model
Methods
Dom.closestChildOf(elm=null, parent)
Search up parent
's child which is elm
or a ancestor of elm
.
Parameters
elm | Node / Element | the node to start the search from |
parent | Element | the parent of the node being looked for. |
Returns | Element / null |
|
Example
const Dom = require("koru/dom");const child = document.createTextNode('child'), otherChild = Dom.h({}); const topChild = Dom.h({div: {div: child}}); const parent = Dom.h({div: topChild}); const tree = Dom.h({div: [parent, {div: {div: otherChild}}]}); assert.same(Dom.closestChildOf(child, parent), topChild); assert.same(Dom.closestChildOf(otherChild, parent), null); assert.same(Dom.closestChildOf(topChild, parent), topChild);
Dom.endMarker(startMarker)
Return the end marker for a start marker.
Example
const Dom = require("koru/dom");const parent = Dom.h({}); const startMarker = Dom.insertStartEndMarkers(parent); assert.same(Dom.endMarker(startMarker), startMarker.nextSibling); assert.same(Dom.endMarker(startMarker).nodeType, document.COMMENT_NODE); assert.same(Dom.endMarker(startMarker).data, 'end');
Dom.ensureInView(elm)
Ensure that elm
is as visible as possible with minimal scrolling.
Example
const Dom = require("koru/dom");// horizontal scroll const block = (text) => Dom.h({ style: 'flex:0 0 50px', div: [text]}); const divs = 'one two three four five'.split(' ').map((t) => block(t)); const container = Dom.h({ style: 'margin:30px;width:75px;height:30px;overflow:scroll;', div: { style: 'display:flex;width:300px', div: divs, }, }); document.body.appendChild(container); Dom.ensureInView(divs[1]); assert.near(container.scrollLeft, 39, 2); Dom.ensureInView(divs[0]); assert.equals(container.scrollLeft, 0, 2);// vertical scroll const block = (text) => Dom.h({style: 'width:50px;height:20px', div: [text]}); const divs = 'one two three four five'.split(' ').map((t) => block(t)); const container = Dom.h({ style: 'margin:30px;width:150px;height:30px;overflow:scroll', div: divs, }); document.body.appendChild(container); Dom.ensureInView(divs[1]); assert.near(container.scrollTop, 20); Dom.ensureInView(divs[0]); assert.equals(container.scrollTop, 0);
Dom.getBoundingClientRect(object)
Get the
boundingClientRect
for different types of objects namely: Range
and Element
. For a collapsed range the result
is calculated around caret position otherwise the object's getBoundingClientRect
is
used. Also if an existing client rect can be passed it will be returned.
Parameters
object | Element / object | The object to calculate for. |
Returns | Object | the rect parameters contains |
Example
const Dom = require("koru/dom");const div = Dom.h({ style: 'position:absolute;left:25px;top:50px;width:150px;height:80px;' + 'font-size:16px;font-family:monospace', contenteditable: true, div: ['Hello ', 'world', {br: ''}, {br: ''}, '']}); document.body.appendChild(div); const keys = 'left top width height'.split(' '); const rect = util.extractKeys(div.getBoundingClientRect(), keys); // an Element assert.equals(util.extractKeys(Dom.getBoundingClientRect(div), keys), rect); // a selection let range = document.createRange(); range.selectNode(div); assert.near(util.extractKeys(Dom.getBoundingClientRect(range), keys), rect); // a position range.setStart(div.firstChild, 4); range.collapse(true); assert.near(util.extractKeys(Dom.getBoundingClientRect(range), keys), { left: 64, top: 50, width: 0, height: 19}, 2); // a client rect assert.same(Dom.getBoundingClientRect(rect), rect);
Dom.getClosest(elm=null, selector)
search up for element that matches selector
Parameters
elm | Element / Node | the element to start from. Text elements start from their parent node. |
selector | string | |
Returns | Element |
Example
const Dom = require("koru/dom");const div = Dom.h({}); div.innerHTML = ` <div class="foo"> <div class="bar"> <button type="button" id="sp">text</button> </div> </div>`; document.body.appendChild(div); const button = document.getElementById('sp'); const foobar = Dom('.foo>.bar'); assert.same(Dom.getClosest(button, '.foo>.bar'), foobar); assert.same(Dom.getClosest(foobar, '.foo>.bar'), foobar); assert.same(Dom.getClosest(button.firstChild, '.foo'), foobar.parentNode);
Dom.getClosestCtx(elm, selector)
search up for element that matches selector
Parameters
elm | Element | the element to start from. Text elements start from their parent node. |
selector | string | |
Returns | Ctx |
Example
const Dom = require("koru/dom");Dom.getClosestCtx(Node`<button type="button" id="sp">text</button>`, ".foo>.bar");// returns {attrEvals: [], evals: [], firstElement: Node`<div class="bar"> <button type="button" id="sp">text</button> </div>`, parentCtx: Ctx({template: nu}Dom.getClosestCtx(Node`<button type="button" id="sp">text</button>`, ".foo");// returns {attrEvals: [], evals: [], firstElement: Node`<div> <div class="foo"> <div class="bar"> <button type="button" id="sp">text</button> </div> </di}
Dom.insertStartEndMarkers(parent, before=null)
Insert a pair of comments into parent
that can be used for start and end markers.
Parameters
parent | Element | the parent node to insert comments into |
[before] | Element | the node to insert the comments before. Comments are appended if before is missing or null. |
Returns | Node | the start comment. end comment can be found by calling endMarker |
Example
const Dom = require("koru/dom");const footer = Dom.h({footer: 'footer'}); const parent = Dom.h({div: footer}); const startMarker = Dom.insertStartEndMarkers(parent, footer);
Dom.isAboveBottom(elm, region)
Determine if an element is above the bottom of a region.
Parameters
elm | Element | |
region | html-doc::Element / object | either a
Dom |
Returns | boolean |
Example
const Dom = require("koru/dom");Dom.isAboveBottom(Node`<div style="position:absolute;left:-12px;width:20px;height:30px">x</div>`, Node`<body><div style="position:absolute;left:-12px;width:20px;height:30px">x</div></body>`);// returns trueDom.isAboveBottom(Node`<div style="position: absolute; left: -12px; width: 20px; height: 30px; bottom: -9px;">x</div>`, Node`<body><div style="position: absolute; left: -12px; width: 20px; height: 30px; bottom: -9px;">x</div></body>`);// returns trueDom.isAboveBottom(Node`<div style="position: absolute; left: -12px; width: 20px; height: 30px; top: 110%;">x</div>`, Node`<body><div style="position: absolute; left: -12px; width: 20px; height: 30px; top: 110%;">x</div></body>`);// returns falseDom.isAboveBottom(Node`<div style="position: absolute; left: -12px; width: 20px; height: 30px; bottom: -17px;">x</div>`, {top: 0, bottom: 50, left: 0, right: 40});// returns falseDom.isAboveBottom(Node`<div style="position: absolute; left: -12px; width: 20px; height: 30px; bottom: -17px;">x</div>`, {top: 0, bottom: 2000, left: 0, right: 40});// returns true
Dom.isInView(elm, regionOrNode)
Determine if an element is within the viewable area of a
region
.
Example
const Dom = require("koru/dom");Dom.isInView(Node`<div style="position:absolute;left:-12px;width:20px;height:30px">x</div>`, Node`<body><div style="position:absolute;left:-12px;width:20px;height:30px">x</div></body>`);// returns falseDom.isInView(Node`<div style="position: absolute; left: -9px; width: 20px; height: 30px;">x</div>`, Node`<body><div style="position: absolute; left: -9px; width: 20px; height: 30px;">x</div></body>`);// returns trueDom.isInView(Node`<div style="position: absolute; left: -9px; width: 20px; height: 30px; bottom: -17px;">x</div>`, {top: 0, bottom: 50, left: 0, right: 40});// returns falseDom.isInView(Node`<div style="position: absolute; left: -9px; width: 20px; height: 30px; bottom: -17px;">x</div>`, {top: 0, bottom: 2000, left: 0, right: 40});// returns true
Dom.loadScript(opts)
Dynamically load a script and wait for it to load
Parameters
opts | object | |
Returns | Promise(undefined) / Promise(Element) |
Example
const Dom = require("koru/dom");await assert.exception( () => Dom.loadScript({src: '/koru/dom/example-test-script-missing.js', id: 'example-script'}), {type: 'error'}, ); document.getElementById('example-script').remove(); const scriptElm = await Dom.loadScript({src: '/koru/dom/example-test-script.js', id: 'example-script'}); assert.dom('#example-script', (elm) => { assert.same(scriptElm, elm); assert.same(elm.getAttribute('testing'), 'loaded'); });
Dom.makeMenustartCallback(callback)
Creates a function suitable for event listeners wanting to open a menu.
Example
const Dom = require("koru/dom");const button = Dom.h({button: 'menu start'}); document.body.appendChild(button); let count = 0; const menuStart = Dom.makeMenustartCallback((event, type) => { if (type === 'menustart') { ++count; } }); button.addEventListener('pointerdown', menuStart); button.addEventListener('click', menuStart); // using mouse Dom.triggerEvent(button, 'pointerdown'); assert.same(count, 1); Dom.triggerEvent(button, 'click'); assert.same(count, 2); // using touch count = 0; Dom.triggerEvent(button, 'pointerdown', {pointerType: 'touch'}); assert.same(count, 0); Dom.triggerEvent(button, 'click', {pointerType: 'touch'}); assert.same(count, 1);
Dom.remove(elm)
Remove element and descontruct its Ctx
Example
const Dom = require("koru/dom");const elm = Dom.h({}); Dom.setCtx(elm, new Dom.Ctx()); document.body.appendChild(elm); assert.same(Dom.remove(elm), true); assert.same(Dom.myCtx(elm), null); assert.same(elm.parentNode, null); assert.same(Dom.remove(elm), false); assert.same(Dom.remove(null), undefined);
Dom.removeInserts(start)
remove inserts between start end markers.
Parameters
start | Node | the start marker; see insertStartEndMarkers |
Example
const Dom = require("koru/dom");Dom.removeInserts({});
Dom.reposition(pos='below', options)
Align element with an origin
Example
const Dom = require("koru/dom");const popup = Dom.h({class: 'popup', $style: 'position:absolute;width:200px;height:10px'}); document.body.appendChild(popup); // default align is left Dom.reposition('above', {popup, boundingClientRect: {left: 50, top: 100, right: 90}}); const rect = popup.getBoundingClientRect(); assert.near(util.extractKeys(rect, ['left', 'top']), {left: 50, top: 90});const popup = Dom.h({ class: 'popup', style: 'position:absolute;width:80px;height:10px'}); document.body.appendChild(popup); // set align justify Dom.reposition('above', { align: 'justify', popup, boundingClientRect: {left: 50, top: 100, right: 120}, }); const rect = popup.getBoundingClientRect(); assert.near(util.extractKeys(rect, ['right', 'top', 'left']), {right: 130, top: 90, left: 50});const popup = Dom.h({ class: 'popup', style: 'position:absolute;width:80px;height:10px'}); document.body.appendChild(popup); // set align right Dom.reposition('above', { align: 'right', popup, boundingClientRect: {left: 50, top: 100, right: 120}, }); const rect = popup.getBoundingClientRect(); assert.near(util.extractKeys(rect, ['right', 'top']), {right: 120, top: 90});
Dom.setCtx(elm, ctx=new Ctx(null, Dom.ctx(elm)))
Parameters
elm | Element | the element to attache the |
[ctx] | Ctx | the context to attach. By default will create a new |
Returns | Ctx | the ctx attached |
Example
const Dom = require("koru/dom");const elm1 = Dom.h({}); const ctx1 = Dom.setCtx(elm1); const elm3 = Dom.h({}); const elm2 = Dom.h({div: elm3}); const ctx2 = Dom.setCtx(elm2, new Ctx(null, ctx1)); const ctx3 = Dom.setCtx(elm3); assert.same(ctx1.firstElement, elm1); assert.same(ctx1.template, null); assert.same(ctx2.firstElement, elm2); assert.same(ctx2.parentCtx, ctx1); assert.same(ctx3.parentCtx, ctx2);
Ctx
Ctx (Context) is used to track DOM elements
Properties
#data | object | The data associated with an element via this context |
#parentCtx | undefined / Ctx | The associated parent Ctx |
Methods
Ctx#addEventListener(elm, type, callback, opts)
This is like the Node#addEventListener
except that it will call
Node#removeEventListener
when the ctx
is destroyed. Also handle the koru event type
'menustart'
.
Example
const Ctx = require("koru/dom/ctx");const ctx = new Ctx(); const button = Dom.h({button: [], type: 'button'}); Dom.setCtx(button, ctx); const callback = stub(), callback2 = stub(), opts = {capture: true}; ctx.addEventListener(button, 'menustart', callback, opts); ctx.addEventListener(button, 'mouseover', callback2); // touch Dom.triggerEvent(button, 'pointerdown', {pointerType: 'touch'}); refute.called(callback); Dom.triggerEvent(button, 'click', {pointerType: 'touch'}); assert.calledOnce(callback); // mouse callback.reset(); Dom.triggerEvent(button, 'pointerdown'); assert.called(callback); Dom.triggerEvent(button, 'click'); assert.calledTwice(callback); // mouseover Dom.triggerEvent(button, 'mouseover'); assert.called(callback2); // destroy Dom.destroyData(button); callback.reset(); Dom.triggerEvent(button, 'pointerdown'); refute.called(callback); Dom.triggerEvent(button, 'mouseover'); assert.calledOnce(callback2);
Ctx#autoUpdate(observe)
Update template contents whenever the ctx.data
contents changes or the ctx.data
object
itself changes.
ctx.data
must have either an observeId
method or an onChange
method in its class otherwise no observation will occur. observeId
is used in
preference to onChange
. BaseModel is such a class.
The observeId
or onChange
handler will be stopped when the ctx
is destroyed.
Parameters
[observe] | function | a optional function that will be called for each update of the subject. |
Example
const Ctx = require("koru/dom/ctx");compileTemplate(`<div>{{name}}{{_id}}</div>`);const stop = stub(); class Custom { static onChange(onChange) { this._onChange = onChange; return {stop}; } } const doc = new Custom(); doc.name = 'old name'; const foo = Dom.tpl.Foo.$render(doc); const ctx = Dom.myCtx(foo); const observer = stub(); ctx.autoUpdate(observer); doc.name = 'new name'; const docChange = DocChange.change(doc, {name: 'old name'}); Custom._onChange(docChange); // change assert.equals(foo.textContent, 'new name'); assert.calledOnceWith(observer, docChange); assert.same(observer.firstCall.thisValue, ctx);
Ctx#stopAutoUpdate()
Stop an autoUpdate. Note that the autoUpdate will automatically stop when the ctx
is destroyed.
Example
const Ctx = require("koru/dom/ctx");const Tpl = compileTemplate(`<div>{{name}}</div>`); const elm = Tpl.$render(new Foo('name1')); const ctx = Dom.myCtx(elm); ctx.autoUpdate(); ctx.stopAutoUpdate(); ctx.data.name = 'name2'; Foo.notify(DocChange.change(ctx.data, {name: 'name1'})); assert.same(elm.textContent, 'name1');
Html
Utilities for building and converting Nodes
Methods
Html.escapeHTML(text)
Escape special html characters
Example
const Html = require("koru/dom/html");assert.same(Html.escapeHTML('<Testing> '), '<Testing>&nbsp;');
Html.html(body, xmlns)
Convert an object
into a html node.
The tagName is determined by either its content not being a string or the other keys are
id
, class
, style
, xmlns
or start with a $
(the $ is stripped).
Array is used when multiple children. Comments have a key of $comment$
. The tagName will
default to "div" if none is given.
When a tag is svg, itself and its children will be in the svg namespace (Client only).
Aliases
h
Parameters
body | object | an object to convert to a Dom node |
[xmlns] | string | use xmlns instead of html |
Returns | Element | A Dom node |
Example
const Html = require("koru/dom/html");const body = {class: 'greeting', id: 'gId', section: { ul: [{li: {span: 'Hello'}}, {$comment$: 'a comment'}, {li: 'two'}, {li: {width: 500, svg: [], viewBox: '0 0 100 100'}}], }, 'data-lang': 'en'}; const ans = util.deepCopy(body); ans.section.ul[3].li.width = '500'; // gets converted back to string const section = Html.html(body); assert.equals(Html.htmlToJson(section), ans); const path = Html.html({path: [], d: 'M0,0 10,10Z'}, Html.SVGNS); assert(path instanceof (isClient ? window.SVGPathElement : global.Element)); const br = Html.h({br: ''}); assert.isNull(br.firstChild);
Html.htmlToJson(node, ns=XHTMLNS)
Convert an Element
to a plain object
Example
const Html = require("koru/dom/html");const obj = {class: 'greeting', id: 'gId', section: { ul: [{li: {span: 'Hello'}}, {li: 'two'}], }, 'data-lang': 'en'}; api.method('htmlToJson'); assert.equals(Html.htmlToJson(Html.h(obj)), obj); const assertConvert = (json) => { assert.elide(() => {assert.equals(Html.htmlToJson(Html.h(json)), json)}); }; assertConvert({div: 'simple'}); assertConvert({}); assertConvert({id: 'Spinner', class: 'spinner dark'}); assertConvert({ol: [ {li: 'one'}, {style: 'width:10px', name: 'li2', li: ['two'], myattr: 'attr3'}]}); assertConvert(['one', 'two', 'three']); assertConvert({input: [], name: 'email'}); assertConvert({input: ''}); assertConvert({div: ['']}); assertConvert({style: 'width:123px'}); assertConvert({style: ['.foo {width:123px}']});
Html.svgUse(href, attrs)
create an svg use link to a ref
Example
const Html = require("koru/dom/html");const circle = Html.svgUse('#icon-circle'); assert.same(circle.getAttributeNS(Html.XLINKNS, 'href'), '#icon-circle'); const menu = Html.svgUse('#icon-hamburger-menu', {x: 3, y: 2}); assert.same(menu.namespaceURI, Html.SVGNS); assert.same(menu.getAttributeNS(Html.XLINKNS, 'href'), '#icon-hamburger-menu'); assert.same(menu.getAttribute('x'), '3'); assert.same(menu.getAttribute('y'), '2');
Template
Template is used to create interactive Dom Trees
Methods
Template.addTemplates(parent, blueprint)
Add nested templates to parent. This is automatically called but is exposed so that it can be overwritten in a sub class.
Example
const Template = require("koru/dom/template");const Tpl = new Template('MyTemplate'); Template.addTemplates(Tpl, { name: 'Sub1', nodes: ['sub 1'], nested: [{name: 'Sub', nodes: ['sub 1 sub']}]}); assert.same(Tpl.Sub1.Sub.$render({}).textContent, 'sub 1 sub');
Template.newTemplate(module, blueprint, parent=this.root)
Create a new Template
from an html blueprint.
Parameters
[module] | Module | if supplied the template will be deleted if
|
blueprint | object | A blue print is usually built by
template-compiler which is called automatically
on html files loaded using
|
[parent] | ||
Returns | Template |
Example
const Template = require("koru/dom/template");Template.newTemplate({Module:myMod}, {name: "Foo", nodes: [{name: 'div'}]});// returns Template(Foo)
Template#$render(data, parentCtx)
Render a template without events
Example
const Template = require("koru/dom/template");const template = new Template();template.$render({});// returns Node`<div></div>`template.$render({});template.$render({});// returns Node`<div></div>`template.$render({});// returns {}template.$render({id: "foo", user: {_id: '123'}});// returns Node`<div id="foo" class="the classes" data-id="123" draggable="true"></div>`template.$render({user: {initial: 'fb', name: 'Foo', nameFunc(){}}});// returns {}template.$render({name: "foo"});// returns Node`<div>foo<p>foo</p></div>`template.$render({user: {initials: 'fb'}});// returns Node`<div>fb</div>`
Enumerable
Enumerable wraps iterables with Array like methods.
Methods
constructor(iter)
Create new Enumerable instance
Example
const Enumerable = require("koru/enumerable");const iter = new Enumerable({*[Symbol.iterator]() {yield 1; yield 3}}); assert.same(iter.count(), 2); assert.equals(Array.from(iter), [1, 3]); assert.same(iter.count(), 2); const iter2 = new Enumerable(function *() {yield 1; yield 3; yield 5}); assert.same(iter2.filter((i) => i != 3).count(), 2); assert.equals(Array.from(iter2), [1, 3, 5]);
Enumerable.count(to, from=1, step=1)
Create an iterator that counts
Parameters
to | number | |
[from] | number | |
[step] | number | |
Returns | Enumerable |
Example
const Enumerable = require("koru/enumerable");assert.equals(Array.from(Enumerable.count(3)), [1, 2, 3]); assert.equals(Array.from(Enumerable.count(20, 13, 3)), [13, 16, 19]);
Enumerable.*mapObjectIter(object, mapper)
return an iterator over an object while mapping (and filtering). If the mapper
returns
undefined
then the value is filtered out of the iterator results
Example
const Enumerable = require("koru/enumerable");const obj = {a: 1, b: 2, c: 3, d: 4}; const names = [], values = []; for (const [n, v] of Enumerable.mapObjectIter(obj, (n, v, i) => (i == 2) ? undefined : 2 * v)) { names.push(n); values.push(v); } assert.equals(names, ['a', 'b', 'd']); assert.equals(values, [2, 4, 8]);
Enumerable.mapObjectToArray(object, mapper)
Map (and filter) an object to array. If the mapper
returns undefined
then the
value is filtered out of the results
Example
const Enumerable = require("koru/enumerable");const obj = {a: 1, b: 2, c: 3, d: 4}; const mapped = Enumerable.mapObjectToArray(obj, (n, v, i) => (i == 2) ? undefined : 2 * v); assert.equals(mapped.length, 3); assert.equals(mapped, [2, 4, 8]);
Enumerable.mapToArray(iter, mapper)
Map (and filter) an iterator to another value. If the mapper
returns undefined
then the
value is filtered out of the results
Example
const Enumerable = require("koru/enumerable");const mapped = Enumerable.mapToArray({*[Symbol.iterator]() {yield 1; yield 5; yield 3}}, (i) => i == 5 ? undefined : 2 * i); assert.equals(mapped.length, 2); assert.equals(mapped, [2, 6]);
Enumerable.propertyValues(object)
Create an iterator over an object's property values
Parameters
object | object | |
Returns | Enumerable |
Example
const Enumerable = require("koru/enumerable");assert.equals(Array.from(Enumerable.propertyValues({a: 1, b: 2})), [1, 2]);
Enumerable.*reverseValues(object)
Return an iterator for the reverse values of an array like structure
Example
const Enumerable = require("koru/enumerable");assert.equals(Array.from(Enumerable.reverseValues([1, 2, 3])), [3, 2, 1]);
Enumerable#every(test)
Return true
if and only if the test
returns a truthy
value for every iteration.
Parameters
test | function | a function called for each iteration with the argument: |
Returns | boolean |
Example
const Enumerable = require("koru/enumerable");const iter = new Enumerable({*[Symbol.iterator]() {yield 1; yield 5; yield 3}}); assert.isTrue(iter.every((i) => i)); assert.isFalse(iter.every((i) => i != 5));
Enumerable#filter(test)
Filter an iterator.
Parameters
test | function | a function called for each iteration with the argument: |
Returns | Enumerable |
Example
const Enumerable = require("koru/enumerable");const iter = new Enumerable({*[Symbol.iterator]() {yield 1; yield 5; yield 3}}); const mapped = iter.filter((i) => i != 5); assert.equals(mapped.count(), 2); assert.equals(Array.from(mapped), [1, 3]); assert.equals(iter.filter((i) => false)[Symbol.iterator]().next(), {done: true, value: void 0});
Enumerable#find(test)
Return first iterated element that test
returns a truthy
value for.
Parameters
test | function | a function called for each iteration with the argument: |
Returns | number |
Example
const Enumerable = require("koru/enumerable");const iter = new Enumerable({*[Symbol.iterator]() {yield 2; yield 5; yield 3}}); assert.equals(iter.find((i) => i % 2 == 1), 5); assert.same(iter.find((i) => i == 7), void 0);
Enumerable#map(mapper)
Map (and filter) an iterator to another value. If the mapper
returns undefined
then the
value is filtered out of the results
Parameters
mapper | function | |
Returns | Enumerable |
Example
const Enumerable = require("koru/enumerable");const iter = new Enumerable({*[Symbol.iterator]() {yield 1; yield 5; yield 3}}); const mapped = iter.map((i) => i == 5 ? undefined : 2 * i); assert.equals(mapped.count(), 2); assert.equals(Array.from(mapped), [2, 6]); assert.equals(iter.map((i) => 2 * i)[Symbol.iterator]().next(), {done: false, value: 2});
Enumerable#reduce(reducer, seed)
Run reducer
on each member returning a single value
Example
const Enumerable = require("koru/enumerable");const iter = new Enumerable({*[Symbol.iterator]() {yield 1; yield 3}}); assert.same(iter.reduce((sum, value) => sum + value, 5), 9); assert.same(iter.reduce((sum, value) => sum - value), -2);
Enumerable#some(test)
Return true
if test
returns a truthy
value for at least one iteration.
Parameters
test | function | a function called for each iteration with the argument: |
Returns | boolean |
Example
const Enumerable = require("koru/enumerable");const iter = new Enumerable({*[Symbol.iterator]() {yield 1; yield 5; yield 3}}); assert.isTrue(iter.some((i) => i == 5)); assert.isFalse(iter.some((i) => false));
format
Text formatter with language translation
Methods
format(fmt, ...args)
Format a string with parameters
Example
const format = require("koru/format");format.format("no args");// returns "no args"format.format("{i0}, {i1} {i2}", "foo", {a: [3, 4, function bar(){}]});// returns "'foo', {a: [3, 4, function bar(){}]} undefined"format.format("abc {1} de\nf {0}", "00");// returns "abc de\nf 00"format.format("abc {1} de\nf {0}", "00", 11);// returns "abc 11 de\nf 00"format.format("{1}edge{} args{2}", "ww", "xx", "yy");// returns "xxedge{ argsyy"format.format("abc{e0}", "<'he llo\"`&>");// returns "abc<'he llo"`&>"format.format(['abc ', 's1', ' def ', 's0'], "00", 11);// returns "abc 11 def 00"format.format("{1}abc{e$foo.bar.baz}", 1, 2, {foo: {bar: {baz: '<fnord>'}}});// returns "2abc<fnord>"format.format("{$foo}", 1);// returns "bar"format.format("{$foo}", {foo: "fuz"});// returns "fuz"format.format("{f0,.2}", 39.99999999999999);// returns "40.00"format.format("hello {f0,.2}", -23.4544);// returns "hello -23.45"format.format("{f0,.1}", 0);// returns "0.0"format.format("{f0,.2}", undefined);// returns ""format.format("{f0,.2z}", 1.3);// returns "1.3"format.format("{f0,.2z}", -4);// returns "-4"format.format("{f0,.2z}", -4.55555);// returns "-4.56"
format.compile(fmt)
Compile a string of text for faster translating and formatting
Example
const format = require("koru/format");format.compile("no args");// returns ['no args']format.compile("abc {1} de\nf {0}");// returns ['abc ', 's1', ' de\nf ', 's0', '']format.compile("{3}edge args{4}");// returns ['', 's3', 'edge args', 's4', '']format.compile("abc {}1} de\nf {0}}");// returns ['abc {1} de\nf ', 's0', '}']format.compile("abc{e1}");// returns ['abc', 'e1', '']format.compile("abc{e$foo.bar.baz}");// returns ['abc', 'e$foo.bar.baz', '']format.compile("abc{f1,.2}");// returns ['abc', 'f1', '.2', '']
format.translate(text, lang='en')
Example
const format = require("koru/format");format.translate(null);// returns nullformat.translate("is_invalid");// returns "is not valid"format.translate("is_invalid", "no");// returns "er ikke gyldig"format.translate("cant_be_less_than:5", "de");// returns "darf nicht kleiner als 5 sein"format.translate(['cant_be_less_than', 20], "de");// returns "darf nicht kleiner als 20 sein"format.translate(['cant_be_less_than', 20], "zu");// returns "can't be less than 20"format.translate("no translation:123", "zu");// returns "no translation:123"format.translate(['test_message', 'one', 'two', 'three']);// returns "testing one and two and three"
FsTools
Convenience wrapper around some node fs
functions
Methods
FsTools.appendData(path, data)
Parameters
path | string | |
data | string | |
Returns | Promise(string) |
Example
const FsTools = require("koru/fs-tools");const ans = fst.appendData('/my/file.txt', 'extra data');
FsTools.readlinkIfExists(path, options)
Parameters
path | string | |
[options] | ||
Returns | Promise(undefined) / Promise(string) |
Example
const FsTools = require("koru/fs-tools");stub(fsp, 'readlink'); fsp.readlink.withArgs('idontexist').invokes(async (c) => {throw {code: 'ENOENT'}}); fsp.readlink.withArgs('accessdenied').invokes(async (c) => {throw {code: 'EACCESS'}}); fsp.readlink.withArgs('iamasymlink').returns(Promise.resolve('pointinghere')); assert.same(await fst.readlinkIfExists('idontexist'), undefined); assert.same(await fst.readlinkIfExists('iamasymlink'), 'pointinghere'); await assert.exception(() => fst.readlinkIfExists('accessdenied'), {code: 'EACCESS'});
Future
Future is a utility class for waiting and resolving promises.
Properties
isResolved |
| |
#promise | Promise(undefined) / Promise(number) | The promise to await for |
Methods
constructor()
Example
const Future = require("koru/future");const future = new Future(); assert.isFalse(future.isResolved); future.resolve(123); assert.isTrue(future.isResolved); assert.same(await future.promise, 123);
Future#promiseAndReset()
After waiting for the promise; reinitialize so it can be waited for again;
Parameters
Returns | Promise(undefined) / Promise(number) |
Example
const Future = require("koru/future");const future = new Future(); future.reject(new Error('reject')); await assert.exception(() => future.promiseAndReset(), {message: 'reject'}); future.resolve(456); assert.same(await future.promiseAndReset(), 456); assert.isFalse(future.isResolved);
Geometry
Methods
Geometry.bezierBox(ps, curve)
Calculate the boundry box for a cubic bezier curve
Parameters
ps | Array | start point |
curve | Array | of the form
|
Returns | object | boundryBox in format |
Example
const Geometry = require("koru/geometry");assert.equals(sut.bezierBox([0,0], [0,0, 20,25, 20,25]), {left: 0, top: 0, right: 20, bottom: 25}); assert.equals(sut.bezierBox([0,0], [0,0, -20,-25, -20,-25]), {left: -20, top: -25, right: 0, bottom: 0}); assert.equals(sut.bezierBox([0,0], [0,0, -20,-25, -20,-25]), {left: -20, top: -25, right: 0, bottom: 0}); assert.near(sut.bezierBox([10000,20000], [-5000,-10000, 57500,70000, 40000,30000]), {left: 7653, top: 13101, right: 43120, bottom: 41454});
Geometry.closestT(point, ps, curve, tol=0.00001)
Calculate t along a line or a bezier curve closes to point
Parameters
point | Array | the point to project on to curve |
ps | Array | start point |
curve | Array | bezier curve (See bezierBox) or end point for line |
[tol] | ||
Returns | number | t along the curve |
Example
const Geometry = require("koru/geometry");Geometry.closestT([300, 30], [300, 30], [100, 70]);// returns 0Geometry.closestT([100, 70], [300, 30], [100, 70]);// returns 1Geometry.closestT([-15, 50], [300, 30], [100, 70]);// returns 1Geometry.closestT([230, 150], [300, 30], [100, 70]);// returns 0.4519230769230769Geometry.closestT([-1000, -3000], [300, 30], [100, 70]);// returns 1Geometry.closestT([2500, 43], [300, 30], [100, 70]);// returns 0Geometry.closestT([300, 130], [300, 130], [-400, -200, 1140, 500, 200, 100]);// returns 0Geometry.closestT([200, 100], [300, 130], [-400, -200, 1140, 500, 200, 100]);// returns 1Geometry.closestT([-15, 50], [300, 130], [-400, -200, 1140, 500, 200, 100]);// returns 0.1970015204601111Geometry.closestT([195, 100], [300, 130], [-400, -200, 1140, 500, 200, 100]);// returns 1Geometry.closestT([600, 200], [300, 130], [-400, -200, 1140, 500, 200, 100]);// returns 0.7498621750479387Geometry.closestT([-1000, -3000], [300, 130], [-400, -200, 1140, 500, 200, 100]);// returns 0.20026090643926941Geometry.closestT([-14, 0], [0, 0], [0, 0, 20, 25, 20, 25]);// returns 0Geometry.closestT([10, 12.5], [0, 0], [0, 0, 20, 25, 20, 25]);// returns 0.49999913899703097Geometry.closestT([20, 25], [0, 0], [0, 0, 20, 25, 20, 25]);// returns 1Geometry.closestT([25978, 28790], [10000, 20000], [-5000, -10000, 57500, 70000, 40000, 30000]);// returns 0.5005837534888542
Geometry.combineBox(a, b)
Combine two boundry boxes
Parameters
a | object | the first boundry box which is modified to include the second |
b | object | the second boundry box |
Returns | object |
|
Example
const Geometry = require("koru/geometry");Geometry.combineBox({left: 0, top: 0, right: 20, bottom: 25}, {left: -20, top: -25, right: 0, bottom: 0});// returns {left: -20, top: -25, right: 20, bottom: 25}Geometry.combineBox({left: -20, top: -25, right: 20, bottom: 25}, {left: -10, top: -15, right: 10, bottom: 15});// returns {left: -20, top: -25, right: 20, bottom: 25}Geometry.combineBox({left: -20, top: -25, right: 20, bottom: 25}, {left: -40, top: -55, right: 60, bottom: 95});// returns {left: -40, top: -55, right: 60, bottom: 95}
Geometry.combineBoxPoint(box, x, y)
Combine a point into a boundy box
Parameters
box | object | the boundry box which is modified to include the point |
x | number | the x coordinate of the point |
y | number | the y coordinate of the point |
Returns | object |
|
Example
const Geometry = require("koru/geometry");Geometry.combineBoxPoint({left: 0, top: 0, right: 20, bottom: 25}, -20, -25);// returns {left: -20, top: -25, right: 20, bottom: 25}Geometry.combineBoxPoint({left: -20, top: -25, right: 20, bottom: 25}, 10, 15);// returns {left: -20, top: -25, right: 20, bottom: 25}Geometry.combineBoxPoint({left: -20, top: -25, right: 20, bottom: 25}, 100, 150);// returns {left: -20, top: -25, right: 100, bottom: 150}
Geometry.rotatePoints(points, angle)
rotate points around (0,0)
Example
const Geometry = require("koru/geometry");Geometry.rotatePoints([0, 0, -10, -20, 30, 40], 180);// returns [0, 0, 10, 20, -30, -40]Geometry.rotatePoints([0, 0, -10, -20, 30, 40], -180);// returns [0, 0, 10, 20, -30, -40]Geometry.rotatePoints([0, 0, -10, -20, 30, 40], 90);// returns [0, 0, 20, -10, -40, 30]Geometry.rotatePoints([0, 0, -10, -20, 30, 40], -90);// returns [0, 0, -20, 10, 40, -30]Geometry.rotatePoints([0, 0, -10, -20, 30, 40], 45);// returns [0, 0, 7.071067811865474, -21.213203435596427, -7.071067811865472, 49.49747468305833]
Geometry.splitBezier(t, ps, curve)
Split a bezier curve into two at point t. The passed curve is modified and the second curve is returned.
Parameters
t | number | 0 < t < 1 where 0 is start point and 1 is end point |
ps | Array | start point |
curve | Array | bezier curve (See bezierBox) |
Returns | Array | the second curves in form |
Example
const Geometry = require("koru/geometry");Geometry.splitBezier(0.5, [10000, 20000], [2500, 5000, 14375, 17500, 25937.5, 28750]);// returns [37500, 40000, 48750, 50000, 40000, 30000]
Geometry.tPoint(t, ps, curve)
Calculate the point at t along a line or a bezier curve
Parameters
t | number | 0 < t < 1 where 0 is start point and 1 is end point |
ps | Array | start point |
curve | Array | bezier curve (See bezierBox) or end point for line |
Returns | Array | the midpoint in form |
Example
const Geometry = require("koru/geometry");Geometry.tPoint(0, [10, 30], [-40, 70]);// returns [10, 30]Geometry.tPoint(0.5, [10, 30], [-40, 70]);// returns [-15, 50]Geometry.tPoint(1, [10, 30], [-40, 70]);// returns [-40, 70]Geometry.tPoint(0, [0, 0], [0, 0, 20, 25, 20, 25]);// returns [0, 0]Geometry.tPoint(0.5, [0, 0], [0, 0, 20, 25, 20, 25]);// returns [10, 12.5]Geometry.tPoint(1, [0, 0], [0, 0, 20, 25, 20, 25]);// returns [20, 25]Geometry.tPoint(0.5, [10000, 20000], [-5000, -10000, 57500, 70000, 40000, 30000]);// returns [25937.5, 28750]
Geometry.tTangent(t, ps, curve)
Calculate the tangent at t along a line or a bezier curve
Parameters
t | number | 0 < t < 1 where 0 is start point and 1 is end point |
ps | Array | start point |
curve | Array | bezier curve (See bezierBox) or end point for line |
Returns | Array | the tangent normalized vector in form |
Example
const Geometry = require("koru/geometry");Geometry.tTangent(0, [10000, 20000], [-5000, -10000, 57500, 70000, 40000, 30000]);// returns [-0.4472135954999579, -0.8944271909999159]Geometry.tTangent(0.5, [10000, 20000], [-5000, -10000, 57500, 70000, 40000, 30000]);// returns [0.7167259309091052, 0.6973549598034537]Geometry.tTangent(1, [10000, 20000], [-5000, -10000, 57500, 70000, 40000, 30000]);// returns [-0.40081883401970775, -0.9161573349021892]Geometry.tTangent(0, [10, 30], [-40, 70]);// returns [-0.7808688094430304, 0.6246950475544243]Geometry.tTangent(1, [10, 30], [-40, 70]);// returns [-0.7808688094430304, 0.6246950475544243]Geometry.tTangent(0.3, [10, 30], [-40, 70]);// returns [-0.7808688094430304, 0.6246950475544243]Geometry.tTangent(0, [0, 0], [0, 0, 20, 25, 20, 25]);// returns [0.6246950475544243, 0.7808688094430304]Geometry.tTangent(0, [0, 0], [20, 25]);// returns [0.6246950475544243, 0.7808688094430304]Geometry.tTangent(0.5, [0, 0], [0, 0, 20, 25, 20, 25]);// returns [0.6246950475544243, 0.7808688094430304]Geometry.tTangent(0, [0, 0], [20, 25]);// returns [0.6246950475544243, 0.7808688094430304]Geometry.tTangent(1, [0, 0], [0, 0, 20, 25, 20, 25]);// returns [0.6246950475544243, 0.7808688094430304]Geometry.tTangent(1, [0, 0], [20, 25]);// returns [0.6246950475544243, 0.7808688094430304]
IdleCheck
IdleCheck keeps count of usage and notifies when idle.
Properties
singleton | IdleCheck | The default |
Methods
constructor()
Example
const IdleCheck = require("koru/idle-check");new IdleCheck();
IdleCheck#waitIdle(func)
waitIdle waits until this.count
drops to zero.
Parameters
func | function |
Example
const IdleCheck = require("koru/idle-check");const check = new IdleCheck(); const callback = stub(); check.waitIdle(callback); assert.called(callback);
koru.Error
Main error class for koru errors.
Properties
#error | number | The http error code for the error |
#reason | string / object | The textual reason or an |
Methods
constructor(error, reason)
Create a new koru.Error
Parameters
error | number | an http error code |
reason | string / object | the reason for the error or an object such as Val results. |
Example
const koru.Error = require("koru/koru-error");const error = new koru.Error(500, 'the reason'); assert.same(error.name, 'KoruError'); assert.same(error.toString(), 'the reason [500]'); assert.same(error.error, 500); assert.equals(error.reason, 'the reason'); const err2 = new koru.Error(400, {name: [['is_invalid']]}); assert.same(err2.toString(), `{name: [['is_invalid']]} [400]`); assert.equals(err2.reason, {name: [['is_invalid']]});
LinkedList
A single linked list. The list is iterable.
Properties
#back | object | the back node in the list |
#backValue | number | the back value in the list |
#front | object | the front node in the list |
#frontValue | number | the front value in the list |
#size | number | The number of nodes in the list |
Methods
LinkedList#addBack(value)
Add value
to back of list.
Example
const LinkedList = require("koru/linked-list");const ll = new LinkedList; ll.addBack(1); ll.addBack(2); ll.addBack(3); assert.equals(listToString(ll), "1 2 3");
LinkedList#clear()
clear all entries. (calls listEmpty
if present)
Example
const LinkedList = require("koru/linked-list");const subject = new LinkedList(); subject.push(1); subject.push(2); assert.equals(subject.size, 2); subject.clear(); assert.equals(subject.size, 0); assert.equals(Array.from(subject), []);
LinkedList#forEach(callback)
visit each entry from front to back
Parameters
callback | function |
Example
const LinkedList = require("koru/linked-list");const subject = new LinkedList(); subject.push('b'); subject.push('a'); const ans = []; subject.forEach(v => {ans.push(v)}); assert.equals(ans, ['a', 'b']);
LinkedList#*nodes()
Return an iterator over the nodes from font to back. (add returns the node
)
Parameters
Returns | Function |
Example
const LinkedList = require("koru/linked-list");const subject = new LinkedList(); const f = ()=>{}; const exp = [ m.is(subject.addBack(1)), m.is(subject.addBack(f)) ]; subject.addBack('a'); const ans = []; for (const h of subject.nodes()) { ans.push(h); if (h.value === f) break; } assert.equals(ans, exp);
LinkedList#pop()
Remove and return value
from front of list.
Parameters
Returns | number |
Example
const LinkedList = require("koru/linked-list");const ll = new LinkedList; ll.push(1); ll.push(2); ll.push(3); assert.same(ll.pop(), 3); assert.same(ll.pop(), 2); assert.equals(listToString(ll), "1");
LinkedList#popNode()
Remove and return node
from front of list.
Parameters
Returns | object |
Example
const LinkedList = require("koru/linked-list");const ll = new LinkedList; ll.push(1); ll.push(2); ll.push(3); assert.equals(ll.popNode(), {value: 3, next: m.object}); assert.equals(listToString(ll), "2 1");
LinkedList#push(value)
Push value
to front of list.
Aliases
addFront
Example
const LinkedList = require("koru/linked-list");const ll = new LinkedList; ll.push(1); ll.push(2); ll.push(3); assert.equals(listToString(ll), "3 2 1");
LinkedList#removeNode(node, prev)
Search for and remove node
Parameters
node | object | the node to remove |
[prev] | undefined / object | where to start the search from. Defaults to |
Returns | object | the node removed or void 0 if not found |
Example
const LinkedList = require("koru/linked-list");add(ll, 1, 2, 3, 4); let prev; for (let node = ll.front; node !== void 0; node = node.next) { if (node.value % 2 == 1) ll.removeNode(node, prev); else prev = node; } assert.equals(listToString(ll), "2 4"); ll.removeNode(ll.back); assert.equals(listToString(ll), "2");
LinkedList#*values()
Return an iterator over the values from front to back.
Aliases
[symbol.iterator]
Parameters
Returns | Function |
Example
const LinkedList = require("koru/linked-list");const subject = new LinkedList(); const f = ()=>{}; subject.push('a'); subject.push(f); subject.push(1); const ans = []; for (const v of subject.values()) { ans.push(v); if (v === f) break; } assert.equals(ans, [1, m.is(f)]);
makeSubject
Make an object observable
See also Observable
Methods
makeSubject( subject={}, observeName='onChange', notifyName='notify', {allStopped, init, stopAllName}={} )
Make an object observable by adding observe and notify methods to it.
Parameters
subject | object | the object observe |
[observeName] | string | method name to call to start observing
|
[notifyName] | string | method name to tell observers of a change (defaults to notify) |
[allStopped] | function | method will be called with subject when all observers have stopped. |
[init] | ||
[stopAllName] | ||
Returns | object | adorned makeSubject::subject parameter |
Example
const makeSubject = require("koru/make-subject");const eg1 = {eg: 1}; const subject = makeSubject(eg1); assert.same(subject, eg1); assert.isFunction(subject.onChange); assert.isFunction(subject.notify); const subject2 = makeSubject({eg: 2}, 'onUpdate', 'updated'); assert.isFunction(subject2.onUpdate); assert.isFunction(subject2.updated);
match
Match allows objects to be tested for equality against a range of pre-built or custom matchers.
The util.deepEqual function will honour any matchers found in the expected
(second)
argument.
Note when testing Core.match is a separate clone of this match framework. This ensures any stubbing of match properties will not interfere with the test asserts.
Properties
any | Match | match anything (or nothing) |
array | Match | match any object that is true for |
baseObject | Match | match any object where constructor is Object |
date | Match | match any date |
error | Match | match any error |
func | Match | match any function |
id | Match | match a valid model |
integer | Match | match any integer |
match | Match | match any matcher |
nil | Match | match undefined or null |
null | Match | match null |
object | Match | match anything of type |
optional | function | match a standard matcher or |
string | Match | match any string |
symbol | Match | match any symbol |
undefined | Match | match undefined |
Methods
match.equal(expected, name='match.equal')
Match expected
using util.deepEqual
Example
const match = require("koru/match");const me = match.equal([1,match.any]); assert.isTrue(me.test([1,'x'])); assert.isTrue(me.test([1, null])); assert.isFalse(me.test([1])); assert.isFalse(me.test([1, 1, null])); assert.isFalse(me.test([2, 1]));
match.is(expected, name=() => `match.is(${util.inspect(expected)})`)
Match exactly; like Object.is
Example
const match = require("koru/match");match.is({foo: 123});// returns match.is({foo: 123})
match(test, name)
Build a custom matcher.
Parameters
test | Function / RegExp / Array | used to test for truthfulness |
[name] | string | override the default name for the matcher. |
Returns | Match |
Example
const match = require("koru/match");const match5 = match(arg => arg == 5); assert.isTrue(match5.test(5)); assert.isTrue(match5.test("5")); assert.isFalse(match5.test(4)); assert.same(''+match(()=>true, 'my message'), 'my message');assert(match(/abc/).test('aabcc')); refute(match(/abc/).test('aabbcc')); assert(match([1, match.any]).test([1, 'foo'])); refute(match([2, match.any]).test([1, 'foo']));
match.not(arg)
Match not. Invert the result of arg
matcher.
Example
const match = require("koru/match");assert.isTrue(match.not(match.string).test(1)); assert.isFalse(match.not(match.number).test(1));
Match
Class of matchers.
Methods
Match#$throwTest(value)
Throw if test fails
Example
const match = require("koru/match");try { assert.isTrue(match.any.$throwTest(null)); } catch(ex) { assert.fail("did not expect "+ex); } try { match.func.$throwTest(123); assert.fail("expected expection"); } catch(ex) { assert.same(ex, 'match.func'); }
Match#test(actual, $throwTest)
Test if matcher matches.
Example
const match = require("koru/match");assert.isTrue(match.any.test(null)); assert.isFalse(match.func.test(123));
MathUtil
Methods
MathUtil.normDist(rng=Math.random, mean=0, stdDev=1)
Make a function that generates a normal distribution of random numbers. Uses the Marsaglia polar method
Parameters
[rng] | function | the random number generation; needs to generate a uniform real distribution between 0 and 1. |
[mean] | number | |
[stdDev] | number | the standard deviation |
Returns | function |
Example
const MathUtil = require("koru/math-util");assert.same(typeof MathUtil.normDist(), 'function'); const rng = Random.prototype.fraction.bind(new Random(1)); const ndg = MathUtil.normDist(rng, 4, 2); assert.near(ndg(), 4.904, 0.001); assert.near(ndg(), 2.093, 0.001); assert.near(ndg(), 5.344, 0.001);
Migration
Run Database migrations to apply or revert changes to a database in order to align the DB with the expectations of the source code.
Files are executed in alphanumeric order and are conventionally named:
yyyy-mm-ddThh-mm-ss-usage.js
where usage
is like create-user
, add-index-to-book
Example: 2018-12-20T05-38-09-add-author_id-to-book.js
Migrations also adds a table called "Migration" to the db which has one column name
containing all the successfully run migrations.
Methods
Migration#async migrateTo(dirPath, pos, verbose)
Migrate the DB to a position. Each migration is run within a transaction so that it only modifies the DB if it succeeds. If a migration fails no further migrations are run.
Parameters
dirPath | string | the directory containing the migration files |
pos | string | Migration files contained in the |
[verbose] | boolean | print messages to |
Returns | Promise(undefined) |
Example
const Migration = require("koru/migrate/migration");const migration = new Migration();migration.migrateTo("koru/migrate/test-migrations", "2015-06-19T17-57-32");// returns undefinedmigration.migrateTo("koru/migrate/test-migrations", "2015-06-19T17-49-31~");// returns undefinedmigration.migrateTo("koru/migrate/test-migrations", " ", true);// returns undefined
Commander
The facilitator of a migration. An instance is passed to a migration file and is used to define the actions to apply or revert for this migration entry.
Example
define(()=> mig =>{
mig.addColumns("Book", "topic_id:id");
});
Methods
Commander#addColumns(tableName, ...args)
Add columns to a DB table.
Parameters
tableName | string | the name of the table to add the columns to. |
args | object / string | either a list of |
Example
const Migration = require("koru/migrate/migration");// file contentsdefine(() => (mig) => { mig.addColumns('Book', { pageCount: {type: 'int8', default: 0}, author_id: 'id', }); });define(() => (mig) => { mig.addColumns('Book', 'pageCount:int8', 'title'); });
Commander#addIndex(tableName, spec)
Add an index to a table.
Parameters
tableName | string | The name of the table to add the index to. | |||||||||||||||
spec | object | The specification of the index containing:
|
Example
const Migration = require("koru/migrate/migration");define(() => (mig) => { mig.addIndex('Book', { columns: ['title DESC', '_id'], unique: true, where: '"pageCount" > 50', }); mig.addIndex('Book', { name: 'short_books', columns: ['title DESC'], where: '"pageCount" < 50', }); });
Commander#createTable(name, fields, indexes)
Create a table in the database. Arguments may also be named ({name, fields, indexes}
)
Parameters
name | string | the name of the table |
fields | object | name-value entries describing the fields of the table. The value for each field can be a string containing the type or a object containing: |
indexes | Array | a list of index specifications to add to the table. See addIndex |
Example
const Migration = require("koru/migrate/migration");define(() => (mig) => { mig.createTable( 'Label', { name: 'text', backgroundColor: {type: 'color', default: '#ffffff'}, }, [ {columns: ['name DESC', '_id'], unique: true}, ['backgroundColor']]); // columns }); assert.equals(Label._colMap._id.oid, 25); assert.equals(Label._colMap._id.collation_name, 'C'); assert.equals(Label._colMap.name.oid, 25); assert.equals(Label._colMap.name.collation_name, void 0); assert.equals(Label._colMap.backgroundColor.oid, 25); assert.equals(Label._colMap.backgroundColor.collation_name, 'C'); await client.query('INSERT INTO "Label" (_id, "name") values ($1,$2)', [ '12345670123456789', 'Bug']); const doc = (await client.query('SELECT * from "Label"'))[0]; assert.same(doc._id, '12345670123456789'); assert.same(doc.name, 'Bug'); assert.same(doc.backgroundColor, '#ffffff');
Commander#reversible({add, revert, resetTables})
Execute reversible database instructions.
Parameters
add | Function | a function to call when adding a migration (migrate up) to the DB. Is passed a pg::Client instance. |
revert | Function | a function to call when reverting a migration (migrate down) to the DB. Is passed a pg::Client instance. |
resetTables | Array | a List of tables to reset the schema definition for |
Example
const Migration = require("koru/migrate/migration");define(() => (mig) => { mig.reversible({ async add(client) { await client.query(`alter table "Book" rename column name to title`); await client.query(`insert into "Book" (_id, title) values ('book123', 'Emma')`); }, async revert(client) { await client.query(`delete from "Book"`); await client.query(`alter table "Book" rename column title to name`); }, resetTables: ['Book'], }); });
Model
Object persistence manager. Defines application models.
BaseModel
The base class for all models
Methods
onChange(callback)
Observe changes to model records.
Parameters
callback | function | is called with a DocChange instance. |
Returns | object | contains a stop method to stop observering |
Example
const BaseModel = require("koru/model/base-model");const observer = stub(); after(Book.onChange(observer)); const Oasis = await Book.create({_id: 'm123', title: 'Oasis', pageCount: 425}); const matchOasis = m.field('_id', Oasis._id); assert.calledWith(observer, DocChange.add(Oasis)); await Oasis.$update('pageCount', 420); assert.calledWith(observer, DocChange.change(matchOasis, {pageCount: 425})); await Oasis.$remove(); assert.calledWith(observer, DocChange.delete(matchOasis));
BaseModel.assertFound(doc)
Assert model instance is found
Example
const BaseModel = require("koru/model/base-model");class Book extends BaseModel {} Book.define({module}); assert.exception(() => { Book.assertFound(null); }, {error: 404, message: 'Book Not found'}); refute.exception(() => { const book = Book.build(); Book.assertFound(book); });
BaseModel.build(attributes, allow_id=false)
Build a new model. Does not copy _id from attributes.
Example
const BaseModel = require("koru/model/base-model");const doc = await Book.create(); const copy = Book.build(doc.attributes); refute.same(doc.attributes, copy.changes); assert.same(doc.title, copy.title); assert.equals(copy._id, null); assert.equals(copy.changes._id, null);
BaseModel.define({module, inspectField='name', name=moduleName(module), fields})
Define and register a model.
Parameters
[module] | Module | needed to auto unload module when file changed |
[inspectField] | ||
[name] | string | name of model. Defaults to derived from module name. |
[fields] | object | call defineFields with fields |
Returns | BaseModel | the model |
Example
const BaseModel = require("koru/model/base-model");class Book extends BaseModel {} Book.define({module, fields: { title: 'text', pages: {type: 'number', number: {'>': 0}}, }}); assert.same(Model.Book, Book); const book = new Book(); book.title = 'The Hedge Knight'; assert.equals(book.changes, {title: 'The Hedge Knight'}); book.pages = -1; refute(book.$isValid()); assert.equals(book[error$], {pages: [['must_be_greater_than', 0]]});
BaseModel.defineFields(fields)
Define the types and validators of the fields in a model. Usually called with define
Valid types are:
type | javascript type | DB type | |
---|---|---|---|
any | object | ||
auto_timestamp | Date | timestamp | Set the timestamp on create; if field name contains /create/i ; otherwise on update. |
baseObject | object | jsonb | |
belongs_to_dbId | string | text | A
|
belongs_to | string | text | Associate this model belonging to another model. A getter is defined to retrieve the associated model using this fields name less the _id suffix. |
boolean | boolean | boolean | |
color | string | text | |
date | Date | date | |
has_many | Array | text[] | Used with the AssociatedValidator to map to many ids. |
id | string | text | |
integer | number | integer | |
jsonb | object | jsonb | |
number | number | double precision | |
object | object | jsonb | |
string | string | text | |
text | string | text | |
user_id_on_create | string | text | Like belongs_to but also sets the field on create to the logged in user_id . |
Parameters
fields | object | an key-value object with keys naming the fields and values defining the field
Types and validators may also make use of other specific options. The field | ||||||||||||||
Returns | BaseModel | the model |
Example
const BaseModel = require("koru/model/base-model");class Book extends BaseModel {} Book.define({module}); Book.defineFields({ title: 'text', pages: {type: 'number', number: {'>': 0}}, }); const book = new Book(); book.title = 'The Hedge Knight'; assert.equals(book.changes, {title: 'The Hedge Knight'}); book.pages = -1; refute(book.$isValid()); assert.equals(book[error$], {pages: [['must_be_greater_than', 0]]});
BaseModel.findById(id)
Find a document by its _id
. Returns the same document each time if called from same
thread.
Example
const BaseModel = require("koru/model/base-model");const doc = await Book.create({title: 'Emma', pageCount: 342}); assert.same(await Book.findById(doc._id), doc);
BaseModel.isIdLocked(id)
Test if an id is locked.
Example
const BaseModel = require("koru/model/base-model");await TransQueue.transaction(async () => { await Book.lockId('bookid123'); assert.isTrue(Book.isIdLocked('bookid123')); assert.isFalse(Book.isIdLocked('bookid456')); }); assert.isFalse(Book.isIdLocked('bookid123'));
BaseModel.async lockId(id)
Wait for a lock on an id in this model using a Mutex. Must be used in a TransQueue. Locks until the transaction is finished.
Parameters
id | string | usally the id of a DB record. |
Returns | Promise(undefined) |
Example
const BaseModel = require("koru/model/base-model");await TransQueue.transaction(async () => { await Book.lockId('bookid123'); assert.isTrue(Book.isIdLocked('bookid123')); await Book.lockId('bookid123'); // does nothing if called more than once }); assert.isFalse(Book.isIdLocked('bookid123')); await assert.exception( () => Book.lockId('bookid123'), {message: 'Attempt to lock while not in a transaction'}, );
BaseModel.remote(funcs)
Define multiple Remote updating Procedure calls prefixed by model's name.
Example
const BaseModel = require("koru/model/base-model");Book.remote({ read() {}, catalog() {}, }); assert(session.isRpc('Book.read')); refute(session.isRpcGet('Book.read')); assert(session.isRpc('Book.catalog'));
BaseModel.remoteGet(funcs)
Define multiple Remote inquiry Procedure calls prefixed by model's name.
Example
const BaseModel = require("koru/model/base-model");Book.remoteGet({ list() {}, about() {}, }); assert(session.isRpc('Book.list')); assert(session.isRpcGet('Book.list')); assert(session.isRpcGet('Book.about'));
BaseModel#$$save()
Is shorthand for $save("assert")
Parameters
Returns | Promise(BaseModel) |
Example
const BaseModel = require("koru/model/base-model");Book.defineFields({author: {type: 'text', required: true}}); const book = Book.build(); try { await book.$$save(); assert.fail('expect throw'); } catch (err) { assert.equals(err.error, 400); assert.equals(err.reason, {author: [['is_required']]}); } book.author = 'T & T'; await book.$$save(); assert.same((await book.$reload(true)).author, 'T & T');
BaseModel#$assertValid()
Validate the document and throw an error if invalid
Parameters
Returns | Promise(undefined) / Promise(boolean) |
Example
const BaseModel = require("koru/model/base-model");Book.defineFields({author: { type: 'text', async validate(field) { if (! this[field]) return 'is_required'}}}); const book = Book.build(); try { await book.$assertValid(); assert.fail('Should not have succeeded'); } catch (err) { assert.same(err.constructor, koru.Error); assert.equals(err.error, 400); assert.equals(err.reason, {author: [['is_required']]}); } book.author = 'T & T'; await book.$assertValid(); assert.same(book[error$], undefined);
BaseModel#$isValid()
Check if a document is valid
Parameters
Returns | Promise(boolean) |
Example
const BaseModel = require("koru/model/base-model");class Book extends BaseModel {} Book.define({ module, fields: { pages: {type: 'number', async validate(field) { await 1; return 'is_invalid'; }, changesOnly: true}, }, }); const book = new Book(); book.attributes.pages = -1; // original$ exists book[original$] = undefined; refute(await book.$isValid()); assert.equals(book[error$], {pages: [['is_invalid']]});
BaseModel#$save(mode)
Validate the document and persist to server DB. Runs before
, after
and onChange
hooks.
Parameters
[mode] | string | If |
Returns | boolean |
Example
const BaseModel = require("koru/model/base-model");const baseModel = new BaseModel();// normal (undefined) mode Book.defineFields({author: {type: 'text', required: true}}); const book = Book.build(); assert.isFalse(await book.$save()); assert.equals(book[error$], {author: [['is_required']]}); book.author = 'T & T'; assert.isTrue(await book.$save()); assert.same(book[error$], undefined); assert.same((await book.$reload(true)).author, 'T & T');// "force" mode Book.defineFields({author: {type: 'text', required: true}}); const book = Book.build({author: null}); spy(book, '$isValid'); await book.$save('force'); assert.called(book.$isValid); // assert validation methods were run assert(await Book.findById(book._id));// "assert" mode Book.defineFields({author: {type: 'text', required: true}}); const book = Book.build(); try { await book.$save('assert'); assert.fail('Should not have saved'); } catch (err) { assert.same(err.constructor, koru.Error); assert.equals(err.error, 400); assert.equals(err.reason, {author: [['is_required']]}); } book.author = 'T & T'; await book.$save('assert'); assert.same(book[error$], undefined); assert.same((await book.$reload(true)).author, 'T & T');
BaseModel#$withChanges(changes=this.changes)
Return a doc representing this doc with the supplied changes staged against it such that calling $save will apply the changes.
If this method is called again with the same changes object then a cached version of the before doc is returned.
Example
const BaseModel = require("koru/model/base-model");const doc = new Book({ _id: '123', pages: {bar: {baz: 'new val', buzz: 5}, fnord: {a: 1}}}); assert.same(doc.$withChanges('add'), doc); assert.same(doc.$withChanges('del'), null); let undo = {$partial: { pages: [ 'bar.baz.$partial', ['$match', 'new val', '$patch', [0, 3, 'orig']], 'bar.buzz', 2, 'fnord.a', 2], author: ['$replace', 'H. G. Wells'], }}; let old = doc.$withChanges(undo); assert.same(old.pages.bar.baz, 'orig val'); assert.same(old.pages.bar.buzz, 2); assert.same(old.author, 'H. G. Wells'); assert.same(doc.pages.bar.baz, 'new val'); assert.same(doc.pages.bar.buzz, 5); assert.same(doc.pages.fnord.a, 1); assert.same(doc.$withChanges(undo), old); old = doc.$withChanges({$partial: { pages: [ 'bar.baz', null, 'bar.buzz', 2, 'fnord.a', 2], author: ['$replace', null], }}); assert.same(old.pages.bar.baz, undefined); assert.same(old.pages.bar.buzz, 2); assert.same(old.author, undefined);
dbBroker
dbBroker allows for multiple databases and server connections within one browser instance.
Methods
dbBroker.makeFactory(DBRunner, ...args)
Make a factory that will create runners as needed for the current thread DB. Runners are useful to keep state information on a per DB basis
Parameters
DBRunner | object | |
args | [any-type] | arbitrary arguments to pass to the constructor |
Returns | object |
Example
const dbBroker = require("koru/model/db-broker-client");const defId = dbBroker.dbId; const altId = "alt"; class DBRunner extends dbBroker.DBRunner { constructor(a, b) { super(); this.a = a; this.b = b; this.hasStopped = false; } stopped() {this.hasStopped = true} } const DBS = dbBroker.makeFactory(DBRunner, 1, 2); const defRunner = DBS.current; assert.same(defRunner.a, 1); assert.same(defRunner.b, 2); assert.same(defRunner.constructor, DBRunner); assert.same(defRunner.dbId, defId); dbBroker.dbId = altId; const altRunner = DBS.current; assert.same(altRunner.dbId, altId); dbBroker.dbId = defId; assert.same(DBS.current, defRunner); assert.equals(Object.keys(DBS.list).sort(), ['alt', 'default']); assert.isFalse(defRunner.hasStopped); DBS.stop(); assert.equals(DBS.list, {}); assert.isTrue(defRunner.hasStopped); assert.isTrue(altRunner.hasStopped);
dbBroker
dbBroker allows for multiple databases to be connected to one nodejs instance
Methods
dbBroker.makeFactory(DBRunner, ...args)
Make a factory that will create runners as needed for the current thread DB. Runners are useful to keep state information on a per DB basis
Parameters
DBRunner | object | |
args | [any-type] | arbitrary arguments to pass to the constructor |
Returns | object |
Example
const dbBroker = require("koru/model/db-broker-server");class DBRunner extends dbBroker.DBRunner { constructor(a, b) { super(); this.a = a; this.b = b; this.hasStopped = false; } stopped() {this.hasStopped = true} } const DBS = sut.makeFactory(DBRunner, 1, 2); const defRunner = DBS.current; assert.same(defRunner.a, 1); assert.same(defRunner.b, 2); assert.same(defRunner.constructor, DBRunner); assert.same(defRunner.db, defDb); sut.db = altDb; const altRunner = DBS.current; assert.same(altRunner.db, altDb); sut.db = defDb; assert.same(DBS.current, defRunner); assert.equals(Object.keys(DBS.list).sort(), ['alt', 'default']); assert.isFalse(defRunner.hasStopped); DBS.stop(); assert.equals(DBS.list, {}); assert.isTrue(defRunner.hasStopped); assert.isTrue(altRunner.hasStopped);
DocChange
DocChange encapsulates a change to a BaseModel instance. Used with BaseModel.onChange and other observe methods. The main properties are:
type
is either "add", "chg", "del"doc
is the document that has changed.undo
is: "del" whentype
is "add", "add" whentype
is "del". Whentype
is "chg"undo
is a change object that will undo the change.flag
is only present on client and a truthy value indicates change was not a simulation. Some truthy values include: "fromServer", "idbLoad", "simComplete" and "stopped" NOTE: DocChange should be treated immutable and not stored as it is recycled by koru. If you need to mutate or store the change use clone.
Properties
#changes | object | Retrieve the changes that were made to |
#isAdd | boolean | Is this change a add |
#isChange | boolean | Is this change from a {#.change |
#isDelete | boolean | Is this change from a delete |
#model | object | Retrieve the model of the |
#was | BaseModel | Retrieve the doc with the |
Methods
DocChange.add(doc, flag)
Create a model change representing an add.
Example
const DocChange = require("koru/model/doc-change");const doc = new Book({_id: 'book1', title: 'Animal Farm'}); const change = DocChange.add(doc, 'serverUpdate'); assert.isTrue(change.isAdd); assert.isFalse(change.isDelete); assert.isFalse(change.isChange); assert.same(change.type, 'add'); assert.same(change.doc, doc); assert.same(change.undo, 'del'); assert.same(change.flag, 'serverUpdate');
DocChange.change(doc, undo, flag)
Create a model change representing a change.
Example
const DocChange = require("koru/model/doc-change");DocChange.change(Model.Book("book1"), {title: "Fanimal Arm"}, "serverUpdate");// returns DocChange.change(Model.Book("book1"), {title: 'Fanimal Arm'}, 'serverUpdate')
DocChange.delete(doc, flag)
Create a model change representing a delete.
Example
const DocChange = require("koru/model/doc-change");DocChange.delete(Model.Book("book1"), "simComplete");// returns DocChange.delete(Model.Book("book1"), 'simComplete')
DocChange#clone()
Clone the change. This is a shallow copy of the change---doc and undo are assigned; not copied.
Parameters
Returns | DocChange |
Example
const DocChange = require("koru/model/doc-change");change = DocChange.change( new Book({_id: 'book1', title: 'Animal Farm'}), {title: 'Fanimal Arm'}); const copy = change.clone(); refute.same(copy, change); assert.same(copy.doc, change.doc); assert.same(copy.undo, change.undo); assert.same(copy.was, change.was); // was is cached
DocChange#hasField(field)
Test if a field has been changed.
Example
const DocChange = require("koru/model/doc-change");const dc = DocChange.change( new Book({_id: 'book1', title: 'Animal Farm', pages: 112}), {title: 'Fanimal Arm'}); assert.isTrue(dc.hasField('title')); assert.isFalse(dc.hasField('pages')); // does not need to be a Model document const add = DocChange.add({name: 'Simon'}); assert.isTrue(add.hasField('name')); assert.isFalse(add.hasField('location')); assert.isTrue(DocChange.delete({name: 'Simon'}).hasField('name')); const change = DocChange.change({name: 'Simon', location: 'home'}, {location: 'work'}); assert.isFalse(change.hasField('name')); assert.isTrue(change.hasField('location'));
DocChange#hasSomeFields(...fields)
Test if any of the fields
have been changed
Example
const DocChange = require("koru/model/doc-change");const dc = DocChange.change( new Book({ _id: 'book1', title: 'Animal Farm', Author: 'George Orwell', pages: 112, }), {title: 'Fanimal Arm', pages: 432}); assert.isTrue(dc.hasSomeFields('author', 'title')); assert.isTrue(dc.hasSomeFields('pages', 'title')); assert.isTrue(dc.hasSomeFields('author', 'pages')); assert.isFalse(dc.hasSomeFields('author')); assert.isFalse(dc.hasSomeFields('author', 'index'));
DocChange#*subDocKeys(field)
Create a iterator over the property names for each property that is different between two objects.
Example
const DocChange = require("koru/model/doc-change");const book = new Book({_id: 'book1', title: 'Animal Farm', index: { d: {dog: [123, 234], donkey: [56, 456]}, p: {pig: [3, 34]}, }}); const undo = Changes.applyAll(book.attributes, {index: { d: {dog: [123, 234]}, h: {horse: [23, 344]}, p: {pig: [3, 34]}, }}); change = DocChange.change(book, undo); assert.equals(Array.from(change.subDocKeys('index')).sort(), ['d', 'h']);
DocChange#*subDocs(field, flag)
Create a iterator over the DocChange
s for each property that is different between two
objects
Example
const DocChange = require("koru/model/doc-change");const book = new Book({_id: 'book1', title: 'Animal Farm', index: { d: {dog: [123, 234], donkey: [56, 456]}, p: {pig: [3, 34]}, }}); const undo = Changes.applyAll(book.attributes, {index: { d: {dog: [123, 234], deer: [34]}, h: {horse: [23, 344]}, p: {pig: [3, 34]}, }}); change = DocChange.change(book, undo); let count = 0; for (const dc of change.subDocs('index')) { if (dc._id === 'd') { ++count; assert.isTrue(dc.isChange); assert.same(dc.doc, book.index.d); assert.equals(dc.undo, {$partial: { deer: null, donkey: ['$replace', [56, 456]], }}); } else { ++count; assert.same(dc._id, 'h'); assert.isTrue(dc.isAdd); assert.same(dc.doc, book.index.h); } } assert.same(count, 2);
Model
MQFactory
Manage durable Message queues.
Methods
MQFactory#getQueue(name)
Get a message queue for current database
Example
const MQFactory = require("koru/model/mq-factory");mqFactory.registerQueue({module, name: 'foo', action: stub()}); const queue = mqFactory.getQueue('foo'); assert(queue); assert.same(mqFactory.getQueue('foo'), queue); assert.same(mqFactory.getQueue('bar'), undefined); dbBroker.db = v.altDb; const altQ = mqFactory.getQueue('foo'); refute.same(altQ, queue); dbBroker.db = v.defDb; assert.same(mqFactory.getQueue('foo'), queue);
MQFactory#registerQueue({module, name, action, retryInterval, local=false})
Register an action with a queue
Parameters
[module] | Module | unregister if module is unloaded. Not available if local is true |
name | string | the name of the queue |
action | function | the action to run when a queued message is ready |
[retryInterval] | number | number of ms to wait before retrying a failed action. Defaults to 60000. A value of -1 means don't retry |
[local] | defaults to false. If true register queue just the current database; otherwise register the queue for all current and future databases. |
Example
const MQFactory = require("koru/model/mq-factory");mqFactory.registerQueue({name: 'foo', action(msg) {doSomethingWith(msg)}}); mqFactory.registerQueue({ module, name: 'bar', retryInterval: -1, action(msg) {doSomethingWith(msg)}});
MQFactory#async start()
Start timers on all queues within current database with existing messages
Parameters
Returns | Promise(undefined) |
Example
const MQFactory = require("koru/model/mq-factory");const mQFactory = new MQFactory();mQFactory.start();// returns undefined
MQ
The class for queue instances.
Methods
MQ#async add({dueAt=util.newDate(), message})
Add a message to the queue. The message is persisted.
Parameters
dueAt | Date | |
message | object | the message to action |
Returns | Promise(undefined) |
Example
const MQFactory = require("koru/model/mq-factory");await queue.add({dueAt: new Date(now + 30), message: {my: 'message'}}); assert.calledWith(koru.setTimeout, m.func, 30); await queue.add({dueAt: new Date(now + 10), message: {another: 'message'}}); assert.calledOnceWith(koru.clearTimeout, 121); assert.calledWith(koru.setTimeout, m.func, 10); assert.equals(await v.defDb.query('select * from "_test_MQ" order by "dueAt"'), [{ _id: 2, name: 'foo', dueAt: new Date(now + 10), message: {another: 'message'}, }, { _id: 1, name: 'foo', dueAt: new Date(now + 30), message: {my: 'message'}, }]);
MQ#async peek(maxResults=1, dueBefore)
Look at messages at the front of the queue without removing them
Parameters
[maxResults] | number | the maximum number of messages to return. Defaults to 1. |
[dueBefore] | Date | |
Returns | Promise(Array) | an array of messages in queue order |
Example
const MQFactory = require("koru/model/mq-factory");await queue.add({dueAt: new Date(now + 30), message: {my: 'message'}}); await queue.add({dueAt: new Date(now + 10), message: {another: 'message'}}); assert.equals(await queue.peek(), [{ _id: 2, dueAt: new Date(now + 10), message: {another: 'message'}, }]); assert.equals(await queue.peek(3), [{ _id: 2, dueAt: new Date(now + 10), message: {another: 'message'}, }, { _id: 1, dueAt: new Date(now + 30), message: {my: 'message'}, }]); assert.equals(await queue.peek(5, new Date(now + 10)), [{ _id: 2, dueAt: new Date(now + 10), message: {another: 'message'}, }]);
MQ#async remove(_id)
Remove a message.
Parameters
_id | number | the id of the message to remove. |
Returns | Promise(number) |
Example
const MQFactory = require("koru/model/mq-factory");await queue.add({dueAt: new Date(now + 10), message: {my: 'message'}}); await queue.add({dueAt: new Date(now + 20), message: {another: 'message'}}); await queue.remove(2); assert.equals(await queue.peek(5), [m.field('_id', 1)]);
MQ#async sendNow()
Skip any retry interval and send the message now if dueAt has passed.
Parameters
Returns | Promise(undefined) |
Example
const MQFactory = require("koru/model/mq-factory");const queue = mqFactory.getQueue('foo'); v.action = async (args) => { throw {retryAfter: 12345}; }; await queue.add({message: [1, 2]}); await koru.setTimeout.yieldAndReset(); assert.calledWith(koru.setTimeout, m.func, 12345); koru.setTimeout.reset(); await queue.sendNow(); assert.calledWith(koru.setTimeout, m.func, 0);
PsSql
A Prepared query that is automatticaly named for reused on DB connections.
Methods
constructor(queryStr, model)
Create a prepared query.
Note: The query is lazily prepared including parsing the string and deriving parameter types.
Parameters
queryStr | string | The sql query string, with symbolic parameters, to prepare |
model | BaseModel | models used to resolve symbolic parameter types |
Example
const PsSql = require("koru/model/ps-sql");const bigBooks = new PsSql(`SELECT sum("pageCount") FROM "Book" WHERE "pageCount" > {$pageCount}`, Book); assert.equals(await bigBooks.fetchOne({pageCount: 300}), {sum: 1214});
PsSql#execute(params)
execute statement returning the tag count and closing the portal.
Parameters
params | object | |
Returns | Promise(number) |
Example
const PsSql = require("koru/model/ps-sql");const byAuthor = new PsSql(`UPDATE "Book" set "pageCount" = -1 WHERE "author" = {$author}`, Book); assert.equals(await byAuthor.execute({author: 'Dima Zales'}), 2); assert.equals(await new PsSql(`SELECT count(1) FROM "Book" WHERE "pageCount" = -1`, Book).value(), 2);
PsSql#fetch(params)
Fetch zero or more rows from the query and close the portal.
Parameters
params | object | |
Returns | Promise(Array) |
Example
const PsSql = require("koru/model/ps-sql");const byAuthor = new PsSql(`SELECT title FROM "Book" WHERE "author" = {$author} order by "pageCount"`, Book); assert.equals(await byAuthor.fetch({author: 'Dima Zales'}), [{title: 'Limbo'}, {title: 'Oasis'}]);
PsSql#fetchOne(params)
Fetch one or zero rows from the query and close the portal.
Parameters
params | object | |
Returns | Promise(object) |
Example
const PsSql = require("koru/model/ps-sql");const byAuthor = new PsSql(`SELECT title FROM "Book" WHERE "author" = {$author} order by "pageCount"`, Book); assert.equals(await byAuthor.fetchOne({author: 'Dima Zales'}), {title: 'Limbo'});
PsSql#async value(params, defValue)
Convience wrapper around fetchOne which returns one value from the row if found else the default value
Parameters
params | object | |
[defValue] | string | |
Returns | Promise(number) / Promise(string) |
Example
const PsSql = require("koru/model/ps-sql");const countByAuthor = new PsSql(`SELECT count(1) FROM "Book" WHERE "author" = {$author}`, Book); assert.equals(await countByAuthor.value({author: 'Dima Zales'}), 2); const bigBooks = new PsSql( `SELECT title FROM "Book" WHERE "pageCount" > {$pageCount} ORDER BY "pageCount" DESC`, Book); assert.equals(await bigBooks.value({pageCount: 3000}, 'No book is that big'), 'No book is that big'); assert.equals(await bigBooks.value({pageCount: 300}, 'No book is that big'), 'The Eye of the World');
Query
Database CRUD API.
Methods
Query.notify(docChange)
Notify observers of an update to a database record. This is called automatically but it is exposed here incase it needs to be called manually.
Parameters
docChange | DocChange |
Example
const Query = require("koru/model/query");Query.notify(DocChange.change(Model.TestModel("foo123", "foo"), {age: 1}, 'stopped'));Query.notify(DocChange.delete(Model.TestModel("foo123", "foo"), undefined));
Query.onAnyChange(callback)
Observe any change to any model.
Parameters
callback | function | is called a DocChange instance. |
Returns | object | contains a stop method to stop observering |
Example
const Query = require("koru/model/query");Query.onAnyChange(EMPTY_FUNC);// returns {callback: function stub(){}, stop: function (){}}
Query#exists()
Parameters
Returns | Promise(boolean) |
Example
const Query = require("koru/model/query");assert.same(await new Query(TestModel).exists(), true); assert.same(await new Query(TestModel).where({_id: 'notfound'}).exists(), false); assert.same(await new Query(TestModel).exists({_id: v.foo._id}), true);
Query#notExists()
Parameters
Returns | Promise(boolean) |
Example
const Query = require("koru/model/query");assert.same(await new Query(TestModel).notExists(), false); assert.same(await new Query(TestModel).where({_id: 'notfound'}).notExists(), true); assert.same(await new Query(TestModel).notExists({_id: v.foo._id}), false);
Query#onChange(callback)
Observe changes to documents matching query.
See Observable#add
Example
const Query = require("koru/model/query");const query = TestModel.query.where((doc) => doc.name.startsWith('F')); let ocdone = false; const oc = stub(async () => { await 1; ocdone = true; }); const handle = query.onChange(oc); const fred = await TestModel.create({name: 'Fred'}); assert.calledWith(oc, DocChange.add(fred)); assert.isTrue(ocdone); oc.reset(); ocdone = false; const emma = await TestModel.create({name: 'Emma'}); refute.called(oc); await emma.$update('name', 'Fiona'); assert.calledWith(oc, DocChange.add(emma.$reload())); assert.isTrue(ocdone); await emma.$update('name', 'Fi'); assert.calledWith(oc, DocChange.change(emma.$reload(), {name: 'Fiona'})); await fred.$update('name', 'Eric'); assert.calledWith(oc, DocChange.delete(fred.$reload())); /** stop cancels observer **/ handle.stop(); oc.reset(); await fred.$update('name', 'Freddy'); refute.called(oc);
Query#where(params, value)
Example
const Query = require("koru/model/query");const query = new Query();query.where("age", {$ne: 5});// returns Query(TestModel)query.where("age", {$nin: [5, 6]});// returns Query(TestModel)query.where({age: {$ne: 5}});// returns Query(TestModel)query.where("age", {$in: [10, 5]});// returns Query(TestModel)query.where("age", {$in: [5, 6]});// returns Query(TestModel)query.where("age", [5, 6]);// returns Query(TestModel)query.where("name", {$regex: "fo+$"});// returns Query(TestModel)query.where("name", {$regex: "FO", $options: "i"});// returns Query(TestModel)query.where("name", {$regex: /R$/i});// returns Query(TestModel)
Query#whereNot(params, value)
Add one or more where-nots to the query. If any where-not test matches then the query does not match record
Parameters
params | string / object | field or directive to match on. If is object then whereNot is called for each key. |
[value] | object / primitive | corresponding to |
Returns | Query |
Example
const Query = require("koru/model/query");const query = new Query();query.whereNot({age: 5});// returns Query(TestModel)query.whereNot("age", [5, 7]);// returns Query(TestModel)query.whereNot("age", [5, 10]);// returns Query(TestModel)query.whereNot("name", "foo");// returns Query(TestModel)
Query#whereSql(...args)
Add a where condition to the query which is written in sql.
Parameters
args | Four formats are supported:
|
Example
const Query = require("koru/model/query");assert.same((await TestModel.query.whereSql( `name = $1 and age > $2`, ['foo2', 3]).fetchOne()).name, 'foo2');assert.same((await TestModel.query.whereSql( `name = {$name} and age > {$age}`, {name: 'foo2', age: 3}).fetchOne()).name, 'foo2');const statement = new SQLStatement(`name = {$name} and age > {$age}`); assert.same((await TestModel.query.whereSql( statement, {name: 'foo2', age: 3}).fetchOne()).name, 'foo2');const name = 'foo2'; assert.same((await TestModel.query.whereSql`name = ${name} and age > ${3}`.fetchOne()).name, 'foo2');
QueryIDB
Support client side persistence using indexedDB
For testing one can use mockIndexedDB in replacement of indexedDB
Properties
#isIdle | boolean | true if db has completed initializing and is not closed and has no outstanding updates, otherwise false |
#isReady | boolean | true if db has completed initializing and is not closed, otherwise false |
Methods
constructor({name, version, upgrade, catchAll})
Open a indexedDB database
Parameters
name | string | the name of the database |
[version] | number | expected version of database |
[upgrade] | function |
|
[catchAll] |
Example
const QueryIDB = require("koru/model/query-idb");new QueryIDB({name: "foo", version: 2, upgrade: upgrade});
QueryIDB.deleteDatabase(name)
delete an entire database
Parameters
name | string | |
Returns | Promise(object) |
Example
const QueryIDB = require("koru/model/query-idb");let done = false; QueryIDB.deleteDatabase('foo').then(() => done = true); await db.whenIdle(); assert(done);
QueryIDB#close()
Close a database. Once closed it may not be used anymore.
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.close();
QueryIDB#count(modelName, query)
count records in a Model
Parameters
modelName | string | |
query | object | |
Returns | Promise(number) |
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.count("TestModel", {});// returns 3
QueryIDB#cursor(modelName, query, direction, action)
Open cursor on an ObjectStore
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.cursor("TestModel", {}, null, (cursor));
QueryIDB#delete(modelName, id)
Delete a record from indexedDB
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.delete("TestModel", "foo123");
QueryIDB#deleteObjectStore(name)
Drop an objectStore and its indexes
Parameters
name | string |
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.deleteObjectStore("TestModel");
QueryIDB#get(modelName, _id)
Find a record in a Model by its _id
Parameters
modelName | string | |
_id | string | |
Returns | Promise(object) |
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.get("TestModel", "foo123");// returns {_id: "foo123", name: "foo", age: 5, gender: "m"}
QueryIDB#getAll(modelName, query)
Find all records in a Model
Parameters
modelName | string | |
[query] | ||
Returns | Promise(Array) |
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.getAll("TestModel");// returns [{_id: 'foo123', age: 5, gender: 'm', name: 'foo'}, {_id: 'foo124', age: 10, gender: 'f', name: 'foo2'}]
QueryIDB#index(modelName, name)
Retreive a named index for an objectStore
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.index("TestModel", "name");// returns {_queryIDB: QueryIDB({name: 'foo', dbId: 'default', catchAll: undefined}), _withIndex: function (){}}queryIDB.index("TestModel", "name");// returns {_queryIDB: QueryIDB({name: 'foo', dbId: 'default', catchAll: undefined}), _withIndex: function (){}}queryIDB.index("TestModel", "name");// returns {_queryIDB: QueryIDB({name: 'foo', dbId: 'default', catchAll: undefined}), _withIndex: function (){}}queryIDB.index("TestModel", "name");// returns {_queryIDB: QueryIDB({name: 'foo', dbId: 'default', catchAll: undefined}), _withIndex: function (){}}queryIDB.index("TestModel", "name");// returns {_queryIDB: QueryIDB({name: 'foo', dbId: 'default', catchAll: undefined}), _withIndex: function (){}}queryIDB.index("TestModel", "name");// returns {_queryIDB: QueryIDB({name: 'foo', dbId: 'default', catchAll: undefined}), _withIndex: function (){}}
QueryIDB#loadDoc(modelName, rec)
Insert a record into a model but ignore #queueChange for same record and do nothing if record already in model unless model[stopGap$] symbol is true;
If record is simulated make from change from client point-of-view else server POV.
Example
const QueryIDB = require("koru/model/query-idb");called = false; after(v.TestModel.onChange(dc => { v.db.queueChange(dc); called = true; })); const rec = {_id: 'foo123', name: 'foo', age: 5, gender: 'm', $sim: ['del', undefined]}; db.loadDoc('TestModel', rec); await db.whenReady(); const {foo} = mockIndexedDB._dbs; const {foo123} = TestModel.docs; assert.same(foo123.attributes, rec); assert.same(rec.$sim, undefined); assert(called); assert.equals(v.simDocs(), {foo123: ['del', undefined]});
QueryIDB#loadDocs(n, recs)
Insert a list of records into a model. See loadDoc
Example
const QueryIDB = require("koru/model/query-idb");let called = false; TestModel.onChange(dc =>{db.queueChange(dc); called = true;}); const recs = [ {_id: 'foo123', name: 'foo', age: 5, gender: 'm'}, {_id: 'foo456', name: 'bar', age: 10, gender: 'f'}, ]; db.loadDocs('TestModel', recs); await db.whenReady(); const foo = v.idb._dbs.foo; assert.equals(TestModel.docs.foo123.attributes, recs[0]); assert.equals(TestModel.docs.foo456.attributes, recs[1]); assert.equals(foo._store.TestModel.docs, {}); assert(called);
QueryIDB#promisify(body)
perform a database action returning a promise
Example
const QueryIDB = require("koru/model/query-idb");const id = await db.promisify( ()=>db.transaction(['TestModel'], 'readwrite') .objectStore('TestModel').put({_id: "id1", name: "foo"}) ); assert.equals(id, "id1");
QueryIDB#put(modelName, rec)
Insert or update a record in indexedDB
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.put("TestModel", {_id: "foo123", name: "foo", age: 5, gender: "m"});
QueryIDB#queueChange(dc)
Queue a model change to update indexedDB when the current trans-queue successfully completes. Changes to model instances with stopGap$ symbol true are ignored.
Parameters
dc | DocChange |
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();{ v.foo = v.idb._dbs.foo; assert.same(v.foo._version, 2); after(v.TestModel.onChange(v.db.queueChange.bind(v.db))); v.f1 = v.TestModel.create({_id: 'foo123', name: 'foo', age: 5, gender: 'm'}); v.fIgnore = v.TestModel.createStopGap({ _id: 'fooIgnore', name: 'foo ignore', age: 10, gender: 'f'}); } await v.db.whenReady(); { refute(v.foo._store.TestModel.docs.fooIgnore); const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, { _id: 'foo123', name: 'foo', age: 5, gender: 'm', $sim: ['del', undefined]}); v.f1.$update('age', 10); } await v.db.whenReady(); { const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, { _id: 'foo123', name: 'foo', age: 10, gender: 'm', $sim: ['del', undefined]}); v.f1.$remove(); } await v.db.whenReady(); { const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, undefined); }await v.db.whenReady(); { v.foo = v.idb._dbs.foo; assert.same(v.foo._version, 2); after(v.TestModel.onChange(v.db.queueChange.bind(v.db)).stop); Query.insertFromServer(v.TestModel, {_id: 'foo123', name: 'foo', age: 5, gender: 'm'}); v.f1 = v.TestModel.findById('foo123'); } await v.db.whenReady(); { const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, {_id: 'foo123', name: 'foo', age: 5, gender: 'm'}); v.f1.$remove(); await v.db.whenReady(); } await v.db.whenReady(); { const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, {_id: 'foo123', $sim: [{ _id: 'foo123', name: 'foo', age: 5, gender: 'm'}, undefined]}); }await v.db.whenReady(); { v.foo = v.idb._dbs.foo; assert.same(v.foo._version, 2); after(v.TestModel.onChange(v.db.queueChange.bind(v.db)).stop); v.f1 = v.TestModel.create({_id: 'foo123', name: 'foo', age: 5, gender: 'm'}); v.fIgnore = v.TestModel.createStopGap({ _id: 'fooIgnore', name: 'foo ignore', age: 10, gender: 'f'}); } await v.db.whenReady(); { refute(v.foo._store.TestModel.docs.fooIgnore); const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, {_id: 'foo123', name: 'foo', age: 5, gender: 'm'}); v.f1.$update('age', 10); } await v.db.whenReady(); { const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, {_id: 'foo123', name: 'foo', age: 10, gender: 'm'}); v.f1.$remove(); await v.db.whenReady(); } await v.db.whenReady(); { const iDoc = v.foo._store.TestModel.docs.foo123; assert.equals(iDoc, undefined); }
QueryIDB#transaction(tables, mode, {oncomplete, onabort}={})
Access to indexeddb transaction
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.transaction("TestModel", "readwrite", {oncomplete: EMPTY_FUNC, onabort: EMPTY_FUNC});// returns {db: Database({_store: {TestModel: ObjectStore({db: {...more}, name: 'TestModel', docs: {...more}, indexes: {...more}})}, _mockdb: MockIndexedDB({_vers}
QueryIDB#whenIdle()
Return a promise that is resolved when the DB is ready and has commited all outstanding updates.
Parameters
Returns | Promise(QueryIDB) |
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.whenIdle();// returns null
QueryIDB#whenReady(noArgs)
Return a promise that is resolved when the DB is ready to query
Parameters
[noArgs] | ||
Returns | Promise(undefined) |
Example
const QueryIDB = require("koru/model/query-idb");const queryIDB = new QueryIDB();queryIDB.whenReady();// returns undefined
RichTextValidator
rich-Text validators.
Enable with Val.register(module, RichTextValidator) which is
conventionally done in app/models/model.js
Methods
RichTextValidator.richText(doc, field, options)
Validate field is rich text. Field error set to 'invalid_html'
if invalid.
Example
const RichTextValidator = require("koru/model/rich-text-validator");// Valid RichText Book.defineFields({ content: {type: 'text', richText: true}, }); const book = Book.build({content: RichText.fromHtml(Dom.h([{b: 'once'}, '\nupon']))}); assert(book.$isValid()); assert.equals(book.content, ['once\nupon', [11, 0, 0, 4]]); book.content = ['invalid', [22, -1]]; refute(book.$isValid()); assert.equals(book[error$].content, [['invalid_html']]);// valid text (string; not array) Book.defineFields({ content: {type: 'text', richText: true}, }); const book = Book.build({content: 'just\ntext'}); assert(book.$isValid()); assert.equals(book.content, 'just\ntext');// Valid RichText Book.defineFields({ content: {type: 'text', richText: true}, }); const book = Book.build({content: [['one', 'two'], null]}); assert(book.$isValid()); assert.equals(book.content, 'one\ntwo'); // converts array to plain text// only checks changes Book.defineFields({ content: {type: 'text', richText: true}, }); const book = Book.build(); book.attributes.content = [['one', 'two'], [-1, -1]]; assert(book.$isValid());// invalid markup Book.defineFields({ content: {type: 'text', richText: true}, }); const book = Book.build({content: [11, 2]}); refute(book.$isValid()); assert.equals(book[error$].content, [['invalid_html']]);// filter html to expected format Book.defineFields({ content: {type: 'text', richText: 'filter'}, }); const book = Book.build({content: RichText.fromHtml(Dom.h({ div: {ol: [{li: 'a'}, {li: 'b'}]}, style: 'text-align:right;'}))}); assert.equals(book.content, ['a\nb', [7, 0, 1, 1, 0, 1, 20, 0, 0, 20, 1, 0]]); assert(book.$isValid()); assert.equals(book.content, ['a\nb', [1, 0, 1, 20, 0, 0, 7, 0, 0, 20, 1, 0, 7, 0, 0]]); assert.equals(Dom.htmlToJson(RichText.toHtml(book.content[0], book.content[1])), [{ ol: [ {li: {style: 'text-align: right;', div: 'a'}}, {li: {style: 'text-align: right;', div: 'b'}}, ]}]);// filter no markup Book.defineFields({ content: {type: 'text', richText: 'filter'}, }); const book = Book.build({content: ['bold', null]}); assert(book.$isValid()); assert.equals(book.content, 'bold');
RichTextValidator.richTextMarkup(doc, field, options)
Validate field (suffixed with 'Markup'
) contains rich text markup. The corresponding
plain text should be in adjacent field named without suffix. An error is set for field
(with 'HTML'
suffix instead of 'Markup'
suffix) to 'invalid_html'
if invalid.
Example
const RichTextValidator = require("koru/model/rich-text-validator");// Valid Book.defineFields({ content: {type: 'text'}, contentMarkup: {type: 'text', richTextMarkup: true}, }); const rt = RichText.fromHtml(Dom.h([{b: 'once'}, '\nupon'])); const book = Book.build({content: rt[0], contentMarkup: rt[1]}); assert(book.$isValid()); assert.equals(book.content, 'once\nupon'); assert.equals(book.contentMarkup, [11, 0, 0, 4]); book.contentMarkup = [22, -1]; refute(book.$isValid()); assert.equals(book[error$].contentHTML, [['invalid_html']]);// filter html to expected format Book.defineFields({ content: {type: 'text'}, contentMarkup: {type: 'text', richTextMarkup: 'filter'}, }); const rt = RichText.fromHtml(Dom.h({ div: {ol: [{li: 'a'}, {li: 'b'}]}, style: 'text-align:right;'})); const book = Book.build({content: rt[0], contentMarkup: rt[1]}); assert(book.$isValid()); assert.equals(book.content, 'a\nb'); assert.equals(book.contentMarkup, [1, 0, 1, 20, 0, 0, 7, 0, 0, 20, 1, 0, 7, 0, 0]); assert.equals(Dom.htmlToJson(RichText.toHtml(book.content, book.contentMarkup)), [{ ol: [ {li: {style: 'text-align: right;', div: 'a'}}, {li: {style: 'text-align: right;', div: 'b'}}, ]}]);// only checks changes Book.defineFields({ content: {type: 'text'}, contentMarkup: {type: 'text', richTextMarkup: true}, }); const book = Book.build(); book.attributes.content = ['one', 'two']; book.attributes.contentMarkup = [-1, -1]; assert(book.$isValid());// only checks changes Book.defineFields({ content: {type: 'text'}, contentMarkup: {type: 'text', richTextMarkup: true}, }); const book = Book.build({content: ['one', 'two'], contentMarkup: null}); refute(book.$isValid()); assert.equals(book[error$].contentHTML, [['invalid_html']]); book.content = 'one\ntwo'; book.contentMarkup = book.attributes.contentMarkup; book.attributes.contentMarkup = [1, 0, 0, -3]; refute(book.$isValid()); assert.equals(book[error$].contentHTML, [['invalid_html']]);
SqlQuery
An optimized Model query using sql for where statement.
Note: all queries must be ran from withing a transaction
Methods
constructor(model, queryStr)
Create a prepared query.
Note: The query is lazily prepared including parsing the string and deriving parameter types.
Can also be called as Model#sqlWhere(queryStr)
Parameters
model | object | models used to resolve symbolic parameter types |
queryStr | string | The sql query string, with symbolic parameters, to prepare |
Example
const SqlQuery = require("koru/model/sql-query");const bigBooks = new SqlQuery(Book, `"pageCount" > {$pageCount} ORDER BY "pageCount"`); assert.same(await bigBooks.fetchOne({pageCount: 300}), await Book.findBy('title', 'Pride and Prejudice'));
SqlQuery#async fetch(params)
Fetch zero or more rows from the query and close the portal.
Parameters
params | object | |
Returns | Promise(Array) |
Example
const SqlQuery = require("koru/model/sql-query");const byAuthor = Book.sqlWhere(`"author" = {$author} ORDER BY "pageCount"`); assert.equals((await byAuthor.fetch({author: 'Dima Zales'})).map((d) => d.summary), ['Limbo by Dima Zales', 'Oasis by Dima Zales']);
SqlQuery#async fetchOne(params)
Fetch one or zero rows from the query and close the portal.
Parameters
params | object | |
Returns | Promise(BaseModel) |
Example
const SqlQuery = require("koru/model/sql-query");const byAuthor = Book.sqlWhere(`"author" = {$author} ORDER BY "pageCount"`); assert.equals(await byAuthor.fetchOne({author: 'Dima Zales'}), await Book.findBy('title', 'Limbo'));
SqlQuery#async forEach(params, callback)
call callback for each row returned from the query.
Parameters
params | object | |
callback | function | |
Returns | Promise(undefined) |
Example
const SqlQuery = require("koru/model/sql-query");const byAuthor = Book.sqlWhere(`"author" = {$author} ORDER BY "pageCount"`); const titles = []; await byAuthor.forEach({author: 'Dima Zales'}, (row) => titles.push(row.summary)); assert.equals(titles, ['Limbo by Dima Zales', 'Oasis by Dima Zales']);
SqlQuery#async *values(params)
return an asyncIterator over the rows returned from the query.
Example
const SqlQuery = require("koru/model/sql-query");const byAuthor = Book.sqlWhere(`author = {$author} ORDER BY "pageCount"`); const titles = []; for await (const row of byAuthor.values({author: 'Dima Zales'})) { titles.push(row.summary); } assert.equals(titles, ['Limbo by Dima Zales', 'Oasis by Dima Zales']);
TestFactory
TestFactory is used to facilitate building and persisting document models for
use within unit tests. A koru system should have a file app/test/factory.js
which defines the
models which TestFactory can use.
Model documents are created in the DB using, say, createBook
for a Book
or createAuthor
for an Author
and are built without saving using, say, buildBook
for a Book
. By default
documents bypass the validation when created but validation can be done using
Builder
Methods
TestFactory.defines(models)
Defines the models to build and create.
Parameters
models | object | Each function in |
Returns | TestFactory |
Example
const TestFactory = require("koru/model/test-factory");TestFactory.defines({ Author(options) { return new TestFactory.Builder('Author', options); }, Book(options) { return new TestFactory.Builder('Book', options) .genName('title') .addField('pages', 100) ; }, }); const book1 = TestFactory.buildBook(); assert.same(book1.title, 'Book 1'); assert.same(book1.author, void 0); assert.same(book1.pages, 100); const book2 = TestFactory.buildBook({pages: 200, author: 'A. A. Milne'}); assert.same(book2.author, 'A. A. Milne'); assert.same(book2.pages, 200);
TestFactory.generateSeq(key)
Generate a sequential number for a key
Example
const TestFactory = require("koru/model/test-factory");const builder = new TestFactory.Builder('Book', {title: 'Now We Are Six'}); builder .addField('title', 'When We Were Very Young') .addField('isbn', 9780140361230) .genSeq('pages', 'book-pages'); assert.equals(builder.defaults, {isbn: 9780140361230, pages: 1}); assert.same(TestFactory.generateSeq('book-pages'), 2); assert.same(TestFactory.generateSeq('book-chapters'), 1);
Builder
A Builder instance is responsible for constructing a model document. See defines
Methods
constructor(modelName, attributes, defaults={})
Create a Builder instance for the specified model.
Parameters
modelName | string | The name of the model to build. |
attributes | object | The field values to assign to the model |
defaults | object | The field values to use if the field is not in |
Example
const TestFactory = require("koru/model/test-factory");const builder = new TestFactory.Builder( 'Book', {title: 'Now We Are Six'}, {author: 'A. A. Milne'}) .addField('title', 'When We Were Very Young') .addField('pages', 112) ; assert.equals(builder.defaults, { author: 'A. A. Milne', pages: 112, }); assert.equals(builder.attributes, {title: 'Now We Are Six'}); assert.equals(builder.makeAttributes(), { author: 'A. A. Milne', pages: 112, title: 'Now We Are Six'});
Builder#addField(field, value)
Add a field to Builder.defaults
.
Parameters
field | string | The name of the field to add. |
value | string / function | The default value for the field. If value is a function the function will be executed if-and-only-if a default value is needed and its return result will be the value used. |
Returns | Builder |
Example
const TestFactory = require("koru/model/test-factory");const builder = new TestFactory.Builder('Book', {title: 'Now We Are Six'}); builder .addField('title', 'When We Were Very Young') .addField('isbn', () => Promise.resolve('9780140361230')) // addField waits for promises .addField('pages', () => builder.field('isbn').length * 10 - 18); await builder.waitPromises(); assert.equals(builder.defaults, {isbn: '9780140361230', pages: 112});
Builder#addPromise(p)
Add a promise to list of outstanding promises
Parameters
p | Promise(undefined) | |
Returns | Builder |
Example
const TestFactory = require("koru/model/test-factory");const list = []; const fut1 = new Future(); const builder = new TestFactory.Builder('Book') .addPromise(fut1.promise.then(() => {list.push(1)})) .addPromise(Promise.resolve().then(() => {list.push(2)})); const p = builder.waitPromises().then(() => {list.push(3)}); fut1.resolve(); assert.equals(list, []); await fut1.promise; assert.equals(list, [2, 1]); const fut2 = new Future(); await p; builder.addPromise(fut2.promise.then(() => {list.push(4)})); builder.addPromise(Promise.resolve().then(() => {fut2.resolve(); list.push(5)})); assert.equals(list, [2, 1, 3]); await builder.waitPromises(); assert.equals(list, [2, 1, 3, 5, 4]); assert.same(builder.waitPromises(), void 0);
Builder#addRef(ref, doc)
Add a default reference to another model.
Parameters
ref | string | |
[doc] | Promise(BaseModel) / function | The default document for the field. if-and-only-if a default value is needed then:
|
Returns | Builder |
Example
const TestFactory = require("koru/model/test-factory");const builder1 = new TestFactory.Builder('Chapter', {number: 1}) .addRef('book') ; builder1.addField('pages', () => TestFactory.last.book?.pages ?? 50); await builder1.waitPromises(); const {book} = TestFactory.last; assert.equals(builder1.defaults, {book_id: book._id, pages: 100}); let waitingForBook; // we are waiting on a lot of promises until we get the book builder1.addPromise(Promise.resolve().then(waitingForBook = Promise.resolve(book))); const builder2 = new TestFactory.Builder('Chapter', {book_id: void 0}) .addRef('book', waitingForBook) // addRef will wait for promise ; assert.equals(builder2.defaults, {}); const builder3 = new TestFactory.Builder('Chapter', {number: 1}) .addRef('book', () => TestFactory.createBook({_id: 'book123'})); await builder3.waitPromises(); assert.equals(builder3.defaults, {book_id: 'book123'});
Builder#build()
build an unsaved document. Called by, say, Factory.buildBook
.
Parameters
Returns | BaseModel |
Example
const TestFactory = require("koru/model/test-factory");const builder = new TestFactory.Builder('Book') .addField('title', 'Winnie-the-Pooh') ; const book = builder.build(); assert.equals(book.attributes, {}); assert.equals(book.changes, {title: 'Winnie-the-Pooh'});
Builder#create()
Create the document. Called by, say, Factory.createBook
. See constructor, useSave
Parameters
Returns | Promise(BaseModel) |
Example
const TestFactory = require("koru/model/test-factory");const builder = new TestFactory.Builder('Book') ; const book = await builder.create(); assert.equals(book.attributes, {_id: m.id});
Builder#genSeq(field, key)
Generate a sequential number for a field
and a key
Example
const TestFactory = require("koru/model/test-factory");const builder = new TestFactory.Builder('Book', {title: 'Now We Are Six'}); builder .addField('title', 'When We Were Very Young') .addField('isbn', 9780140361230) .genSeq('pages', 'book-pages'); assert.equals(builder.defaults, {isbn: 9780140361230, pages: 1});
Builder#useSave(value)
Determine if the normal Model save code should be run.
Parameters
value | boolean / string |
|
Returns | Builder |
Example
const TestFactory = require("koru/model/test-factory");intercept(Book.prototype, 'validate', function () { if (this.author === void 0) { this.author = 'Anon'; } }); { // no validation const builder = new TestFactory.Builder('Book') .useSave(false) // the default ; const book = await builder.create(); assert.equals(book.attributes, {_id: m.id}); } { // throw exception if invalid (useSave(true)) const builder = new TestFactory.Builder('Book') .useSave(true) ; await assert.exception( () => builder.create(), {error: 400, reason: {title: [['is_required']]}}); } { // force save (useSave('force')) const builder = new TestFactory.Builder('Book') .useSave('force') ; const book = await builder.create(); assert.equals(book.attributes, {_id: m.id, author: 'Anon'}); }
Builder#waitPromises()
Return a promise that whats for all addPromise
to complete
Parameters
Returns | Promise(undefined) |
Example
const TestFactory = require("koru/model/test-factory");const list = []; const fut1 = new Future(); const builder = new TestFactory.Builder('Book') .addPromise(fut1.promise.then(() => {list.push(1)})) .addPromise(Promise.resolve().then(() => {list.push(2)})); const p = builder.waitPromises().then(() => {list.push(3)}); fut1.resolve(); await p; assert.equals(list, [2, 1, 3]);
TransQueue
Run code within a transaction.
Methods
TransQueue.finally(callback)
Register a callback to be run when the transaction finishes either successfully or by aborting.
Parameters
callback | function | the function to run on transaction end. |
Example
const TransQueue = require("koru/model/trans-queue");const func1 = stub(); const func2 = stub(); try { TransQueue.transaction(() => { TransQueue.finally(func2); try { TransQueue.transaction(() => { TransQueue.finally(func1); refute.called(func1); // not called until outer transaction finishes throw 'abort'; }); } finally { assert.calledOnce(func1); refute.called(func2); } }); } catch (ex) { if (ex !== 'abort') throw ex; } assert.calledOnce(func1); assert.calledOnce(func2); func1.reset(); TransQueue.transaction(() => { TransQueue.finally(func1); }); assert.calledOnce(func1);
TransQueue.isInTransaction()
Test if currently in a transaction.
Parameters
Returns | boolean |
Example
const TransQueue = require("koru/model/trans-queue");assert.isFalse(TransQueue.isInTransaction()); TransQueue.transaction(() => { assert.isTrue(TransQueue.isInTransaction()); }); assert.isFalse(TransQueue.isInTransaction());assert.isFalse(TransQueue.isInTransaction()); await TransQueue.transaction(async () => { await 1; assert.isTrue(TransQueue.isInTransaction()); }); assert.isFalse(TransQueue.isInTransaction());
TransQueue.nonNested(db, body)
Ensure we a in a transaction. If a DB is supplied ensure we a wrapped in a DB transaction
Parameters
[db] | BaseModel / function | An optional dataBase client or Model to start transaction in |
[body] | Function / function | the function to run within the transaction |
Returns | Promise(undefined) |
Example
const TransQueue = require("koru/model/trans-queue");// No existing transaction await TransQueue.nonNested(TestModel, async () => { assert.isTrue(TransQueue.inTransaction); await TransQueue.nonNested(TestModel, () => { assert.same(isClient ? 0 : TestModel.db.existingTran.savepoint, 0); }); });// No wrapped DB transaction await TransQueue.transaction(async () => { assert.isTrue(TransQueue.inTransaction); await TransQueue.nonNested(TestModel, async () => { TransQueue.onSuccess(() => { assert.same(isClient ? 0 : TestModel.db.existingTran.savepoint, 0); }); assert.same(isClient ? 0 : TestModel.db.existingTran.savepoint, 0); await TransQueue.nonNested(TestModel, async () => { assert.same(isClient ? 0 : TestModel.db.existingTran.savepoint, 0); await TransQueue.nonNested(() => { assert.same(isClient ? 0 : TestModel.db.existingTran.savepoint, 0); }); }); }); });// Server Only: existing DB transaction await TestModel.db.transaction(async () => { await TransQueue.nonNested(TestModel, async () => { assert.same(TestModel.db.existingTran.savepoint, 0); }); });
TransQueue.onAbort(callback)
Register a callback to be run if the transaction aborts.
Parameters
callback | function | the function to run only if aborted. |
Example
const TransQueue = require("koru/model/trans-queue");const func1 = stub(); const func2 = stub(); try { TransQueue.transaction(() => { TransQueue.onAbort(func2); try { TransQueue.transaction(() => { try { TransQueue.onAbort(func1); throw 'abort'; } finally { refute.called(func1); // not called until outer transaction finishes } }); } catch (err) { throw err; } finally { assert.calledOnce(func1); refute.called(func2); } }); } catch (ex) { if (ex !== 'abort') throw ex; } assert.calledOnce(func1); assert.calledOnce(func2); func1.reset(); TransQueue.transaction(() => { TransQueue.onAbort(func1); }); refute.called(func1);
TransQueue.onSuccess(callback)
Register a callback to be run if the transaction finishes successfully or if no transaction is running.
Parameters
callback | function | the function to run on success. |
Example
const TransQueue = require("koru/model/trans-queue");const func1 = stub(); const func2 = stub(); TransQueue.transaction(() => { TransQueue.onSuccess(func2); TransQueue.transaction(() => { TransQueue.onSuccess(func1); refute.called(func1); // not called until outer transaction finishes }); assert.calledOnce(func1); refute.called(func2); }); return; assert.calledOnce(func2); func1.reset(); TransQueue.onSuccess(func1); // called now since no transaction assert.calledOnce(func1); func1.reset(); try { TransQueue.transaction(() => { TransQueue.onSuccess(func1); throw 'abort'; }); } catch (ex) { if (ex !== 'abort') throw ex; } refute.called(func1); // not called since transaction aborted
TransQueue.transaction(db, body)
Start a transaction. On server the transaction is linked to util.thread
and a DB
transaction can also be attached. On client only one transaction can be running.
Parameters
[db] | Client | an optional database connection which has a
|
[body] | the function to run within the transaction. |
Example
const TransQueue = require("koru/model/trans-queue");let inTrans = false; TransQueue.transaction(() => { inTrans = TransQueue.isInTransaction(); }); assert.isTrue(inTrans);
Val
Utilities to help validate models.
Methods
Val.addError(doc, field, ...args)
Add an error to an object; usually a model document.
Example
const Val = require("koru/model/validation");const doc = {}; Val.addError(doc, 'foo', 'is_too_big', 400); Val.addError(doc, 'foo', 'is_wrong_color', 'red'); assert.equals(doc[error$], {foo: [['is_too_big', 400], ['is_wrong_color', 'red']]});
Val.assertDocChanges(doc, spec, new_spec=ID_SPEC)
Assert doc changes are allowed.
Example
const Val = require("koru/model/validation");spy(Val, 'assertCheck'); const existing = {changes: {name: 'new name'}, $isNewRecord() {return false}}; Val.assertDocChanges(existing, {name: 'string'}); assert.calledWithExactly(Val.assertCheck, existing.changes, {name: 'string'}); Val.assertCheck.reset(); const newDoc = {changes: {_id: '123', name: 'new name'}, $isNewRecord() {return true}}; Val.assertDocChanges(newDoc, {name: 'string'}); assert.calledWithExactly( Val.assertCheck, newDoc.changes, {name: 'string'}, {altSpec: {_id: 'id'}}); Val.assertCheck.reset(); Val.assertDocChanges(newDoc, {name: 'string'}, {_id: 'any'}); assert.calledWithExactly( Val.assertCheck, newDoc.changes, {name: 'string'}, {altSpec: {_id: 'any'}}); TH.noInfo(); Val.assertCheck.reset(); assert.accessDenied(() => { Val.assertDocChanges(newDoc, {name: 'string'}, null); }); refute.called(Val.assertCheck);
Val.register(module, map)
Register one or more collections of validators. This is conventionally done in
app/models/model.js
Parameters
module | Module | |
map | object | a collection of validation functions or a collection of collection of validation functions. |
Example
const Val = require("koru/model/validation");stub(module, 'onUnload'); Val.register(module, {TextValidator, RequiredValidator}); Val.register(module, InclusionValidator); assert.isFunction(Val.validators('normalize')); assert.isFunction(Val.validators('inclusion')); assert.isFunction(Val.validators('required')); module.onUnload.yieldAll(); assert.same(Val.validators('normalize'), undefined); assert.same(Val.validators('inclusion'), undefined); assert.same(Val.validators('required'), undefined);
Error
Methods
Error.msgFor(doc, field, other_error)
Extract the translated error message for a field
Example
const Val = require("koru/model/validation");const doc = {[error$]: {foo: [['too_long', 34]]}}; assert.same(Val.Error.msgFor(doc, 'foo'), '34 characters is the maximum allowed'); assert.same(Val.Error.msgFor(doc[error$], 'foo'), '34 characters is the maximum allowed'); assert.same(Val.Error.msgFor('error one'), 'error one'); assert.same(Val.Error.msgFor([['error 1'], 'error 2']), 'error 1, error 2');
Error.toString(doc)
Translate all errors
Example
const Val = require("koru/model/validation");const doc = {[error$]: {foo: [['too_long', 34]], bar: [['is_invalid']]}}; assert.same(Val.Error.toString(doc), 'foo: 34 characters is the maximum allowed; ' + 'bar: is not valid');
Overview
The validators restrict the values of fields in a Model; either by setting an error or converting the field values. Validators are invoked when a Model document is saved or BaseModel#$isValid() is called.
See Val
AssociatedValidator
Validate the association between two model documents.
Enable with Val.register(module, AssociatedValidator) which is conventionally
done in app/models/model.js
Methods
AssociatedValidator.associated(doc, field, options, {type, changesOnly, model})
Ensure field contains the id of another model document given certain constraints.
Duplicates are not allowed. Ids will be arranged in ascending order.
addAuthors(['a123', 'a789']); Book.defineFields({ author_ids: {type: 'has_many', associated: true}, }); const book = Book.build({author_ids: ['a123', 'a789', 'a123']}); refute(book.$isValid()); assert.equals(book[error$].author_ids, [['duplicates']]); assert.equals(book.author_ids, ['a123', 'a123', 'a789']);
If filter
is true duplicates will be removed
addAuthors(['a123', 'a789']); Book.defineFields({ author_ids: {type: 'has_many', associated: {filter: true}}, }); const book = Book.build({author_ids: ['a123', 'a789', 'a123']}); assert(book.$isValid()); assert.equals(book.author_ids, ['a123', 'a789']);
Parameters
doc | BaseModel | |
field | string | |
options | boolean / object |
|
type | string | When the field
Otherwise the field is expected to be an array of ids
|
[changesOnly] | boolean | When this
|
[model] | BaseModel | A
|
Returns | Promise(undefined) |
Example
const AssociatedValidator = require("koru/model/validators/associated-validator");addAuthors(['a123', 'a789']); Book.defineFields({ author_ids: {type: 'has_many', associated: true}, }); const book = Book.build({author_ids: ['a123', 'a456', 'a789']}); refute(book.$isValid()); assert.equals(book[error$].author_ids, [['not_found']]);addAuthorsAsync(['a123', 'a789']); Book.defineFields({ author_ids: {type: 'has_many', associated: true}, }); const book = Book.build({author_ids: ['a123', 'a456', 'a789']}); refute(await book.$isValid()); assert.equals(book[error$].author_ids, [['not_found']]);// is_invalid Book.defineFields({ author_ids: {type: 'has_many', associated: true}, }); const book = Book.build({author_ids: 'a123'}); // not an array refute(book.$isValid()); assert.equals(book[error$].author_ids, [['is_invalid']]);
LengthValidator
Validate the length of an object.
Enable with Val.register(module, LengthValidator) which is conventionally done
in app/models/model.js
Methods
LengthValidator.maxLength(doc, field, length)
Ensure field is not greater than length.
Example
const LengthValidator = require("koru/model/validators/length-validator");Book.defineFields({ title: {type: 'text', maxLength: 10} }); const book = Book.build({title: 'Animal Farm'}); refute(book.$isValid()); assert.equals(book[error$].title, [["too_long", 10]]);
TextValidator
Text validators and convertors.
Enable with Val.register(module, TextValidator) which is conventionally done
in app/models/model.js
Methods
TextValidator.boolean(doc, field, boolType)
Ensure field is a boolean. Strings of trimmed lower case 'true'
, 'on'
, '1'
and 't'
are converted to true
. Strings of trimmed lower case 'false'
, 'off'
, '0'
and 'f'
are converted to false
. If converted value is not of type boolean
, undefined
or
null
a 'not_a_boolean'
error is added.
Example
const TextValidator = require("koru/model/validators/text-validator");// trueOnly Book.defineFields({ inPrint: {type: 'boolean', boolean: 'trueOnly'} }); const book = Book.build({inPrint: false}); assert(book.$isValid()); assert.same(book.inPrint, void 0); book.inPrint = true; assert(book.$isValid()); assert.same(book.inPrint, true);// accepted true values Book.defineFields({ inPrint: {type: 'boolean', boolean: true} }); const book = Book.build(); for (const val of ['trUe ', 'T', ' 1', 'on', true]) { book.inPrint = val; assert(book.$isValid()); assert.isTrue(book.inPrint); }// accepted false values Book.defineFields({ inPrint: {type: 'boolean', boolean: true} }); const book = Book.build(); for (const val of [' FALSE ', 'f', ' 0', 'off', false]) { book.inPrint = val; assert(book.$isValid()); assert.isFalse(book.inPrint); }// invalid values Book.defineFields({ inPrint: {type: 'boolean', boolean: true} }); const book = Book.build(); for (const val of [' FALS ', 'tru', ' ', 0, 1]) { book.inPrint = val; refute(book.$isValid()); assert.same(book.inPrint, val); assert.equals(book[error$]['inPrint'],[['not_a_boolean']]); }// null or undefined Book.defineFields({ inPrint: {type: 'boolean', boolean: true} }); const book = Book.build({inPrint: null}); assert(book.$isValid()); assert.same(book.inPrint, void 0); book.inPrint = void 0; assert(book.$isValid()); assert.same(book.inPrint, void 0);
TextValidator.normalize(doc, field, options)
Ensure text is in a normalized form
Parameters
doc | BaseModel | |
field | string | |
options | string |
|
Example
const TextValidator = require("koru/model/validators/text-validator");Book.defineFields({ title: {type: 'text', normalize: 'downcase'} }); const book = Book.build({title: 'Animal Farm'}); assert(book.$isValid()); assert.same(book.title, 'animal farm');Book.defineFields({ title: {type: 'text', normalize: 'upcase'} }); const book = Book.build({title: 'Animal Farm'}); assert(book.$isValid()); assert.same(book.title, 'ANIMAL FARM');Book.defineFields({ title: {type: 'text', normalize: 'upcase'} }); const book = Book.build({title: 12345}); refute(book.$isValid()); assert.same(book.title, 12345); assert.equals(book[error$].title,[['not_a_string']]); book.title = null; assert(book.$isValid());
ValidateValidator
Validate field with custom validation function.
Enable with Val.register(module, ValidateValidator) which is conventionally done
in app/models/model.js
Methods
ValidateValidator.validate(doc, field, validator)
Run validator
on field
(if changed)
Parameters
doc | BaseModel | |
field | string | |
validator | function | A function which has the document as |
Returns | Promise(undefined) | an optional string which is an error message if invalid. The message will be added
to the documents errors for the |
Example
const ValidateValidator = require("koru/model/validators/validate-validator");Book.defineFields({ ISBN: {type: 'text', validate(field) { // normalize and validate the ISBN const val = this[field]; if (typeof val === 'string') { const norm = val.replace(/-/g, ''); if (norm.length == 13) { if (checkDigit13Valid(norm)) { if (norm !== val) this[field] = norm; return; // is valid } } } return 'is_invalid'; }}, }); const book = Book.build({ISBN: '978-3-16-148410-0'}); assert(book.$isValid()); assert.equals(book.ISBN, '9783161484100'); book.ISBN = '222-3-16-148410-0'; refute(book.$isValid()); assert.equals(book[error$].ISBN, [['is_invalid']]); assert.equals(book.ISBN, '222-3-16-148410-0');
ModuleGraph
Methods
ModuleGraph.findPath(start, goal)
Finds shortest dependency path from one module to another module that it (indirectly) requires.
Parameters
start | Module | the module to start from |
goal | Module | the module to look for |
Returns | [Module,...] | from |
Example
const ModuleGraph = require("koru/module-graph");ModuleGraph.findPath({Module:koru/module-graph-test}, {Module:koru/util-base});// returns [{Module:koru/module-graph-test}, {Module:koru/main}, {Module:koru/util}, {Module:koru/util-base}]
ModuleGraph.isRequiredBy(supplier, user)
Test if supplier
is required by user
.
Parameters
supplier | Module | the module to look for |
user | Module | the module to start from |
Returns | boolean | true if path found from user to supplier |
Example
const ModuleGraph = require("koru/module-graph");ModuleGraph.isRequiredBy({Module:koru/util-base}, {Module:koru/module-graph-test});// returns trueModuleGraph.isRequiredBy({Module:koru/module-graph-test}, {Module:koru/util-base});// returns false
Mutex
Mutex implements a semaphore lock.
Properties
#isLocked | boolean | true if mutex is locked |
#isLockedByMe | boolean | true if mutex is locked by the current thread. On client there is only one thread so same result as |
Methods
constructor()
Construct a Mutex.
Example
const Mutex = require("koru/mutex");const mutex = new Mutex(); let counter = 0; try { await mutex.lock(); koru.runFiber(async () => { await mutex.lock(); assert.isTrue(mutex.isLocked); assert.isTrue(mutex.isLockedByMe); koru.runFiber(() => { assert.isTrue(mutex.isLocked); isServer && assert.isFalse(mutex.isLockedByMe); }); counter = 1; mutex.unlock(); assert.isTrue(mutex.isLocked); isServer && assert.isFalse(mutex.isLockedByMe); }); } finally { assert.isTrue(mutex.isLocked); assert.isTrue(mutex.isLockedByMe); mutex.unlock(); } await mutex.lock(); try { assert.same(counter, 1); } finally { mutex.unlock(); } assert.isFalse(mutex.isLocked); assert.isFalse(mutex.isLockedByMe);
Mutex#lock()
Aquire a lock on the mutex. Will pause until the mutex is unlocked
Example
const Mutex = require("koru/mutex");const mutex = new Mutex(); assert.isFalse(mutex.isLocked); assert.isFalse(mutex.isLockedByMe); await mutex.lock(); assert.isTrue(mutex.isLocked); assert.isTrue(mutex.isLockedByMe);
Mutex#unlock()
Release a lock on the mutex. Will allow another fiber to aquire the lock
Example
const Mutex = require("koru/mutex");const mutex = new Mutex(); await mutex.lock(); assert.isTrue(mutex.isLocked); mutex.unlock(); assert.isFalse(mutex.isLocked); assert.isFalse(mutex.isLockedByMe);
Observable
An observeable object. Observable keeps track of subjects and notifies all of them if asked. An Observable instance is iteratable.
See also makeSubject
Methods
constructor(allStopped)
Make an instance of Observable
Parameters
[allStopped] | function | method will be called when all observers have stopped. |
Example
const Observable = require("koru/observable");const allStopped = stub(); const subject = new Observable(allStopped); const observer1 = stub(), observer2 = stub(); const handle1 = subject.add(observer1); const handle2 = subject.add(observer2); handle1.stop(); refute.called(allStopped); handle2.stop(); assert.called(allStopped);
Observable#add(callback)
add an observer a subject
Aliases
onChange
Parameters
callback | function | called with the arguments sent by |
Returns | object | a handle that has the methods
|
Example
const Observable = require("koru/observable");const subject = new Observable(); const observer1 = stub(), observer2 = stub(); const handle1 = subject.add(observer1); const handle2 = subject.add(observer2); assert.same(handle1.callback, observer1); subject.notify(123, 'abc'), assert.calledWith(observer1, 123, 'abc'); assert.calledWith(observer2, 123, 'abc'); handle1.stop(); subject.notify('call2'); refute.calledWith(observer1, 'call2'); assert.calledWith(observer2, 'call2');
Observable#forEach(callback)
visit each observer
Parameters
callback | function |
Example
const Observable = require("koru/observable");const subject = new Observable(); const observer1 = stub(), observer2 = stub(); const exp = [ m.is(subject.add(observer1)), m.is(subject.add(observer2)), ]; const ans = []; subject.forEach((h) => {ans.push(h)}); assert.equals(ans, exp);
Observable#notify(...args)
Notify all observers. Observers are notified in order they were added; first added, first notified. If any of the observers return a promise then notify will wait for it to resolve before calling the next observer and will return a promise that will resolve once all observers have completed their callbacks.
Parameters
args | ...any-type | arguments to send to observers (see add) |
Returns | any-type | the first argument. Wrapped in a promise if any observers are async. |
Example
const Observable = require("koru/observable");const subject = new Observable(); const observer1 = stub(), observer2 = stub(); subject.add(observer1); subject.add(observer2); assert.same( subject.notify(123, 'abc'), 123, ); assert.calledWith(observer1, 123, 'abc'); assert.calledWith(observer2, 123, 'abc'); assert(observer1.calledBefore(observer2));const subject = new Observable(); const stub1 = stub(), stub2 = stub(); subject.add(async (a, b) => {await 1; stub1(a, b)}); subject.add(async (a, b) => {stub2(a, b); await 2}); const ans = subject.notify(123, 'abc'); refute.called(stub1); assert(isPromise(ans)); assert.same( await ans, 123, ); assert.calledWith(stub1, 123, 'abc'); assert.calledWith(stub2, 123, 'abc'); assert(stub1.calledBefore(stub2));
Observable#stopAll()
Stop all observers
Example
const Observable = require("koru/observable");const subject = new Observable(); const observer1 = stub(), observer2 = stub(); subject.add(observer1); subject.add(observer2); subject.stopAll(); subject.notify(123, 'abc'); refute.called(observer1); refute.called(observer2);
HtmlParser
HTML parsing helpers
Methods
HtmlParser.parse(code, { filename='<anonymous>', onopentag=nop, ontext=nop, oncomment=nop, onclosetag=nop, }={})
Parse a string of HTML markup calling the callbacks when matched. All callbacks are passed
code
, spos
, epos
where spos
and epos
are indexes marking the correspoinding slice
of code
.
Note: Not all markup errors will throw an exception.
Parameters
code | string | the HTML markup to parse |
[filename] | string | A filename to use if a markup error is discovered. |
onopentag | function | called when a start tag is found.
|
ontext | function | called when plain text is found
|
oncomment | function | called when a comment is found
|
onclosetag | function | called when a tag scope has concluded (even when no end tag)
|
Example
const HtmlParser = require("koru/parse/html-parser");const code = ` <div id="top" class="one two" disabled> <input name="title" value=" "hello"> <!-- I'm a comment --> <p> <b>bold</b> <p> <br> <span>content <inside></span> <!-- <p> comment 2 --> </div> `; const mapAttrs = (attrs) => { let ans = ''; for (const n in attrs) ans += ' ' + (attrs[n] === n ? n : `${n}="${attrs[n]}"`); return ans; }; let result = ''; const tags = [], comments = []; let level = 0; HTMLParser.parse(code, { onopentag: (name, attrs, code, spos, epos) => { tags.push(util.isObjEmpty(attrs) ? [++level, name] : [++level, name, attrs]); result += code.slice(spos, epos); }, ontext: (code, spos, epos) => { result += code.slice(spos, epos); }, oncomment: (code, spos, epos) => { const text = code.slice(spos, epos); comments.push(text); result += text; }, onclosetag: (name, type, code, spos, epos) => { --level; tags.push(['end ' + name, type]); if (type === 'end') { result += code.slice(spos, epos); } }, }); assert.equals(tags, [ [1, 'div', {id: 'top', class: 'one two', disabled: ''}], [2, 'input', {name: 'title', value: '\n"hello'}], ['end input', 'self'], [2, 'p'], [3, 'b'], ['end b', 'end'], ['end p', 'missing'], [2, 'p'], [3, 'br'], ['end br', 'self'], [3, 'span'], ['end span', 'end'], ['end p', 'missing'], ['end div', 'end'], ]); assert.equals(comments, ["<!-- I'm a comment -->", '<!--\n <p>\n comment 2\n -->']); assert.equals(code, result);
JsPrinter
Methods
constructor({input, write, ast=parse(input)})
Create a new javascript printer
Example
const JsPrinter = require("koru/parse/js-printer");const example = () => { return 1+2; }; let output = ''; const simple = new JsPrinter({ input: example.toString(), write: (token) => { output += token; }, }); simple.print(simple.ast); simple.catchup(simple.ast.end); assert.same(output, example.toString());
pg
Interface to PostgreSQL.
Config | |
url | The default url to connect to; see libpq - Connection |
Properties
defaultDb | Client | Return the default Client database connection. |
#connectionCount | The number of connections to database server |
Methods
pg.connect(url, name)
Create a new database Client connected to the url
Parameters
url | string | |
[name] | string | The name to give to the connection. By default it is the schema name. |
Returns | Client |
Example
const pg = require("koru/pg/driver");pg.connect("host=/var/run/postgresql dbname=korutest options='-c search_path=public,pg_catalog'");// returns Pg.Driver("undefined")pg.connect("postgresql://localhost/korutest", "conn2");// returns Pg.Driver("conn2")
Client
A connection to a database.
See pg.connect
Properties
#inTransaction | boolean | determine if client is in a transaction |
Methods
constructor(url, name, formatOptions)
Example
const pg = require("koru/pg/driver");const Client = pg.defaultDb.constructor; const client = new Client('host=/var/run/postgresql dbname=korutest', 'public'); const client2 = new Client(undefined, 'my name'); assert.same(client._url, 'host=/var/run/postgresql dbname=korutest'); assert.same(client.name, 'public'); assert.same(client2.name, 'my name');
Client#async explainQuery(...args)
Run an EXPLAIN ANALYZE on given query and return result text.
Parameters
args | string | |
Returns | Promise(string) |
Example
const pg = require("koru/pg/driver");const client = pg.defaultDb;client.explainQuery("SELECT {$a}::int + {$b}::int as ans", {a: 1, b: 2});// returns Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.000..0.000 rows=1 loops=1) Planning Time: 0.007 ms Execution Time: 0.003 ms
Client#jsFieldToPg(col, type, conn)
Example
const pg = require("koru/pg/driver");await pg.defaultDb.withConn((conn) => { assert.equals(pg.defaultDb.jsFieldToPg('foo', 'text'), '"foo" text'); assert.equals(pg.defaultDb.jsFieldToPg('foo', 'id'), '"foo" text collate "C"'); assert.equals(pg.defaultDb.jsFieldToPg('foo', 'color'), '"foo" text collate "C"'); assert.equals(pg.defaultDb.jsFieldToPg('foo', 'belongs_to'), '"foo" text collate "C"'); assert.equals(pg.defaultDb.jsFieldToPg('foo', 'has_many'), '"foo" text[] collate "C"'); assert.equals(pg.defaultDb.jsFieldToPg('runs', 'number'), '"runs" double precision'); assert.equals(pg.defaultDb.jsFieldToPg('name'), '"name" text'); assert.equals(pg.defaultDb.jsFieldToPg('dob', {type: 'date'}), '"dob" date'); assert.equals( pg.defaultDb.jsFieldToPg('map', {type: 'object', default: {treasure: 'lost'}}), `"map" jsonb DEFAULT '{"treasure":"lost"}'::jsonb`); });
Client#query(...args)
Query the database with a SQL instruction. Five formats are supported (most performant first):
query(text)
where no parameters are in the query textquery(text, params)
where parameters correspond to array position (1 is first position)query(sqlStatment, params)
wheresqlStatment
is a pre-compiled SQLStatement and params is a key-value object.query(text, params)
where params is a key-value objectquery`queryTemplate`
Aliases
exec
Parameters
args | string / Array / SQLStatement | |
Returns | [object] | a list of result records |
Example
const pg = require("koru/pg/driver");const a = 3, b = 2; assert.equals( (await pg.defaultDb.query(`SELECT {$a}::int + {$b}::int as ans`, {a, b}))[0].ans, 5); assert.equals( (await pg.defaultDb.query(`SELECT $1::int + $2::int as ans`, [a, b]))[0].ans, 5); assert.equals( (await pg.defaultDb.query`SELECT ${a}::int + ${b}::int as ans`)[0].ans, 5); const statment = new SQLStatement(`SELECT {$a}::int + {$b}::int as ans`); assert.equals( (await pg.defaultDb.query(statment, {a, b}))[0].ans, 5);
Client#timeLimitQuery(...args)
Same as query but limit to time the query can run for. This method will wrap the query in a transaction/savepoint. Does not support queryTemplate.
Parameters
args | string | |
Returns | Promise(Array) / Promise(undefined) |
Example
const pg = require("koru/pg/driver");try { assert.same((await pg.defaultDb.timeLimitQuery(`SELECT 'a' || $1 as a`, ['b'], {}))[0].a, 'ab'); const ans = await pg.defaultDb.timeLimitQuery( `SELECT pg_sleep($1)`, [1.002], {timeout: 1, timeoutMessage: 'My message'}); assert.fail('Expected timeout '); } catch (e) { if (e.error !== 504) throw e; assert.same(e.reason, 'My message'); }
SQLStatement
A SQL statement with embedded parameters pre compiled to improve efficiency. Not to be confused with an SQL prepared statement; this is a client side optimization only.
Properties
#text | string | The converted query text with inserted numeric parameters; changes when convertArgs is called. |
Methods
constructor(text='')
Compile a SQLStatement.
Parameters
text | string | a SQL statement with embedded parameters in the form |
Example
const SQLStatement = require("koru/pg/sql-statement");const statment = new SQLStatement( `SELECT {$foo}::int+{$bar}::int as a, {$foo}::text || '0' as b`); assert.equals((await Driver.defaultDb.query(statment, {foo: 10, bar: 5}))[0], {a: 15, b: '100'}); assert.equals((await Driver.defaultDb.query(new SQLStatement('select 1 as a')))[0], {a: 1});
SQLStatement#append(value)
Append an SQLStatement to this SQLStatement. Converts this statement.
Parameters
value | SQLStatement | the statement to append. |
Returns | SQLStatement | this statement. |
Example
const SQLStatement = require("koru/pg/sql-statement");const s1 = new SQLStatement(`SELECT {$foo}::int`); const s2 = new SQLStatement(', {$bar}::int+{$foo}::int'); assert.same(s1.append(s2), s1); assert.equals(s1.text, 'SELECT $1::int, $2::int+$1::int');
SQLStatement#appendText(text)
Append text to this SQLStatement. Converts this statement.
Parameters
text | string | |
Returns | SQLStatement | this statement. |
Example
const SQLStatement = require("koru/pg/sql-statement");const s1 = new SQLStatement(`SELECT {$foo}::int`); assert.same(s1.appendText(', 123'), s1); assert.equals(s1.text, 'SELECT $1::int, 123');
SQLStatement#clone()
Clone this SQLStatement.
Parameters
Returns | SQLStatement |
Example
const SQLStatement = require("koru/pg/sql-statement");const s1 = new SQLStatement(`SELECT {$foo}::int`); const s2 = s1.clone(); s2.convertArgs({foo: 1}, [1, 2]); assert.equals(s1.text, 'SELECT $1::int'); assert.equals(s2.text, 'SELECT $3::int');
SQLStatement#convertArgs(params, initial=[])
Convert key-value param to a corresponding paramter array for this statement. If an
initial
parameter is supplied then parameters will be appended to that array. This
determines what $n
parameters will be returned in the #text
property.
Parameters
params | object | |
[initial] | Array | An existing list of parameters. Defaults to empty list |
Returns | Array |
|
Example
const SQLStatement = require("koru/pg/sql-statement");const statment = new SQLStatement(`SELECT {$foo}::int+{$bar}::int as a, {$foo}::text || '0' as b`); assert.equals(statment.convertArgs({foo: 10, bar: 9}), [10, 9]); assert.equals(statment.text, "SELECT $1::int+$2::int as a, $1::text || '0' as b"); assert.equals(statment.convertArgs({foo: 30, bar: 40}, [1, 2]), [1, 2, 30, 40]); assert.equals(statment.text, "SELECT $3::int+$4::int as a, $3::text || '0' as b");
PromiseQueue
Queue callbacks to happen after previous queued callbacks using promises.
Methods
constructor()
Construct a PromiseQueue
Example
const PromiseQueue = require("koru/promise-queue");const queue = new PromiseQueue(); let i = 4; queue.add(()=>{ i = i * 2}); queue.add(()=>{ ++i}); assert.same(i, 4); await queue.empty(); assert.same(i, 9);
PromiseQueue#add(callback)
add a callback to the queue. Callback is called even if a previous callback throws an exception. The previous callbacks output is available as an argument to this callback.
Parameters
callback | function |
Example
const PromiseQueue = require("koru/promise-queue");const queue = new PromiseQueue(); let i = 4; queue.add(()=>{ throw "err"}); queue.add((err) => i += err === "err" ? 2 : -2); queue.add(()=>{ ++i}); await queue.empty(); assert.same(i, 7);
PromiseQueue#async empty()
wait for the queue to be empty
Parameters
Returns | Promise(undefined) |
Example
const PromiseQueue = require("koru/promise-queue");const queue = new PromiseQueue(); await queue.empty(); let i = 4; queue.add(()=>{ i = i * 2}); const p = queue.empty(); // p won't resolve until queue empty queue.add(()=>{ ++i}); assert.same(i, 4); queue.add(()=>{ i *= 5}); await p; assert.same(i, 45); queue.add(()=>{ i *= 10}); await queue.empty(); await queue.empty(); assert.same(i, 450);
Overview
The publish subscribe systems allows clients to be informed of changes on the server. The usual scenario is when a client is interested in one or more Models.
To get reactive changes to server models. A client subscribes to a publication either using the simplistic AllPub and AllSub classes or custom classes like the following:
Client | Server |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
The example above is deficient in several ways:
- It does not send updated related only to the shelf requested
- It does not coordinate with other publications which may be publishing the same documents.
- It does not use
lastSubscribed
to reduce server-to-client traffic.
It is non-trivial to fix these deficiencies; however:
- {../publication.discreteLastSubscribed} can be used to only send updates. When sending only
updates it is important the
lastSubscribedMaximumAge
is set and that no records are actually deleted until the document has been unchanged for the duration of Publication.lastSubscribedMaximumAge. In this way clients will not miss any data changes. - Union can be used to combine subscriptions to reduce traffic.
AllPub
AllPub is an extended Publication which publicizes all documents in every defined Model. By default all client subscriptions are handled by one unified observer and an identical message is sent to all subscribers. When extending AllPub individual observations can go in the init method and union observations can go in the initUnion method.
See also AllSub
Properties
requireUserId | boolean | require user to be signed in |
Methods
AllPub.excludeModel(...names)
Exclude Models from being published. UserLogin is always excluded. This will clear includeModel
Parameters
names | string |
Example
const AllPub = require("koru/pubsub/all-pub");const models = 'Book Author UserLogin ErrorLog AuditLog'.split(' '); const db = new MockDB(models); const mc = new MockConn(conn); const {Book, Author, UserLogin, AuditLog, ErrorLog} = db.models; const book = await Book.create(); const author = await Author.create(); const userLogin = await UserLogin.create(); const auditLog = await AuditLog.create(); const errorLog = await ErrorLog.create(); class MyAllPub extends AllPub {} MyAllPub.pubName = 'All'; // register publication All MyAllPub.excludeModel('AuditLog', 'ErrorLog'); const sub = await conn.onSubscribe('s123', 1, 'All'); mc.assertAdded(book); mc.assertAdded(author); mc.refuteAdded(userLogin); mc.refuteAdded(auditLog); mc.refuteAdded(errorLog); let bookChange, auditLogChange; await TransQueue.transaction(async () => { bookChange = await db.change(book); auditLogChange = await db.change(auditLog); }); mc.assertChange(bookChange); mc.refuteChange(auditLogChange);
AllPub.includeModel(...names)
Explicitly include the Models which should be published. All other models are excluded. This clears excludeModel
Parameters
names | string |
Example
const AllPub = require("koru/pubsub/all-pub");const models = 'Book Author UserLogin ErrorLog AuditLog'.split(' '); const db = new MockDB(models); const mc = new MockConn(conn); const {Book, Author, UserLogin, AuditLog, ErrorLog} = db.models; const book = await Book.create(); const author = await Author.create(); const userLogin = await UserLogin.create(); const auditLog = await AuditLog.create(); const errorLog = await ErrorLog.create(); class MyAllPub extends AllPub {} MyAllPub.pubName = 'All'; // register publication All MyAllPub.includeModel('UserLogin', 'Author'); assert.equals(Array.from(MyAllPub.includedModels()).map((m) => m.modelName).sort(), [ 'Author', 'UserLogin']); const sub = await conn.onSubscribe('s123', 1, 'All'); mc.refuteAdded(book); mc.assertAdded(author); mc.assertAdded(userLogin); mc.refuteAdded(auditLog); mc.refuteAdded(errorLog); let bookChange, authorChange; await TransQueue.transaction(async () => { bookChange = await db.change(book); authorChange = await db.change(author); }); mc.refuteChange(bookChange); mc.assertChange(authorChange);
AllPub.*includedModels()
Return an iterator over the models that are included in the subscription
Parameters
Returns | Function |
Example
const AllPub = require("koru/pubsub/all-pub");const db = new MockDB(['Book', 'Author']); const {Book, Author} = db.models; assert.equals(Array.from(AllPub.includedModels()).map((m) => m.modelName).sort(), [ 'Author', 'Book']);
AllPub#async init()
init will send the contents of the database to each client that subscribes. This method is called automatically and only needs to be overridden if additionaly setup is required.
When multiple clients connect, within the same time period, only one pass of the DB is made and the result is sent to all waiting clients.
Parameters
Returns | Promise(undefined) |
Example
const AllPub = require("koru/pubsub/all-pub");const db = new MockDB(['Book']); const mc = new MockConn(conn); const {Book} = db.models; const book1 = await Book.create(); const book2 = await Book.create(); class MyAllPub extends AllPub {} const sub1p = koru.runFiber(() => conn.onSubscribe('s123', 1, 'All')); const sub2p = koru.runFiber(() => conn.onSubscribe('s124', 1, 'All')); future.resolve(); const sub1 = await sub1p; const sub2 = await sub2p; assert.calledTwice(mc.sendEncoded); assert.same(mc.sendEncoded.firstCall.args[0], mc.sendEncoded.lastCall.args[0]); mc.assertAdded(book1); mc.assertAdded(book2);
AllSub
AllSub is an extended Subscription which will subscribe to all documents in every defined Model.
See Also AllPub
Methods
AllSub.excludeModel(...names)
Exclude Models from being subscribed to. UserLogin is always excluded. This will clear includeModel
Parameters
names | string |
Example
const AllSub = require("koru/pubsub/all-sub");const models = 'Book Author UserLogin ErrorLog AuditLog'.split(' '); const db = new MockDB(models); const {Book, Author, UserLogin, AuditLog, ErrorLog} = db.models; const book = Book.create(); const author = Author.create(); const userLogin = UserLogin.create(); const auditLog = AuditLog.create(); const errorLog = ErrorLog.create(); class MyAllSub extends AllSub {} MyAllSub.pubName = 'All'; // register publication All MyAllSub.excludeModel('AuditLog', 'ErrorLog'); const sub = MyAllSub.subscribe(); assert(sub._matches.Book); assert(sub._matches.Author); refute(sub._matches.UserLogin); refute(sub._matches.AuditLog); refute(sub._matches.ErrorLog);
AllSub.includeModel(...names)
Explicitly include the Models which should be published. All other models are excluded. This clears excludeModel
Parameters
names | string |
Example
const AllSub = require("koru/pubsub/all-sub");const models = 'Book Author UserLogin ErrorLog AuditLog'.split(' '); const db = new MockDB(models); const {Book, Author, UserLogin, AuditLog, ErrorLog} = db.models; const book = Book.create(); const author = Author.create(); const userLogin = UserLogin.create(); const auditLog = AuditLog.create(); const errorLog = ErrorLog.create(); class MyAllSub extends AllSub {} MyAllSub.pubName = 'All'; // register publication All MyAllSub.includeModel('UserLogin', 'Author'); assert.equals(Array.from(MyAllSub.includedModels()).map((m) => m.modelName).sort(), [ 'Author', 'UserLogin']); const sub = MyAllSub.subscribe(); refute(sub._matches.Book); assert(sub._matches.Author); assert(sub._matches.UserLogin); refute(sub._matches.AuditLog); refute(sub._matches.ErrorLog);
AllSub.*includedModels()
Return an iterator over the models that are included in the subscription
Parameters
Returns | Function |
Example
const AllSub = require("koru/pubsub/all-sub");const db = new MockDB(['Book', 'Author']); const {Book, Author} = db.models; assert.equals(Array.from(AllSub.includedModels()).map((m) => m.modelName).sort(), [ 'Author', 'Book']);
AllSub.subscribe(args, callback)
Subscribe to all models in model-map
Parameters
[args] | ||
[callback] | ||
Returns | AllSub |
Example
const AllSub = require("koru/pubsub/all-sub");const sub = AllSub.subscribe();
ModelMatch
A class to create a match registry that compares a Model document to a set of match functions.
Used in Subscription
Methods
constructor()
Create a ModelMatch registry.
Example
const ModelMatch = require("koru/pubsub/model-match");new ModelMatch();
ModelMatch#has(doc)
Test if a document matches a matcher. See register.
Parameters
doc | BaseModel | the document to test if matches a matcher |
Returns | boolean |
|
Example
const ModelMatch = require("koru/pubsub/model-match");const myMatch = new ModelMatch(); const book1 = Book.fetch(); const book2 = Book.fetch(); const book3 = Book.fetch(); assert.same(myMatch.has(book1), void 0); const m1 = myMatch.register('Book', (doc) => {}); assert.same(myMatch.has(book1), void 0); const m2 = myMatch.register('Book', (doc) => { return doc === book1 ? true : void 0; }); const m3 = myMatch.register('Book', (doc) => { return doc === book2; }); assert.isTrue(myMatch.has(book1)); assert.isTrue(myMatch.has(book2)); assert.isFalse(myMatch.has(book3)); m3.delete(); assert.isTrue(myMatch.has(book1)); assert.same(myMatch.has(book2), void 0);
ModelMatch#register(modelName, comparator)
Register a matcher agains a model.
See also has, Subscription#match
Parameters
modelName | string | or model |
comparator | function | The |
Returns | object |
|
Example
const ModelMatch = require("koru/pubsub/model-match");const myMatch = new ModelMatch(); const book1 = Book.fetch(); const book2 = Book.fetch(); const book3 = Book.fetch(); const comparator = (doc) => doc === book1 || (doc === book2 ? false : void 0); const m1 = myMatch.register('Book', comparator); assert.isTrue(myMatch.has(book1)); assert.isFalse(myMatch.has(book2)); assert.same(myMatch.has(book3), void 0);
PreloadSubscription
PreloadSubscription extends Subscription to facilitate preloading documents from a client QueryIDB or similar in addition to fetching from server. This class overrides the Subscription#connect method with calls to the methods described below.
Methods
constructor(args, session)
Used to initialise matchers and any other common synchronous work
Parameters
[args] | ...any-type | the arguments to send to the publication. |
[session] | Session | The session to subscribe to (defaults to Session). |
Example
const PreloadSubscription = require("koru/pubsub/preload-subscription");class LibrarySub extends PreloadSubscription { constructor(args) { super(args); const {shelf} = args; this.match(Book, (doc) => doc.shelf === shelf); } } LibrarySub.pubName = 'Library'; const sub = LibrarySub.subscribe({shelf: 'mathematics'}); const book1 = {shelf: 'mathematics'}; assert.isTrue(sub._matches.Book.value(book1));
PreloadSubscription#getQueryIDB()
Parameters
Returns | object |
Example
const PreloadSubscription = require("koru/pubsub/preload-subscription");class LibrarySub extends PreloadSubscription { getQueryIDB() {return idb} async preload(idb) { this.shelf = await idb.get('last', 'self'); } }
PreloadSubscription#preload(idb)
Override this (usally) async method to load client records, using say,
QueryIDB. Should await
for loads to complete before returning. Any
exception thrown during execution will be caught and sent to the
Subscription#onConnect observers.
Parameters
idb | returned from getQueryIDB | |
Returns | "skipServer" / "waitServer" / undefined | Unless return is If return is |
Example
const PreloadSubscription = require("koru/pubsub/preload-subscription");class LibrarySub extends PreloadSubscription { getQueryIDB() {return idb} async preload(idb) { const shelf = await idb.get('last', 'self'); if (self == null) return 'waitServer'; const books = await idb.index('Book', 'shelf').getAll(IDBKeyRange.only(shelf.name)); idb.loadDocs('Book', books); if (isOffline) return 'skipServer'; } } LibrarySub.pubName = 'Library';
PreloadSubscription#serverResponse(err, idb)
Override this method to intercept the serverResponse to the subscribe.
Parameters
err | null / koru.Error | null for success else error from server |
idb | object / undefined | the object returned from getQueryIDB |
Example
const PreloadSubscription = require("koru/pubsub/preload-subscription");let callbackNotCalled = false; class LibrarySub extends PreloadSubscription { getQueryIDB() {return idb;} serverResponse(err, idb) { this.serverResponseArgs = [err, idb]; callbackNotCalled = callback.firstCall === void 0; } } let resolve; const promise = new Promise((_resolve) => {resolve = _resolve}); const callback = (err) => {resolve(err)}; const sub = LibrarySub.subscribe({shelf: 'mathematics'}, callback); await connect.promise; sub[connected$]({lastSubscribed: 0}); const err = await promise; if (err) throw err; assert.isTrue(callbackNotCalled); // assert serverResponse called before callback assert.equals(sub.serverResponseArgs, [null, idb]); // server error class BookSub extends PreloadSubscription { serverResponse(err, idb) { this.serverResponseArgs = [err, idb]; } } const sub2 = BookSub.subscribe('book1'); const err2 = new koru.Error(500, 'server error'); sub2.stop(err2); assert.equals(sub2.serverResponseArgs, [err2, void 0]);
Publication
A Publication is a abstract interface for handling subscriptions.
See also Subscription
Properties
lastSubscribedInterval | number | Allow grouping subscription downloads to an interval bin so that subscriptions wanting similar data can be satisfied with one pass of the database. Specified in milliseconds. Defaults to 5mins. |
lastSubscribedMaximumAge | number | Any subscription with a lastSubscribed older than this is aborted with error 400, reason
Client subscriptions should not send a |
#lastSubscribed | number | The subscriptions last successful subscription time in ms |
#userId | string | userId is a short cut to |
Methods
constructor({id, conn, lastSubscribed})
Build an incomming subscription.
Parameters
id | string | The id of the subscription |
conn | ServerConnection | The connection of the subscription |
lastSubscribed | number | the time in ms when the subscription last connected |
Example
const Publication = require("koru/pubsub/publication");const lastSubscribed = Date.now() - util.DAY; let sub; class Library extends Publication { constructor(options) { super(options); sub = this; } } Library.module = module; assert.same(Library.pubName, 'Library'); await conn.onSubscribe('sub1', 1, 'Library', {shelf: 'mathematics'}, lastSubscribed); assert.same(sub.constructor, Library); assert.same(sub.conn, conn); assert.same(sub.id, 'sub1'); assert.same(sub.lastSubscribed, util.dateNow());
Publication.discreteLastSubscribed(time)
Convert time
to the lower lastSubscribedInterval
boundry.
Example
const Publication = require("koru/pubsub/publication");Publication.lastSubscribedInterval = 20*60*1000; assert.equals( Publication.discreteLastSubscribed(+ new Date(2019, 0, 4, 9, 10, 11, 123)), + new Date(2019, 0, 4, 9, 0));
Publication#init(args)
This is where to fetch and send documents matching the args
supplied.
On completion of the init method the server will inform the client the connection is
successful. If an error is thrown then the client will be inform the connection is
unsuccessful.
Parameters
args | ...any-type | from client |
Example
const Publication = require("koru/pubsub/publication");const lastSubscribed = now - util.DAY; let sub; class Library extends Publication { init({shelf}) { sub = this; Val.allowIfValid(typeof shelf === 'string', 'shelf'); this.conn.added('Book', 'b123', {title: 'Principia Mathematica'}); } } Library.pubName = 'Library'; await conn.onSubscribe('sub1', 1, 'Library', {shelf: 'mathematics'}, lastSubscribed); assert.same(sub.conn, conn); assert.same(sub.id, 'sub1'); assert.same(sub.lastSubscribed, util.dateNow()); assert.calledWith(conn.added, 'Book', 'b123', {title: 'Principia Mathematica'}); assert.calledWith(conn.sendBinary, 'Q', ['sub1', 1, 200, util.dateNow()]); assert.isFalse(sub.isStopped);// Validation error conn.sendBinary.reset(); await conn.onSubscribe('sub1', 1, 'Library', {shelf: 123}, lastSubscribed); assert.calledWith(conn.sendBinary, 'Q', ['sub1', 1, 400, {shelf: [['is_invalid']]}]); assert.isTrue(sub.isStopped);
Publication#onMessage(message)
Called when a message has been sent from the subscription. Messages are used to alter the state of a subscription. If an error is thrown the client callback will receive the error.
Parameters
message | object |
Example
const Publication = require("koru/pubsub/publication");let sub; class Library extends Publication { init(args) { this.args = args; sub = this; } onMessage(message) { const name = message.addShelf; if (name !== undefined) { this.args.shelf.push(name); Book.where({shelf: name}).forEach((doc) => { this.conn.sendBinary('A', ['Book', doc._id, doc.attributes]); }); return 'done :)'; } } } Library.pubName = 'Library'; await conn.onSubscribe('sub1', 1, 'Library', {shelf: ['mathematics']}); assert.calledWith(conn.sendBinary, 'Q', ['sub1', 1, 200, m.number]); conn.sendBinary.reset(); await conn.onSubscribe('sub1', 2, null, {addShelf: 'fiction'}); assert.equals(conn.sendBinary.firstCall.args, [ 'A', ['Book', 'book2', {name: 'The Bone People', shelf: 'fiction'}]]); assert.equals(conn.sendBinary.lastCall.args, [ 'Q', ['sub1', 2, 0, 'done :)']]);
Publication#postMessage(message)
Post message
directly to the subscriber client. See Subscription#onMessage
Parameters
message | any-type |
Example
const Publication = require("koru/pubsub/publication");let sub; class Library extends Publication { init() {sub = this} } Library.pubName = 'Library'; await conn.onSubscribe('sub1', 1, 'Library', {shelf: ['mathematics']}); sub.postMessage({my: 'message'});
Publication#stop()
Stop a subscription. This method can be called directly on the server to stop a subscription. It will also be called indirectly by a stop request from the client.
Example
const Publication = require("koru/pubsub/publication");// server stops the subscription sub.stop(); assert.isTrue(sub.isStopped); assert.calledWith(conn.sendBinary, 'Q', [sub.id]); // tell client we're stopped// client stops the subscription await conn.onSubscribe('sub1', 2); assert.isTrue(sub.isStopped); refute.called(conn.sendBinary); // no need to send to client
Publication#userIdChanged(newUID, oldUID)
The default behavior is to do nothing. Override this if an userId change needs to be handled.
Example
const Publication = require("koru/pubsub/publication");class Library extends Publication { async userIdChanged(newUID, oldUID) { if (newUID === undefined) this.stop(); } } Library.pubName = 'Library'; const sub = await conn.onSubscribe('sub1', 1, 'Library'); spy(sub, 'stop'); await sub.conn.setUserId('uid123'); refute.called(sub.stop); await sub.conn.setUserId(undefined); assert.called(sub.stop);
Subscription
A Subscription is a abstract interface for subscribing to publications.
See also Publication
Properties
lastSubscribedMaximumAge | number | Any subscription with a lastSubscribed older than this sends 0 (no last subscribed) to the server. Specified in milliseconds. Defaults to -1 (always send 0). |
Methods
constructor(args, session=Session)
Create a subscription
Parameters
[args] | ...any-type | the arguments to send to the publication. |
[session] | The session to subscribe to (defaults to Session). |
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { } Library.module = module; assert.same(Library.pubName, 'Library'); const sub = new Library({shelf: 'mathematics'}); assert.same(sub._id, '1'); assert.equals(sub.args, {shelf: 'mathematics'}); assert.same(sub.subSession, SubscriptionSession.get(Session)); const sub2 = new Library(Session); assert.same(sub.subSession, sub2.subSession); assert.same(sub2._id, '2');
Subscription.markForRemove(doc)
Mark the given document as a simulated add which will be removed if not updated by the
server. This method can be called without a this
value
Parameters
doc | BaseModel |
Example
const Subscription = require("koru/pubsub/subscription");const {markForRemove} = Subscription; const book1 = Book.create(); markForRemove(book1); assert.equals(Query.simDocsFor(Book)[book1._id], ['del', void 0]);
Subscription.subscribe(args, callback)
A convience method to create a subscription that connects to the publication and calls callback on connect.
Parameters
args | any-type | the arguments to send to the server |
[callback] | function | called when connection complete |
Returns | Subscription | instance of subscription |
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { } let response; const sub = Library.subscribe({shelf: 'mathematics'}, (error) => { response = {error, state: sub.state}; }); assert.same(sub.state, 'connect'); assert.same(response, void 0); responseFromServer(); assert.equals(response, {error: null, state: 'active'});
Subscription#connect()
Connect to the Publication when session is ready.
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { } Library.module = module; const sub = new Library({shelf: 'mathematics'}); assert.same(sub.state, 'new'); assert.isTrue(sub.isClosed); sub.connect(); assert.same(sub.state, 'connect'); assert.isFalse(sub.isClosed); waitForServer(); assert.same(sub.state, 'active'); assert.isFalse(sub.isClosed);
Subscription#filterDoc(doc)
Remove a model document if it does not match this subscription
Example
const Subscription = require("koru/pubsub/subscription");const sub = new Library(); sub.filterDoc(book1);
Subscription#filterModels(...modelNames)
Remove model documents that do not match this subscription
Parameters
modelNames | string |
Example
const Subscription = require("koru/pubsub/subscription");const sub = new Library(); sub.filterModels('Book', 'Catalog');
Subscription#match(modelName, test)
Register a match function used to check if a document should be in the database.
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { constructor(args) { super(args); this.match(Book, (doc) => /lord/i.test(doc.name)); } }
Subscription#onConnect(callback)
Observe connection completions. The callback
is called on success with a null argument,
on error with an error
argument, and if stopped before connected with
koru.Error(409, 'stopped')
.
Callbacks will only be called once. To be called again after a re-connect they will need to
be set up again inside the callback.
Parameters
callback | function | called with an |
Returns | object | handler with a |
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { } Library.module = module; { /** success */ const sub1 = new Library({shelf: 'mathematics'}); let resonse; sub1.onConnect((error) => { resonse = {error, state: sub1.state}; }); sub1.connect(); const lastSubscribed = Date.now(); waitForServerResponse(sub1, {error: null, lastSubscribed}); assert.equals(resonse, {error: null, state: 'active'}); assert.same(sub1.lastSubscribed, lastSubscribed); } { /** error **/ const sub2 = new Library(); let resonse; sub2.onConnect((error) => { resonse = {error, state: sub2.state}; }); sub2.connect({shelf: 123}); waitForServerResponse(sub2, {error: {code: 400, reason: {self: [['is_invalid']]}}}); assert.equals(resonse, { error: m((err) => (err instanceof koru.Error) && err.error === 400 && util.deepEqual(err.reason, {self: [['is_invalid']]})), state: 'stopped'}); } { /** stopped early **/ const sub3 = new Library(); let resonse; sub3.onConnect((error) => { resonse = {error, state: sub3.state}; }); sub3.connect({shelf: 'history'}); sub3.stop(); assert.equals(resonse, { error: m((e) => e.error == 409 && e.reason == 'stopped'), state: 'stopped'}); assert.isTrue(sub3.isClosed); }
Subscription#onMessage(message)
Override this method to receive an unsolicited message
from the publication server.
Parameters
message | any-type |
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { onMessage(message) { this.answer = message; } } const sub = Library.subscribe([123]); server.postMessage({hello: 'subscriber'}); assert.equals(sub.answer, {hello: 'subscriber'});
Subscription#postMessage(message, callback)
Send a message to publication. Messages can be used to alter the state of the publication.
Messages are NOT re-transmitted if the connection is lost; Intead the subscription args
should be modified to reflect the change in state. In such cases the callback will be
added to the onConnect queue.
Parameters
message | any-type | the message to send |
callback | function | a function with the arguments |
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { } const onConnect = stub(); const sub = Library.subscribe([123, 456], onConnect); sub.args.push(789); let done = false; sub.postMessage({addArg: 789}, (err, result) => { if (err) sub.stop(); done = result === 'added'; }); receivePost(sub); assert.isTrue(done);
Subscription#reconnecting()
Override this method to be called when a subscription reconnect is attempted. Calling stop within this method will stop the reconnect.
When there is no lastSubscribed
time, or lastSubscribed is older than
lastSubscribedMaximumAge
, markForRemove should be called on documents matching this
subscription.
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { /** ⏿ ⮧ here we override reconnecting **/ reconnecting() { Book.query.forEach(Subscription.markForRemove); } } const reconnecting = spy(Library.prototype, 'reconnecting'); const sub = new Library(); sub.connect(); refute.called(reconnecting); Session.reconnected(); // simulate a session reconnect assert.calledOnce(reconnecting);
Subscription#stop(error)
Stops a subscription. Releases any matchers that were set up and calls stopped.
Parameters
[error] |
Example
const Subscription = require("koru/pubsub/subscription");const doc1 = Book.create({_id: 'doc1'}); const doc2 = Book.create({_id: 'doc2'}); const doc3 = Book.create({_id: 'doc3'}); const mr = SubscriptionSession.get(Session).match.register('Book', (doc) => { return doc === doc2; }); class Library extends Subscription { stopped(unmatch) { unmatch(doc1); unmatch(doc2); } } const sub = new Library(); sub.match('Book', () => true); sub.connect(); assert(Book.findById('doc1')); assert(sub.subSession.match.has(doc1, 'stopped')); sub.stop(); refute(Book.findById('doc1')); assert(Book.findById('doc2')); assert(Book.findById('doc3')); assert.isTrue(sub.isClosed);
Subscription#stopped(unmatch)
Override this method to be called with an unmatch
function when the subscription is
stopped.
Parameters
unmatch | function | can be called on all documents that no longer match this subscription. |
Example
const Subscription = require("koru/pubsub/subscription");const doc1 = Book.create({_id: 'doc1'}); const doc2 = Book.create({_id: 'doc2'}); const doc3 = Book.create({_id: 'doc3'}); const mr = SubscriptionSession.get(Session).match.register('Book', (doc) => { return doc === doc2; }); class Library extends Subscription { stopped(unmatch) { unmatch(doc1); unmatch(doc2); } } const sub = new Library(); sub.match('Book', () => true); sub.connect(); assert(Book.findById('doc1')); assert(sub.subSession.match.has(doc1, 'stopped')); sub.stop(); refute(Book.findById('doc1')); assert(Book.findById('doc2')); assert(Book.findById('doc3')); assert.isTrue(sub.isClosed);
Subscription#unmatch(modelName)
Deregister a match function.
Example
const Subscription = require("koru/pubsub/subscription");class Library extends Subscription { constructor(args) { super(args); this.match(Book, (doc) => /lord/i.test(doc.name)); } noBooks() { this.unmatch(Book); } }
Subscription#userIdChanged(newUID, oldUID)
Override this method to change the default behavior of doing nothing when the user id changes.
Example
const Subscription = require("koru/pubsub/subscription");const subscription = new Subscription();subscription.userIdChanged("uid123", undefined);subscription.userIdChanged("uid456", "uid123");subscription.userIdChanged(undefined, "uid456");
Union
Union is an interface used to combine server subscriptions to minimise work on the server.
Properties
#handles | Array | An array to store any handles that should be stopped when the union is empty. A handle
is anything that has a |
Methods
constructor()
Create a Union instance
Example
const Union = require("koru/pubsub/union");const myHandle = {stop: stub()}; class MyUnion extends Union { constructor() { super(); this.handles.push(myHandle); } } const union = new MyUnion(); assert.equals(union.handles, [myHandle]);
Union#async addSub(sub, lastSubscribed=sub.lastSubscribed)
Add a subscriber to a union. This will cause loadInitial to be run for the subscriber.
For performance, if other subscribers are added to the union then they will be added to the
same loadQueue (if still running) as a previous subscriber if and only if their
Publication.discreteLastSubscribed is the same as a previous subscriber and their
Publication#lastSubscribed is greater than the minLastSubscribed
passed to
loadInitial
; otherwise a new loadInitial will be run.
It is important to note that addSub blocks the thread and queues batchUpdate until loadInitial has finished. This ensures that the initial load will be sent before any changes during the load.
Parameters
sub | Publication | the subscriber to add |
[lastSubscribed] | number | when sub last subscribed (in ms). Defaults to Publication#lastSubscribed |
Returns | Promise(undefined) |
Example
const Union = require("koru/pubsub/union");const book1 = await Book.create(); const book2 = await Book.create(); class MyUnion extends Union { constructor(author_id) { super(); this.author_id = author_id; } async loadInitial(encoder) { await Book.where('author_id', this.author_id).forEach((doc) => {encoder.addDoc(doc)}); } } const union = new MyUnion('a123'); const sub = new Publication({id: 'sub1', conn, lastSubscribed: void 0}); /** ⏿ ⮧ here we add the sub **/ await union.addSub(sub); const msgs = mc.decodeLastSend(); assert.equals(msgs, [ ['A', ['Book', {_id: 'book1', name: 'Book 1'}]], ['A', ['Book', {_id: 'book2', name: 'Book 2'}]], ]);
Union#async addSubByToken(sub, token)
Like addSub but instead of using lastSubscribed to group for loadInitial the token is used to group for loadByToken. The token can be anything is compared sameness.
This is useful when critera changes such as permisssions which causes a sub
to change
unions which requires some records to be removed from the client and some new records to be
added. Usually the client will automatically handle the removes but the adds will need to
be calculated based on the new and old unions. The token should contain the information
needed to calculate the subset.
Parameters
sub | Publication | |
token | Union | |
Returns | Promise(undefined) |
Example
const Union = require("koru/pubsub/union");const book1 = await Book.create({genre_ids: ['drama', 'comedy']}); const book2 = await Book.create({genre_ids: ['drama']}); class MyUnion extends Union { constructor(genre_id) { super(); this.genre_id = genre_id; } async loadByToken(encoder, oldUnion) { await Book .where('genre_ids', this.genre_id) .whereNot('genre_ids', oldUnion.genre_id) .forEach((doc) => {encoder.addDoc(doc)}); } } const oldUnion = new MyUnion('comedy'); const union = new MyUnion('drama'); const sub = new Publication({id: 'sub1', conn, lastSubscribed: void 0}); /** ⏿ ⮧ here we add the sub **/ await union.addSubByToken(sub, oldUnion); const msgs = mc.decodeLastSend(); assert.equals(msgs, [ ['A', ['Book', book2.attributes]], ]);
Union#batchUpdate()
The batchUpdate function is built by the buildBatchUpdate method on Union
construction. It is used to broadcast document updates to multiple client subscriptions. It
does not need a this
argument.
Updates are batched until the successful end of the current transaction and the resulting message is sent to all subs in the union. If the transaction is aborted no messages are sent.
See also initObservers
Example
const Union = require("koru/pubsub/union");const db = new MockDB(['Book']); const mc = new MockConn(conn); const {Book} = db.models; const book1 = await Book.create(); const book2 = await Book.create(); class MyUnion extends Union { initObservers() { /** ⏿ here we pass the batchUpdate ⮧ **/ this.handles.push(Book.onChange(this.batchUpdate)); } } const union = new MyUnion(); const sub = new Publication({id: 's123', conn}); await union.addSub(sub); await TransQueue.transaction(async () => { await db.change(book1); await Book.create(); await db.remove(book2); }); const msgs = mc.decodeLastSend(); assert.equals(msgs, [ ['C', ['Book', 'book1', {name: 'name change'}]], ['A', ['Book', {_id: 'book3', name: 'Book 3'}]], ['R', ['Book', 'book2', void 0]], ]);
Union#buildBatchUpdate()
buildBatchUpdate builds the batchUpdate function that can be used to broadcast document updates to multiple client subscriptions. It is called during Union construction.
Parameters
Returns | function |
Example
const Union = require("koru/pubsub/union");class MyUnion extends Union { } const union = new MyUnion(); assert.isFunction(union.batchUpdate);
Union#buildUpdate(dc)
Override this to manipulate the document sent to clients. By default calls ServerConnection.buildUpdate.
Example
const Union = require("koru/pubsub/union");class MyUnion extends Union { buildUpdate(dc) { const upd = super.buildUpdate(dc); if (upd[0] === 'C') { upd[1][2].name = 'filtered'; } return upd; } } const union = new MyUnion(); const book1 = await Book.create(); assert.equals( union.buildUpdate(DocChange.change(book1, {name: 'old name'})), ['C', ['Book', 'book1', {name: 'filtered'}]], );
Union#encodeUpdate(dc)
encode a DocChange ready for sending via sendEncoded or sendEncodedWhenIdle.
Parameters
dc | DocChange | for the document that has been updated |
Returns | Uint8Array | an encoded (binary) update command |
Example
const Union = require("koru/pubsub/union");const sub1 = new Publication({id: 'sub123', conn}); await union.addSub(sub1); const book1 = await Book.create(); const msg = union.encodeUpdate(DocChange.change(book1, {name: 'old name'})); assert.equals(msg[0], 67); /* 'C' */ // is a Change command assert.equals(ConnTH.decodeMessage(msg, conn), ['Book', 'book1', {name: 'Book 1'}]);
Union#initObservers()
Override this method to start observers when one or more subscribers are added. the
onEmpty method should be overriden to stop any handles not stored in the
handles
array property.
buildBatchUpdate can be used to build an updater suitable as an argument for base-model.onChange
Example
const Union = require("koru/pubsub/union");class MyUnion extends Union { onEmpty() { super.onEmpty(); for (const handle of this.handles) handle.stop(); } initObservers() { const batchUpdate = this.buildBatchUpdate(); this.handles.push(Book.onChange(batchUpdate)); } } const union = new MyUnion(); const initObservers = spy(union, 'initObservers'); const onEmpty = spy(union, 'onEmpty'); const sub1 = new Publication({id: 'sub1', conn: conn1}); await union.addSub(sub1); const sub2 = new Publication({id: 'sub2', conn: conn2}); await union.addSub(sub2); union.removeSub(sub1); assert.calledOnce(initObservers); refute.called(onEmpty); union.removeSub(sub2); assert.calledOnce(onEmpty);
Union#loadByToken(encoder, token)
Like loadInitial but instead of using minLastSubscribed passes the token from addSubByToken to calculate which documents to load.
Parameters
encoder | an encoder object; see loadInitial | |
token | from addSubByToken. Usually an old union the subscriptions belonged to. |
Example
const Union = require("koru/pubsub/union");// see addSubByToken for better example class MyUnion extends Union { /** ⏿ ⮧ here we loadByToken **/ async loadByToken(encoder, token) { if (token === 'myToken') { encoder.addDoc(book1); encoder.remDoc(book2, 'stopped'); } } } const union = new MyUnion(); const sub1 = new Publication({id: 'sub1', conn}); await union.addSubByToken(sub1, 'myToken'); const msgs = mc.decodeLastSend(); assert.equals(msgs, [ ['A', ['Book', book1.attributes]], ['R', ['Book', book2._id, 'stopped']], ]);
Union#loadInitial(encoder, minLastSubscribed)
Override this method to select the initial documents to download when a new group of subscribers is added. Subscribers are partitioned by their Publication.discreteLastSubscribed time.
Parameters
encoder | an object that has methods
| |
minLastSubscribed | the lastSubscribed time related to the first subscriber for this load request. Only subscribers with a lastSubscribed >= first subscriber will be added to the load. |
Example
const Union = require("koru/pubsub/union");const book1 = await Book.create({updatedAt: new Date(now - 80000)}); const book2 = await Book.create({updatedAt: new Date(now - 40000)}); const book3 = await Book.create({updatedAt: new Date(now - 50000), state: 'D'}); const book4 = await Book.create({updatedAt: new Date(now - 31000), state: 'C'}); class MyUnion extends Union { async loadInitial(encoder, minLastSubscribed) { const addDoc = encoder.addDoc.bind(encoder); const chgDoc = encoder.chgDoc.bind(encoder); const remDoc = encoder.remDoc.bind(encoder); if (minLastSubscribed == 0) { await Book.whereNot({state: 'D'}).forEach(addDoc); } else { await Book.where({updatedAt: {$gte: new Date(minLastSubscribed)}}).forEach((doc) => { switch (doc.state) { case 'D': remDoc(doc); break; case 'C': chgDoc(doc, {state: doc.state}); break; default: addDoc(doc); } }); } } } const union = new MyUnion(); const sub1 = new Publication({id: 'sub1', conn}); // no lastSubscribed await union.addSub(sub1); assert.equals(mc.decodeLastSend(), [ ['A', ['Book', book1.attributes]], ['A', ['Book', book2.attributes]], ['A', ['Book', book4.attributes]], ]); const lastSubscribed = now - 600000; const sub2 = new Publication({id: 'sub2', conn: conn2, lastSubscribed}); await union.addSub(sub2); assert.equals(mc2.decodeLastSend(), [ ['A', ['Book', book2.attributes]], ['R', ['Book', 'book3', void 0]], ['C', ['Book', 'book4', {state: 'C'}]], ]);
Union#onEmpty()
This method is called when all subscribers have been removed from the union. It stops any
handles that have been pushed on to #handles
and resets #handles
length to 0. If
overriden ensure that super.onEmpty()
is run.
See initObservers
Example
const Union = require("koru/pubsub/union");const union = new Union(); const onEmpty = spy(union, 'onEmpty'); await union.addSub(sub); union.removeSub(sub); assert.called(onEmpty);
Union#removeSub(sub)
Remove a subscriber from a union. When all subscriber have been removed from the union onEmpty will be called
See addSub, initObservers
Parameters
sub | Publication |
Example
const Union = require("koru/pubsub/union");const db = new MockDB(['Book']); const mc = new MockConn(conn); const {Book} = db.models; const book1 = await Book.create(); const book2 = await Book.create(); let onEmptyCalled = false; class MyUnion extends Union { onEmpty() { super.onEmpty(); onEmptyCalled = true; } } const union = new MyUnion(); class MyPub extends Publication { constructor(options) { super(options); } stop() { super.stop(); /** ⏿ ⮧ here we remove the sub **/ union.removeSub(this); } } const sub = new MyPub({id: 'sub1', conn, lastSubscribed: void 0}); await union.addSub(sub); sub.stop(); assert.isTrue(onEmptyCalled);
Union#sendEncoded(msg)
Send a pre-encoded message to all subscribers. Called by batchUpdate.
Parameters
msg | string | the pre-encoded message |
Example
const Union = require("koru/pubsub/union");const sub1 = new Publication({id: 'sub123', conn}); await union.addSub(sub1); const sub2 = new Publication({id: 'sub124', conn: conn2}); await union.addSub(sub1); await union.addSub(sub2); union.sendEncoded('myEncodedMessage'); assert.calledWith(conn.sendEncoded, 'myEncodedMessage'); assert.calledWith(conn2.sendEncoded, 'myEncodedMessage');
Union#sendEncodedWhenIdle(msg)
Delays calling sendEncoded until loadInitial and loadByToken have finished
Parameters
msg | string |
Example
const Union = require("koru/pubsub/union");const union = new Union(); union.sendEncoded = stub(); let callCount; union.loadInitial = () => { union.sendEncodedWhenIdle('msg1'); callCount = union.sendEncoded.callCount; }; const sub1 = new Publication({id: 'sub123', conn}); await union.addSub(sub1); assert.same(callCount, 0); assert.calledOnceWith(union.sendEncoded, 'msg1'); union.sendEncodedWhenIdle('msg2'); assert.calledWith(union.sendEncoded, 'msg2');
Union#subs()
Return an iterator over the union's subs.
Parameters
Returns | Function |
Example
const Union = require("koru/pubsub/union");const sub1 = new Publication({id: 'sub123', conn}); await union.addSub(sub1); const sub2 = new Publication({id: 'sub124', conn: conn2}); await union.addSub(sub2); assert.equals(Array.from(union.subs()), [sub1, sub2]);
Random
Generate random fractions and ids using a pseudo-random number generator (PRNG) or a cryptographically secure PRNG (CSPRNG). If a CSRNG is not available on the client then some random like tokens will be used to seed a PRNG instead.
The PRNG uses AccSha256 to generate the sequence.
Properties
global | Random | a instance of a CSPRNG |
Methods
constructor(...tokens)
Create a new PRNG.
Aliases
create
a deprecated alternative static method.
Example
const Random = require("koru/random");// seeded const r1 = new Random(0); assert.same(r1.id(), "kFsE9G6DL26jiPd2U"); assert.same(r1.id(), "o8kyB8YoOT2NCM03U"); // same seed produces same numbers assert.same(new Random(0).id(), "kFsE9G6DL26jiPd2U"); // multiple tokens const r2 = new Random("hello", "world"); assert.same(r2.id(), 'NTuiM1uEZR7vz2Nd5'); const csprng = new Random(); refute.same(csprng.id(), 'IwillNeverMatchid');
Random.hexString(value)
Example
const Random = require("koru/random");intercept(Random.global, 'hexString', (n)=>'f007ba11c4a2'.slice(0,n)); assert.same(Random.hexString(8), "f007ba11"); const token = "abc123"; util.thread.random = new Random(token); assert.same(Random.hexString(33), 'ce8b9dfb29ed185b23fd764c97af4f108');
Random.id()
Generate a new random id using the id method from firstly: util.thread.random
if defined; otherwise random.global
. Using util.thread.random
allows for repeatable and
effecient ids to be produced for a database transaction.
Parameters
Returns | string |
Example
const Random = require("koru/random");intercept(Random.global, 'id', ()=>'aGlobalId'); assert.same(Random.id(), "aGlobalId"); const id = "abc123"; util.thread.random = new Random(id); assert.same(Random.id(), "kfx4FPotz6UerPhgc");
Random#fraction()
Generate a number between 0 and 1.
Parameters
Returns | number |
Example
const Random = require("koru/random");const random = new Random(1,2,3); assert.equals(random.fraction(), 0.26225688261911273); assert.equals(random.fraction(), 0.5628666188567877); assert.equals(random.fraction(), 0.09692026115953922);
Random#hexString(digits)
Generate a sequence of hexadecimal characters.
Example
const Random = require("koru/random");const random = new Random(6); assert.same(random.hexString(2), "c7"); assert.same(random.hexString(7), "e8b19da");
Random#id()
Generate a sequence of characters suitable for Model ids.
Example
const Random = require("koru/random");const random = new Random(0); assert.same(random.id(), "kFsE9G6DL26jiPd2U"); assert.same(random.id(), "o8kyB8YoOT2NCM03U"); assert.same(random.id(), "3KkBsZqIxSLG9cNmT");
ServerPages
Server side page rendering coordinator.
ServerPages enables apps to serve dynamically created server-side web pages. By convention ServerPages follow the CRUD, Verbs, and Actions rules defined in Rails namely:
HTTP Verb | Path | Controller#Action | Used for |
---|---|---|---|
GET | /books | Books#index | display a list of all books |
GET | /books/new | Books#new | return an HTML form for creating a new book |
POST | /books | Books#create | create a new book |
GET | /books/:id | Books#show | display a specific book |
GET | /books/:id/edit | Books#edit | return an HTML form for editing a book |
PATCH/PUT | /books/:id | Books#update | update a specific book |
DELETE | /books/:id | Books#destroy | delete a specific book |
To implement index, new, show and edit actions all that is needed is a corresponding
Template like Book.Show
. The create, update and destroy actions need the action
method implemented in the controller like
BooksController
.
For the show, edit, update and destroy actions the id can be accessed from {{params.id}}
in
the template or this.params.id
in the controller.
Alternatively Controller actions matching the HTTP verb can be used and these take precedence over other actions; so if the controller has index, show, new, edit and get action methods then the get method will override the other four action methods. See BaseController for more ways to override the default actions.
Creating a server page
The simplest way to create a new server-page is to run ./scripts/koru generate server-page book
(or select from emacs koru menu). This will create the following files under
app/server-pages
:
book.html
- The view as an html template file. book.md may be used instead for a markdown templet file.book.js
- The controller corresponding to thebook.html
view used for updates and to control rendering the view.book.less
- A lessjs (or css) file that is included in the rendered page (if using default layout).
If a default layout does not exist then one will be created in app/server-pages/layouts
with
the files: default.html
, default.js
and default.less
Configuration
No configuration is needed for server-pages; they are automatically added when the first call
to koru generate server-page
is made. This will register a page server for the
WebServer in the app/startup-server.js
file.
Any pages under app/server-pages
will be automatically loaded when am HTTP request is made
that corresponds to the page.
Methods
ServerPages.async build(WebServer, pageDir='server-pages', pathRoot='DEFAULT')
Register a page server with a webServer.
Parameters
WebServer | WebServer | the web-server to handler pages for. |
[pageDir] | the directory to find views and controllers under relative to the app directory. | |
[pathRoot] | handle pages starting with this path root. | |
Returns | Promise(ServerPages) |
Example
const ServerPages = require("koru/server-pages/main");const WebServer = require('koru/web-server');const sp = await ServerPages.build(WebServer);
ServerPages#stop()
Deregister this page server from WebServer
Example
const ServerPages = require("koru/server-pages/main");const serverPages = await ServerPages.build(WebServer);serverPages.stop();
BaseController
BaseController provides the default actions for page requests. Action controllers extend BaseController to intercept actions. See ServerPages
Controllers are not constructed directly; rather ServerPages will invoke the constructor when the user requests a page associated with the controller.
Properties
view | Template | Templete for index action. All other action templates are children of this template. Example request
|
view.Edit | Template | Templete for edit action. Example request
|
view.New | Template | Templete for new action. Example request
|
view.Show | Template | Templete for show action. Example request
|
#body | The body of the request. If the content type is: |
Methods
<class extends BaseController>#async aroundFilter(callback)
An aroundFiler runs "around" an action controller. It is called are the $parser has
run. You can control which action is called by changing the the value of this.action
.
Parameters
callback | Function | call this function to run the |
Returns | Promise(undefined) |
Example
const BaseController = require("koru/server-pages/base-controller");class Foo extends BaseController { async aroundFilter(callback) { this.params.userId = 'uid123'; if (this.action == 'edit' && this.pathParts[0] == '1234') { this.action = 'specialEdit'; } await callback(this); testDone = this.rendered; } specialEdit() { this.renderHTML(Dom.h({div: this.params.userId})); } }
<class extends BaseController>#create()
Implement this action to process create requests.
POST /books
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { async create() { const book = await Book.create(await this.getBody()); this.redirect('/books/' + book._id); } }
<class extends BaseController>#destroy()
Implement this action to process destroy requests.
DELETE /books/:id
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { destroy() { const book = Book.findById(this.params.id); book.$remove(); this.redirect('/books'); } }
<class extends BaseController>#edit()
Implement this action to control show requests:
GET /books/:id/edit
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { edit() { this.params.book = Book.findById(this.params.id); } }
<class extends BaseController>#index()
Implement this action to control index requests:
GET /books
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { index() { this.params.books = Book.query; } }
<class extends BaseController>#new()
Implement this action to control new requests:
GET /books/new
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { new() { this.params.book = Book.build(); } }
<class extends BaseController>#show()
Implement this action to control show requests:
GET /books/:id
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { show() { this.params.book = Book.findById(this.params.id); } }
<class extends BaseController>#update()
Implement this action to process update requests.
PUT /books/:id
or PATCH /books/:id
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { update() { const book = Book.findById(this.params.id); book.changes = this.body; book.$$save(); this.redirect('/books/' + book._id); } }
BaseController#$parser()
The default request parser. Override this method for full control of the request.
Example
const BaseController = require("koru/server-pages/base-controller");class Books extends BaseController { $parser() { if (this.method === 'DELETE' && ! Auth.canDelete(this)) { this.error(403, 'You do not have delete access'); return; } super.$parser(); } }
BaseController#error(code, message)
Send an error response to the client;
Example
const BaseController = require("koru/server-pages/base-controller");const baseController = new BaseController();baseController.error(418, "Short and stout");
BaseController#redirect(url, code=302)
Send a redirect response to the client.
Example
const BaseController = require("koru/server-pages/base-controller");const baseController = new BaseController();baseController.redirect("/foo/1234");
BaseController#async render(content, {layout=this.App.defaultLayout}={})
Respond to the client with the rendered content wrapped in the specified layoyut.
Parameters
content | Element | usually rendered html but can be whatever the layout requires. |
[layout] | object | The layout View to wrap the content. defaults to
|
Returns | Promise(undefined) |
Example
const BaseController = require("koru/server-pages/base-controller");class HelloController extends BaseController { $parser() { this.render(Dom.h({div: 'Hello world'}), {layout: {$render({content}) { return Dom.h({html: [{head: {title: 'My First App'}}, {body: content}]}); }}}); } }
BaseController#async renderHTML(html)
Respond to the client with the rendered HTML content.
Parameters
html | Element | the html element to render. |
Returns | Promise(undefined) |
Example
const BaseController = require("koru/server-pages/base-controller");class HelloController extends BaseController { async $parser() { await this.renderHTML(Dom.h({div: 'Hello world'})); } }
InlineScript
Build a script function that contains from various modules that is suitable for insertion into an html page. It works similar to the AMD module loading but builds just one monolithic script without any other dependencies.
Methods
constructor(dir)
Create a new InlineScript processor.
Example
const InlineScript = require("koru/server-pages/inline-script");const script = new InlineScript(module.dir); script.require('./test-inline/simple'); assert.equals(script.map, {'koru/server-pages/test-inline/simple.js': 0}); assert.equals(script.modules, ["'simple'"]);
InlineScript#add(id, object)
Add a named object to the modules list
Parameters
id | string | the id to reference the object with a |
object | string | the object to add (will be converted to string using |
Example
const InlineScript = require("koru/server-pages/inline-script");const script = new InlineScript(module.dir); script.add('obj-1', JSON.stringify([1, 2, 3])); script.add('obj-2', "new Date()"); assert.same(script.require('obj-2'), 1); assert.equals(script.modules[script.map['obj-1']], '[1,2,3]'); assert.equals(script.modules[script.map['obj-2']], 'new Date()');
InlineScript#generate()
Generate a source script ready for instertion into a web page.
Parameters
Returns | string |
Example
const InlineScript = require("koru/server-pages/inline-script");const script = new InlineScript(module.dir); script.require('./test-inline/nested'); const ans = vm.runInThisContext(script.generate(), { filename: 'inline', displayErrors: true, timeout: 5000}); assert.equals(ans(), 'nested, level2, simple, simple');
InlineScript#require(id)
Load another module (if not already loaded) and return its body function. require is also available with a define body.
Example
const InlineScript = require("koru/server-pages/inline-script");const script = new InlineScript(module.dir); script.require('./test-inline/nested'); assert.equals(script.map, { 'koru/server-pages/test-inline/simple.js': 0, 'koru/server-pages/test-inline/level2.js': 1, 'koru/server-pages/test-inline/nested.js': 2, }); assert.match(script.modules[2], /Simple = modules\[0\];/); assert.match(script.modules[2], /Level2 = modules\[1\];/); assert.equals(script.modules[0], "'simple'");
Session
The main or active session for client server communication. See webSocketSenderFactory
Properties
_id | string | The Session _id: "default" |
Methods
Session.defineRpc(name, func=util.voidFunc)
Define a remote proceedure call
Example
const Session = require("koru/session/main");refute(Session.isRpcGet("Book.update")); refute(Session.isRpc("Book.update")); Session.defineRpc('Book.update', func); assert.same(Session._rpcs['Book.update'], func); refute(Session.isRpcGet("Book.update")); assert(Session.isRpc("Book.update"));
Session.defineRpcGet(name, func=util.voidFunc)
Define a read-only (GET) remote proceedure call
Example
const Session = require("koru/session/main");Session.defineRpcGet("Book.list", func);// returns koru/session/base
Session.openBatch()
Build an encoded batch message.
Parameters
Returns | object |
|
Example
const Session = require("koru/session/main");const {push, encode} = Session.openBatch(); push(['A', ['Book', {_id: 'book1', title: 'Dune'}]]); push(['R', ['Book', 'book2']]); const msg = encode(); assert.equals(String.fromCharCode(msg[0]), 'W'); assert.equals(message.decodeMessage(msg.subarray(1), Session.globalDict), [ ['A', ['Book', {_id: 'book1', title: 'Dune'}]], ['R', ['Book', 'book2']]]);
init
Attach rpc to a session
Properties
#lastMsgId | Return lastRpc msgId to use with cancelRpc |
Methods
init(session, {rpcQueue=new RPCQueue()}={})
Wire up rpc to a session
Parameters
session | attach rpc methods to this session | |
rpcQueue | queue to store messages yet to have a response. This can be a persistent queue like rcp-idb-queue |
init#cancelRpc(msgId)
Return lastRpc msgId to use with cancelRpc
Example
const init = require("koru/session/client-rpc-base");const init = new init();init.cancelRpc("1rid1");// returns trueinit.cancelRpc("1rid1");init.cancelRpc("2rid1");// returns true
init#checkMsgId(msgId)
Ensure than next msgId will be greater than this one
Parameters
msgId | string |
Example
const init = require("koru/session/client-rpc-base");const init = new init();init.checkMsgId("1rid1");init.checkMsgId("14rid1");init.checkMsgId("15rid1");init.checkMsgId("14rid1");init.checkMsgId("16rid1");
init#replaceRpcQueue(value)
Replace the rpc queue with a different one.
Parameters
value | object |
Example
const init = require("koru/session/client-rpc-base");const init = new init();init.replaceRpcQueue({push: EMPTY_FUNC});
ConnTH
A Helper for Publication tests.
Methods
assert.encodedCall(conn, type, exp)
assert.encodedCall
can be used for determining if a union batchUpdate was called.
Parameters
conn | ServerConnection | |
type | string | |
exp | Array | |
Returns | function |
Example
const ConnTH = require("koru/session/conn-th-server");const conn = ConnTH.mockConnection(); const book1 = await Book.create(); const book2 = await Book.create(); class MyUnion extends Union { async loadInitial(encoder, discreteLastSubscribed) { await Book.query.forEach((doc) => {encoder.addDoc(doc)}); } } const union = new MyUnion(); const sub = new Publication({id: 'sub1', conn}); await union.addSub(sub); assert.encodedCall(conn, 'A', ['Book', {_id: 'book1', name: 'Book 1'}]);
ConnTH.decodeEncodedCall(conn, call)
convert an encoded call back to objects
Parameters
conn | ServerConnection | |
call | Call | |
Returns | object |
Example
const ConnTH = require("koru/session/conn-th-server");const conn = ConnTH.mockConnection(); const book1 = await Book.create(); const book2 = await Book.create(); class MyUnion extends Union { async loadInitial(encoder, discreteLastSubscribed) { await Book.query.forEach((doc) => {encoder.addDoc(doc)}); } } const union = new MyUnion(); const sub = new Publication({id: 'sub1', conn}); await union.addSub(sub); assert.equals(ConnTH.decodeEncodedCall(conn, conn.sendEncoded.firstCall), { type: 'W', data: [ ['A', ['Book', {_id: 'book1', name: 'Book 1'}]], ['A', ['Book', {_id: 'book2', name: 'Book 2'}]], ]});
ConnTH.mockConnection(sessId='s123', session=Session)
Create a connection useful for testing publications.
Parameters
sessId | string | |
[session] | ||
Returns | ServerConnection |
Example
const ConnTH = require("koru/session/conn-th-server");const conn = ConnTH.mockConnection('sess123'); class Library extends Publication { } Library.pubName = 'Library'; const sub1 = await conn.onSubscribe('sub1', 1, 'Library'); after(() => {ConnTH.stopAllSubs(conn)}); assert.same(sub1.conn, conn);
ConnTH.stopAllSubs(conn)
Stop all stubs for a connection. Useful in test teardown to ensure observers have stopped.
Parameters
conn | ServerConnection |
Example
const ConnTH = require("koru/session/conn-th-server");const conn = ConnTH.mockConnection('sess123'); class Library extends Publication { } Library.pubName = 'Library'; const sub1 = await conn.onSubscribe('sub1', 1, 'Library'); const stop = spy(sub1, 'stop'); ConnTH.stopAllSubs(conn); assert.called(stop);
ReverseRpcReceiver
Methods
constructor(session, cmd='F')
Create an reverse rpc handler
Parameters
session | object | used to receive rpc calls on |
[cmd] | string | session command to tie this rpc service to. Defaults to 'F'. |
Example
const ReverseRpcReceiver = require("koru/session/reverse-rpc-receiver");const reverseRpc = new ReverseRpcReceiver(sess); const foo = stub().returns('success'); reverseRpc.define('foo.rpc', foo); const receive = sess._commands.F; assert.isFunction(receive); await receive(['1234', 'foo.rpc', 1, 2]); assert.calledWith(sess.sendBinary, 'F', ['1234', 'r', 'success']); assert.calledWith(foo, 1, 2); assert.same(foo.lastCall.thisValue, sess);
ReverseRpcReceiver#define(name, func)
Set the ServerConnection for the queue. And send any queued messages to it.
Parameters
name | string | |
func | function | |
Returns | ReverseRpcReceiver |
Example
const ReverseRpcReceiver = require("koru/session/reverse-rpc-receiver");const reverseRpc = new ReverseRpcReceiver(sess); function myRpc(arg1, arg2) {} reverseRpc.define('myRpc', myRpc); assert.same(reverseRpc._rpcs.myRpc, myRpc);
ReverseRpcSender
Methods
constructor({conn, cmd='F', rpcQueue=new RPCQueue(cmd)}={})
Create an reverse rpc handler
Parameters
[conn] | ServerConnection | optional ServerConnection |
[cmd] | string | session command to tie this rpc service to. Defaults to 'F'. |
[rpcQueue] | RPCQueue | queue to store messages yet to have a response. This can be a persistent queue. (Defaults to RpcQueue). |
Example
const ReverseRpcSender = require("koru/session/reverse-rpc-sender");const cmd = 'F'; const rpcQueue = new RPCQueue(cmd); const reverseRpc = new ReverseRpcSender({conn, cmd, rpcQueue}); reverseRpc.rpc('foo.rpc', 1, 2); const msgId = '1' + reverseRpc.baseId.toString(36); assert.equals(rpcQueue.get(msgId), [[msgId, 'foo.rpc', 1, 2], null]);
ReverseRpcSender.configureSession(session, cmd='F')
Enable a session to receive reverseRpc callbacks
Parameters
session | object | the session that initiates the callbacks |
cmd | string | session command to tie this rpc service to. Defaults to 'F'. |
Example
const ReverseRpcSender = require("koru/session/reverse-rpc-sender");ReverseRpcSender.configureSession(mySession, 'F'); assert.calledOnceWith(mySession.provide, 'F', m.func);
ReverseRpcSender#rpc(name, ...args)
perform a remote procedure call.
Parameters
name | string | of the method to call |
args | string | the arguments to pass to the method followed by an optional callback |
Returns | string |
Example
const ReverseRpcSender = require("koru/session/reverse-rpc-sender");reverseRpc.rpc('getHealth', 'battery', 'PSU', (err, result) => { if (err != null) { handler.error(err); } else { handler.healthResult(result); } }); assert.calledWith(conn.sendBinary, 'F', ['1' + reverseRpc.baseId, 'getHealth', 'battery', 'PSU']);
ReverseRpcSender#setConn(conn)
Set the ServerConnection for the queue. And send any queued messages to it.
Example
const ReverseRpcSender = require("koru/session/reverse-rpc-sender");const reverseRpc = new ReverseRpcSender(); const conn = {sendBinary: stub()}; reverseRpc.rpc('foo.rpc', 1, 2); reverseRpc.setConn(conn); assert.calledWith(conn.sendBinary, 'F', ['1' + reverseRpc.baseId, 'foo.rpc', 1, 2]); reverseRpc.rpc('another'); assert.calledWith(conn.sendBinary, 'F', ['2' + reverseRpc.baseId, 'another']);
RPCIDBQueue
IndexedDB queue for RPC messages to be sent. This queue is for a persistent indexedDB queue which is suitable for offline support.
Call reload to re-populate the message queue when the app is loaded and before other messages are preloaded.
RpcGet methods are not persisted.
Methods
constructor(qdb)
Build a new queue
Parameters
qdb | object |
Example
const RPCIDBQueue = require("koru/session/rpc-idb-queue");new RPCIDBQueue({result: {_mockdb: MockIndexedDB({_version: 0, restore(){}, _dbs: {foo: Database({_store: {}, _mockdb: {...more}, _version: 0})}, _pending: null}), _store: {}, }, onupgradeneeded: undefined, onsuccess: undefined});
RPCIDBQueue#reload(session)
reload all waiting messages into memory for resend
Parameters
session | object | |
Returns | Promise(undefined) |
Example
const RPCIDBQueue = require("koru/session/rpc-idb-queue");const rPCIDBQueue = new RPCIDBQueue();rPCIDBQueue.reload({state: {incPending: function stub(){}}, sendBinary: sendBinary, checkMsgId: EMPTY_FUNC});// returns undefined
RPCQueue
Default queue for RPC messages to be sent. This queue is for an in memory queue but can be replaced by a persistent queue for offline-mode.
Methods
constructor(cmd='M')
Build a new queue
Parameters
[cmd] |
Example
const RPCQueue = require("koru/session/rpc-queue");new RPCQueue();
RPCQueue#resend(session)
Iterating over the queue returns messages in msgId order.
Parameters
session | object |
Example
const RPCQueue = require("koru/session/rpc-queue");const rPCQueue = new RPCQueue();rPCQueue.resend({sendBinary: sendBinary, checkMsgId: checkMsgId});
ServerConnection
ServerConnection is the server side of a client-server webSocket connection.
Methods
ServerConnection.buildUpdate(dc)
BuildUpdate converts a DocChange object into a update command to send to clients.
Parameters
dc | DocChange | for the document that has been updated | ||||||||
Returns | Array | an update command. Where
|
Example
const ServerConnection = require("koru/session/server-connection");assert.equals(ServerConnection.buildUpdate(DocChange.add(book1)), ['A', ['Book', {_id: 'book1', name: 'Book 1'}]]); assert.equals(ServerConnection.buildUpdate(DocChange.change(book1, {name: 'old name'})), ['C', ['Book', 'book1', {name: 'new name'}]]); assert.equals(ServerConnection.buildUpdate(DocChange.delete(book1)), ['R', ['Book', 'book1', undefined]]); assert.equals(ServerConnection.buildUpdate(DocChange.delete(book1, 'stopped')), ['R', ['Book', 'book1', 'stopped']]);
ServerConnection.filterDoc(doc, filter)
Filter out attributes from a doc. The filtered attributes are shallow copied.
Parameters
doc | object | the document to be filtered. |
filter | object | an Object who properties will override the document. |
Returns | object | an object suitable for sending to client; namely it has an |
Example
const ServerConnection = require("koru/session/server-connection");const doc = { _id: 'book1', constructor: {modelName: 'Book'}, other: 123, attributes: {name: 'The little yellow digger', wholesalePrice: 1095}, }; const filteredDoc = ServerConnection.filterDoc(doc, {wholesalePrice: true}); assert.equals(filteredDoc, { _id: 'book1', constructor: {modelName: 'Book'}, attributes: {name: 'The little yellow digger'}, });
ServerConnection#added(name, attrs, filter)
Send a document added message to client with an optional attribute remove filter
Example
const ServerConnection = require("koru/session/server-connection");const book = {_id: 'id123', title: '1984', published: 1948}; conn.added('Book', book); assert.calledWith(conn.sendBinary, 'A', ['Book', book]); conn.added('Book', book, {published: true}); assert.calledWith(conn.sendBinary, 'A', ['Book', {_id: 'id123', title: '1984'}]);
ServerConnection#batchMessage(type, data)
Batch a binary message and send once current transaction completes successfully. The message is encoded immediately.
Parameters
type | string | the one character type for the message. See base#provide. |
data | Array | a object or primitive to encode and send as a binary message. |
Example
const ServerConnection = require("koru/session/server-connection");TransQueue.transaction(() => { conn.batchMessage('R', ['Foo', {_id: 'foo1'}]); conn.batchMessage('R', ['Foo', {_id: 'foo2'}]); koru.runFiber(() => { TransQueue.transaction(() => { conn.batchMessage('R', ['Bar', {_id: 'bar1'}]); }); }); koru.runFiber(() => { try { TransQueue.transaction(() => { conn.batchMessage('R', ['Nat', {_id: 'nat1'}]); throw 'abort'; }); } catch (ex) { if (ex !== 'abort') throw ex; } }); }); assert.calledTwice(conn.sendEncoded); assert.encodedCall(conn, 'R', ['Foo', {_id: 'foo1'}]); assert.encodedCall(conn, 'R', ['Foo', {_id: 'foo2'}]); assert.encodedCall(conn, 'R', ['Bar', {_id: 'bar1'}]); refute.encodedCall(conn, 'R', ['Nat', {_id: 'nat1'}]);
ServerConnection#changed(name, id, attrs, filter)
Send a document changed message to client with an optional attribute remove filter.
Example
const ServerConnection = require("koru/session/server-connection");conn.changed('Book', 'id123', {title: '1984'}); assert.calledWith(conn.sendBinary, 'C', ['Book', 'id123', {title: '1984'}]); conn.changed('Book', 'id123', {title: '1984', published: 1948}, {published: true}); assert.calledWith(conn.sendBinary, 'C', ['Book', 'id123', {title: '1984'}]);
ServerConnection#removed(name, id, flag)
Send a document removed message to client
Example
const ServerConnection = require("koru/session/server-connection");const serverConnection = new ServerConnection();conn.removed('Book', 'id123'); assert.calledWith(conn.sendBinary, 'R', ['Book', 'id123', undefined]);conn.removed('Book', 'id123', 'stopped'); assert.calledWith(conn.sendBinary, 'R', ['Book', 'id123', 'stopped']);
ServerConnection#send(type, data)
Send a text message to the client.
Parameters
type | string | the one character type for the message. See base#provide. |
data | string | the text message to send. |
Example
const ServerConnection = require("koru/session/server-connection");conn.send('X', 'FOO'); assert.calledWith(v.ws.send, 'XFOO');
ServerConnection#sendBinary(type, data)
Send a object to client as a binary message.
Parameters
type | string | the one character type for the message. See base#provide. |
[data] | any-type | a object or primitive to encode and send as a binary message. |
Example
const ServerConnection = require("koru/session/server-connection");conn.sendBinary('M', [1, 2, 3]);
ServerConnection#sendEncoded(msg)
Send a pre encoded binary message to the client.
Parameters
msg | string |
Example
const ServerConnection = require("koru/session/server-connection");conn.sendEncoded('myMessage'); assert.calledWith(conn.ws.send, 'myMessage', {binary: true});
webSocketSenderFactory
Build WebSocket clients (senders).
Methods
webSocketSenderFactory( _session, sessState, execWrapper=koru.fiberConnWrapper, base=_session)
Example
const webSocketSenderFactory = require("koru/session/web-socket-sender-factory");const mySession = webSocketSenderFactory(new SessionBase('foo'), stateFactory()); const wsConnection = {}; mySession.newWs = stub().returns(wsConnection); mySession.start(); assert.called(mySession.newWs); assert.same(wsConnection.binaryType, 'arraybuffer');
Stacktrace
Stacktrace provides methods to normalize stack frames from different browsers and nodejs. Koru normalizes error messages before displaying them in logs using util.extractError and Core::AssertionError.
Methods
Stacktrace.elideFrames(error, count)
Elide the top count
frames from an error
's normalized stack frame
Parameters
error | AssertionError | the Error to elide from |
count | number |
Example
const Stacktrace = require("koru/stacktrace");const inner1 = () => {inner2()}; const inner2 = () => {inner3()}; const inner3 = () => { throw new AssertionError('I have a shortened customStack'); }; try { inner1(); } catch (err) { Stacktrace.elideFrames(err, 2); assert.equals(Stacktrace.normalize(err), [ m(/ at .*inner1.* \(koru\/stacktrace-test.js:\d+:\d+\)/), m(/ at .*((Test\.)?test elideFrames|anonymous).* \(koru\/stacktrace-test.js:\d+:\d+\)/), ]); }
Stacktrace.normalize(error)
Normalize the stack trace for an error
. See elideFrames and replaceStack for
manipulation of the stack trace. By default other frames are elided from the trace when
they are part of Koru's internal workings; to see a full stack trace set
util.FULL_STACK to true.
Parameters
error | AssertionError | |
Returns | Array | a normalized array of stack frames |
Example
const Stacktrace = require("koru/stacktrace");const inner1 = () => {inner2()}; const inner2 = () => {inner3()}; const inner3 = () => { throw new AssertionError('I failed'); }; try { inner1(); } catch (err) { assert.equals(Stacktrace.normalize(err), [ m(/ at .*inner3.* \(koru\/stacktrace-test.js:\d+:\d+\)/), m(/ at .*inner2.* \(koru\/stacktrace-test.js:\d+:\d+\)/), m(/ at .*inner1.* \(koru\/stacktrace-test.js:\d+:\d+\)/), m(/ at .*((Test\.)?test normalize|anonymous).* \(koru\/stacktrace-test.js:\d+:\d+\)/), ]); assert.same(Stacktrace.normalize(err), Stacktrace.normalize(err)); }
Stacktrace.replaceStack(error, replacementError)
Replace the normalized stack frame.
Parameters
error | Error | the Error who's frame is to be replaced. |
replacementError | Error | the Error with the normalized stack frame to use. |
Example
const Stacktrace = require("koru/stacktrace");const inner1 = () => {inner2()}; const inner2 = () => {inner3()}; const inner3 = () => { const err = new Error('I failed'); const err2 = (() => new Error('I use another stack'))(); Stacktrace.replaceStack(err2, err); assert.same(Stacktrace.normalize(err2), Stacktrace.normalize(err)); }; inner1();
Symbols
Well known koru symbols.
Properties
ctx$ | symbol | The Ctx of a HTML element |
endMarker$ | symbol | Associate a start with an end; used with HTML elements |
error$ | symbol | Used to hold errors on a model |
inspect$ | symbol | Override the util.inspect value displayed |
original$ | symbol | store the original value on a change |
private$ | symbol | Private values associated with an object |
stopGap$ | symbol | A interim value for an object; used with model |
stubName$ | symbol | Used to name stubbed functions in API documentation |
withId$ | symbol | Used to associate and id with an object. See util.withId |
API
API is a semi-automatic API document generator. It uses unit-tests to determine types and values at test time.
Method document comments need to be inside the test method; not in the production code and not outside the test method.
Examples can be verbatim from the test method by surrounding the example between //[
and
//]
comments. The special comment //[no-more-examples]
will discard any further examples in
this scope (useful with group tests and topics).
Methods
API.class(options)
Document constructor
for the current subject.
Parameters
[options.sig] | string / function | override the call signature. Sig is used as the subject if it is a function. |
[options.intro] | string / function | the abstract for the method being documented. Defaults to test comment. |
Returns | object | a ProxyClass which is to be used instead of |
Example
const API = require("koru/test/api");const Book = API.class(); const book = new Book('There and back again'); assert.same(book.title, 'There and back again');
API.comment(comment)
Add a comment before the next example
Parameters
comment | string |
Example
const API = require("koru/test/api");class Excercise { static register(name, minutes) {} begin() {} } API.module({subjectModule: {id: 'myMod', exports: Excercise}}); API.method('register'); API.comment('Optionally set the default duration'); Excercise.register('Jogging', 5); // This call gets the comment Excercise.register('Skipping'); // This call get no comment
API.custom(func, options)
Document a custom function in the current module
Parameters
func | function | the function to document |
[options.name] | string | override the name of func |
[options.sig] | string | replace of prefix the function signature. If it ends with a ".", "#" or a ":" then it will prefix otherwise it will replace. |
[options.intro] | function / string | the abstract for the method being documented. Defaults to test comment. |
Returns | function | a ProxyClass which is to be used instead of |
Example
const API = require("koru/test/api");function myCustomFunction(arg) { this.ans = arg; return 'success'; } const thisValue = {}; let proxy = API.custom(myCustomFunction); proxy.call(thisValue, 2); assert.same(thisValue.ans, 2); proxy = API.custom(myCustomFunction, {name: 'example2', sig: 'foobar = function example2(arg)'}); proxy.call(thisValue, 4); assert.same(thisValue.ans, 4); proxy = API.custom(myCustomFunction, {name: 'example3'}); proxy.call(thisValue, 4); assert.equals(API.instance.customMethods.example3.sig, 'example3(arg)'); proxy = API.custom(myCustomFunction, {name: 'example4', sig: 'Container#'}); proxy.call(thisValue, 4); assert.equals(API.instance.customMethods.example4.sigPrefix, 'Container#'); assert.equals(API.instance.customMethods.example4.sig, 'example4(arg)'); proxy = API.custom(myCustomFunction, {name: 'example5', sig: 'Container.'}); proxy.call(thisValue, 4); assert.equals(API.instance.customMethods.example5.sigPrefix, 'Container.'); assert.equals(API.instance.customMethods.example5.sig, 'example5(arg)'); proxy = API.custom(myCustomFunction, {name: 'example6', sig: 'Container.foo()'}); proxy.call(thisValue, 4); assert.equals(API.instance.customMethods.example6.sigPrefix, 'Container.'); assert.equals(API.instance.customMethods.example6.sig, 'foo()');
API.customIntercept(object, options)
Intercept a function and document it like custom.
Parameters
object | object | the container of the function to document |
[options.name] | string | the name of function to intercept defaults to test name |
[options.sig] | string | replace of prefix the function signature. If it ends with a ".", "#" or a ":" then it will prefix otherwise it will replace. |
[options.intro] | function / string | the abstract for the method being documented. Defaults to test comment. |
Returns | function | the original function |
Example
const API = require("koru/test/api");class Book { print(copies) { return printer.print(this, copies); } } API.module({subjectModule: {id: 'myMod', exports: {}}}); const thisValue = {}; let orig = API.customIntercept(Book.prototype, {name: 'print', sig: 'Book#'}); const book = new Book(); assert.same('success', book.print(2));
API.example(body)
Run a section of as an example of a method call.
Use API.exampleCont(body)
to continue an example.
Alternately the special comments //[
, //[#
and //]
can be used instead. //[
records
a new example, //[#
continues an example and //]
stops recording.
//[ let Iam = 'example one'; //] Iam = 'outside an example'; //[# Iam = 'continuing example one'; //] //[ Iam = 'example two'; //]
Example
const API = require("koru/test/api");class Color { static define(name, value) { return this.colors[name] = value; } // ... } Color.colors = {}; API.module({subjectModule: {id: 'myMod', exports: Color}}); API.method('define'); API.example('const foo = "can put any valid code here";'); API.example(() => { // this body of code is executed Color.define('red', '#f00'); Color.define('blue', '#00f'); }); API.exampleCont('// comment\n'); API.exampleCont(() => {assert.same(Color.colors.red, '#f00')}); assert.same(API.example(() => {return Color.define('green', '#0f0')}), '#0f0');
API.innerSubject(subject, subjectName, options)
Document a subject within a module.
Parameters
subject | object / string | either the actual subject or the property name of the subject if accessible from the current subject |
[subjectName] | string / null | override the subject name |
[options.info] | string | property info line (if subject is a |
[options.abstract] | function | introduction to the subject. If abstract is a |
[options.initExample] | string | code that can be used to initialize |
[options.initInstExample] | string | code that can be used to initialize an instance of
|
Returns | API | an API instance for the given |
Example
const API = require("koru/test/api");class Book { constructor() {this._chapters = []} newChapter() { const chapter = new this.constructor .Chapter(10); this._chapters.push(chapter); return chapter; } } Book.Chapter = class { constructor(startPage) {this.page = startPage} goto() {return this.page} }; API.module({ subjectModule: {id: 'myMod1', exports: Book}, subjectName: 'myHelper'}); API.innerSubject('Chapter', null, { info: 'Chapter info', }) .protoMethod('goto'); const book = new Book(); const chapter = book.newChapter(); assert.same(chapter.goto(), 10);
API.method(methodName, options)
Document methodName
for the current subject
Example
const API = require("koru/test/api");API.method("fnord");API.method("foo", {subject: {foo(){}}, intro: "intro"});
API.module({subjectModule, subjectName, pseudoModule, initExample, initInstExample}={})
Initiate documentation of the module. Subsequent calls to API
methods will act of the given module
.
Parameters
[subjectModule] | object | defaults to the module corresponding to the current test module. |
[subjectName] | string | defaults to a hopefully reasonable name |
[pseudoModule] | ||
[initExample] | string | code that can be used to initialize |
[initInstExample] | string | code that can be used to initialize an instance of
|
Returns | API |
Example
const API = require("koru/test/api");API.module();// returns API(API)API.module({subjectModule: {exports: {clean(){}}, id: 'myMod1'}, subjectName: "myHelper", initExample: "Init example", initInstExample: "Init inst example"});// returns API(myHelper)API.module({subjectModule: {exports: function Book(){}, id: 'myMod2'}});// returns API(Book)
API.property(name, options)
Example
const API = require("koru/test/api");const defaults = { logger() {}, width: 800, height: 600, theme: { name: 'light', primaryColor: '#aaf', }, }; let name, logger; API.module({subjectModule: {id: 'myMod', exports: defaults}, subjectName: 'defaults'}); API.property('theme', { info: 'The default theme', properties: { name: (value) => { name = value; return 'The theme name is ${value}'; }, primaryColor: 'The primary color is ${value}', }, }); assert.same(name, 'light'); API.property('logger', (value) => { logger = value; return 'The default logger is ${value}'; }); assert.same(logger, defaults.logger);test({'pageCount'() { /** * The number of pages in the book **/ const book = {pageCount: 400}; API.module({ subjectModule: {id: 'myMod', exports: book}, subjectName: 'Book'}); API.property('pageCount'); // extracts the comment above assert.equals(book.pageCount, 400); }});const book = { get title() {return this._title}, set title(value) {this._title = value}, }; API.module({ subjectModule: {id: 'myMod', exports: book}, subjectName: 'Book'}); API.property('title', { info: 'Get/set the book title', }); API.comment('sets title'); book.title = 'Room'; assert.same(book._title, 'Room'); assert.same(book.title, book._title);
API.protoMethod(methodName, options)
Document prototype methodName
for the current subject
Parameters
[methodName] | string | the name of the prototype method to document |
[options.subject] | object | override the instance to document. This
defaults to |
[options.intro] | function / string | the abstract for the method being documented. Defaults to test comment. |
Example
const API = require("koru/test/api");class Tree { constructor(name) { this.name = name; this.branches = 10; } prune(branchCount) { return this.branches -= branchCount; } async graft(branchCount) { return this.branches += branchCount; } } API.module({subjectModule: {id: 'myMod', exports: Tree}}); API.protoMethod('prune'); const plum = new Tree('Plum'); assert.same(plum.prune(3), 7); assert.same(plum.prune(2), 5); /** Overriding subject.prototype **/ const subject = {anything() {return 'I could be anything'}}; API.protoMethod('anything', {subject}); assert.same(subject.anything(), 'I could be anything');
API.protoProperty(name, options, subject)
Document a property of the current subject's prototype. The property can be either plain value or a get/set function.
See property
Parameters
name | string | the property to document |
[options] | object | see property |
[subject] | object | defaults to subject.prototype |
Example
const API = require("koru/test/api");class Book { constructor(title) { this._title = title; } get title() {return this._title} } API.module({subjectModule: {id: 'myMod', exports: Book}}); const book = new Book('Jungle Book', 504); API.protoProperty('title', {info: 'The title'}); book.bookMark = 100; API.protoProperty('bookMark', {info: 'record page'}, book); assert.same(book.title, 'Jungle Book');
API.topic(options)
Record a test as a topic for insertion into a method test document comment using the syntax
{{topic:[<path>:]<topicName>}}
and {{example:<number>}}
Parameters
[options.name] | string | override the name of the topic. Defaults to test name. |
[options.intro] | function / string | the narrative for the topic being documented. Defaults to test comment. |
Returns | object |
Example
const API = require("koru/test/api");test('borrow book', () => { 'use strict'; /** * In order to borrow a book it needs to be found in the library; then call the borrow * method on it: * * {{example:0}} * * When the book has been read it can be returned to the library: * * {{example:1}} **/ API.topic(); let receipt; //[ const book = Library.findBook('Dune'); if (book !== undefined) { receipt = book.borrow(); } //] //[ receipt.returnBook(); //] assert.equals(receipt.book, Library.findBook('Dune')); }); test('findBook', () => { /** * After browsing the library you may want to borrow a book. * * {{topic:borrow book}} (or with path {{topic:models/library:borrow book}}) **/ API.method(); const book = Library.findBook('Dune'); assert(book); });
Core
The heart of the test framework.
Properties
AssertionError | AssertionError | the AssertionError class |
__elidePoint | AssertionError | The current |
match | function | A clone of the match framework. This is conventionally assigned to the constant |
test | Test | The currently running test |
Methods
assert.elide(body, adjust=0)
Elide stack starting from caller
Parameters
body | function | the elided body to excute |
[adjust] | number | the number of additional stack frames to elide. |
Example
const Core = require("koru/test/core");const inner = () => { assert.fail('I failed'); }; let ex; try { (() => { assert.elide(() => { inner(); }, 1); })(); } catch (e) { ex = e; } assert.instanceof(ex, AssertionError); assert.same(ex.message, 'I failed'); assert.equals(ex.customStack, new AssertionError('I failed', 1).customStack);
assert.fail(message='failed', elidePoint=0)
throw assertionError
Example
const Core = require("koru/test/core");let ex; const inner1 = () => {inner2()}; const inner2 = () => { try { assert.fail('I failed', 1); } catch (e) { ex = e; } }; inner1(); assert.instanceof(ex, AssertionError); assert.same(ex.message, 'I failed'); assert.equals(Stacktrace.normalize(ex), [ m(/^ at.*inner1.*core-test.js/), m(/^ at.*core-test.js/), ]);
Core.assert(truth, msg='Expected truthness')
Assert is truthy. Contains methods for more convenient assertions in assert. assert
is a global method.
Parameters
truth | boolean |
|
[msg] | string | A message to display if the assertion fails |
Returns | number / boolean |
Example
const Core = require("koru/test/core");let ex;try { assert(1, 'I succeeded'); assert(true, 'So did I'); assert(0, 'I failed'); } catch (e) { ex = e; }assert.instanceof(ex, AssertionError); assert.same(ex.message, 'I failed');
Core.deepEqual(actual, expected, hint, hintField, maxLevel=util.MAXLEVEL)
Like util.deepEqual except allows a hint to show where values don't match.
Parameters
actual | null / object / number / Array | |
expected | null / undefined / string / object / number / Array | |
[hint] | object | |
[hintField] | string | |
[maxLevel] | ||
Returns | boolean |
Example
const Core = require("koru/test/core");Core.deepEqual(null, null);// returns trueCore.deepEqual(null, undefined);// returns trueCore.deepEqual(null, "");// returns falseCore.deepEqual({}, {});// returns trueCore.deepEqual(0, 0);// returns falseCore.deepEqual({a: 0}, {a: 0});// returns falseCore.deepEqual({a: null}, {b: null}, {}, "keyCheck");// returns falseCore.deepEqual([1, 2, null], [1, match((v) => v % 2 === 0), match.any]);// returns trueCore.deepEqual([1, 1], [1, match((v) => v % 2 === 0)]);// returns falseCore.deepEqual([2, 2], [1, match((v) => v % 2 === 0)]);// returns falseCore.deepEqual({a: 1, b: {c: 1, d: [1, {e: [false]}]}}, {a: 1, b: {c: 1, d: [1, {e: [false]}]}});// returns trueCore.deepEqual({a: 1, b: {c: 1, d: [1, {e: [false]}]}}, {a: 1, b: {c: 1, d: [1, {e: [true]}]}});// returns falseCore.deepEqual({a: 1, b: {c: 0, d: [1, {e: [false]}]}}, {a: 1, b: {c: 0, d: [1, {e: [false]}]}});// returns falseCore.deepEqual({a: 1, b: {c: 1, d: [1, {e: [false]}]}}, {a: 1, b: {c: 1, d: [1, {e: [false], f: undefined}]}});// returns falseCore.deepEqual({a: 1}, {a: "1"});// returns false
Core.refute(truth, msg)
Assert is falsy. Contains methods for more convenient assertions in assert. refute
is a global method.
Parameters
truth | boolean |
|
msg | A message to display if the assertion fails |
Example
const Core = require("koru/test/core");let ex;try { refute(0, 'I succeeded'); refute(false, 'So did I'); refute(true, 'I failed'); } catch (e) { ex = e; }assert.instanceof(ex, AssertionError); assert.same(ex.message, 'I failed');
AssertionError
An error thrown by an assertion methods.
Properties
#customStack | The normalized stack trace with the elided frames and message |
Methods
constructor(message, elidePoint=0)
Create an AssertionError.
Parameters
message | string | the message for the error |
[elidePoint] | number / AssertionError | if a |
Example
const Core = require("koru/test/core");const inner1 = () => {inner2()}; const inner2 = () => {inner3()}; const inner3 = () => { const err = new AssertionError('I failed'); assert(err instanceof Error); assert.same(err.message, 'I failed'); assert.equals(Stacktrace.normalize(err), [ m(/ at .*inner3.* \(koru\/test\/core-test.js:\d+:\d+\)/), m(/ at .*inner2.* \(koru\/test\/core-test.js:\d+:\d+\)/), m(/ at .*inner1.* \(koru\/test\/core-test.js:\d+:\d+\)/), m(/ at .* \(koru\/test\/core-test.js:\d+:\d+\)/), ]); const err2 = new AssertionError('I have a shortened customStack', 2); assert.equals(Stacktrace.normalize(err).slice(2), Stacktrace.normalize(err2)); const err3 = (() => new AssertionError('I use another stack', err2))(); assert.same(Stacktrace.normalize(err3), Stacktrace.normalize(err2)); }; inner1();
MockPromise
Methods
MockPromise._poll()
Synchronously run any outstanding promises.
Example
const MockPromise = require("koru/test/mock-promise");// How to stub a Promise TH.stubProperty((isServer ? global : self), 'Promise', {value: MockPromise}); let done = false; Promise.resolve(true).then(v => done = v); assert.isFalse(done); Promise._poll(); assert.isTrue(done);
Stubber
Spies and Stubs are functions which record the call arguments, the this
value and the return
of each call made to them. They can intercept calls to Object getter, setter and function
properties.
Spies will just record the interaction whereas Stubs will replace the call with a custom function.
A Spy is an instance of a Stub.
Stubs are usually called from tests using the specialized version in test. The test versions will auto restore after the Test or TestCase has completed.
const {stub, spy, intercept} = TH;
Methods
Stubber.intercept(object, prop, replacement=() => {}, restore)
Create an intercept. An intercept is a lightweight stub. It does not record any calls.
Example
const Stubber = require("koru/test/stubber");const Book = { lastReadPage: 0, read(pageNo) {this.lastReadPage = pageNo}, }; stubber.intercept(Book, 'read', function (n) {this.lastReadPage = n - 2}); Book.read(28); assert.same(Book.lastReadPage, 26); Book.read.restore(); Book.read(28); assert.same(Book.lastReadPage, 28);
Stubber.isStubbed(func)
Determine if a function is stubbed.
Example
const Stubber = require("koru/test/stubber");const book = { read() {}, }; assert.isFalse(stubber.isStubbed(book.read)); stubber.stub(book, 'read'); assert.isTrue(stubber.isStubbed(book.read));
Stubber.spy(object, property)
Create a spy. A spy is a Stub that calls the original function.
Example
const Stubber = require("koru/test/stubber");const Book = { lastReadPage: 0, read(pageNo) {this.lastReadPage = pageNo}, }; const read = stubber.spy(Book, 'read'); assert.same(read, Book.read); Book.read(28); assert.same(Book.lastReadPage, 28); assert.calledWith(read, 28);
Stubber.stub(object, property, repFunc)
Create a Stub. Can stub a object function or be unattached.
Example
const Stubber = require("koru/test/stubber");const standalone = stubber.stub(); standalone(); assert.called(standalone); const privateMethod$ = Symbol(); const Book = { lastReadPage: 0, read(pageNo) {this.lastReadPage = pageNo}, [privateMethod$]() {}, }; const read = stubber.stub(Book, 'read', (n) => n * 2); assert.same(read, Book.read); assert.equals(Book.read(28), 56); assert.same(Book.lastReadPage, 0); assert.calledWith(read, 28); const pm = stubber.stub(Book, privateMethod$); Book[privateMethod$](); assert.called(pm);
Stub
Stub and Spy methods.
Properties
#callCount | number | How many times the stub has been called |
#called | boolean | Has the stub been called at least once |
#calledOnce | boolean | Has the stub been called exactly once |
#calledThrice | boolean | Has the stub been called exactly 3 times |
#calledTwice | boolean | Has the stub been called exactly twice |
#firstCall | Call | The first call to the stub |
#lastCall | Call | The last call to the stub |
Methods
Stub#restore()
Restore the original function
Example
const Stubber = require("koru/test/stubber");let args; const obj = {foo() {args = 'aStub'}}; stubber.stub(obj, 'foo', (a, b, c) => {args = [a, b, c]});obj.foo(1, 2, 3); assert.equals(args, [1, 2, 3]); assert.calledWith(obj.foo, 1, 2, 3); obj.foo.restore(); obj.foo(1, 2, 3); assert.equals(args, 'aStub');
Stub#calledAfter(before)
Test this stub is called after before
.
Example
const Stubber = require("koru/test/stubber");aStub = stubber.stub(); //[ aStub(1, 2, 3); aStub(4, 5, 6); //]const stub2 = stubber.stub(); stub2(); assert.isTrue(stub2.calledAfter(aStub));
Stub#calledBefore(after)
Test this stub is called before after
.
Example
const Stubber = require("koru/test/stubber");aStub = stubber.stub(); //[ aStub(1, 2, 3); aStub(4, 5, 6); //]const stub2 = stubber.stub(); stub2(); assert.isTrue(aStub.calledBefore(stub2));
Stub#calledWith(...args)
Was the stub called with the given args
.
Parameters
args | number | the args to test were passed; extra args in the call will be ignored. |
Returns | boolean | true when the stub was called with |
Example
const Stubber = require("koru/test/stubber");aStub = stubber.stub(); //[ aStub(1, 2, 3); aStub(4, 5, 6); //]const aStub = stubber.stub(); aStub(1, 2, 3); aStub(4, 5, 6); assert.isTrue(aStub.calledWith(1, 2)); assert.isTrue(aStub.calledWith(1, 2, 3)); assert.isTrue(aStub.calledWith(4)); assert.isFalse(aStub.calledWith(1, 2, 3, 4)); assert.isFalse(aStub.calledWith(5));
Stub#calledWithExactly(...args)
Was the stub called with the given args
and no extra args.
Example
const Stubber = require("koru/test/stubber");aStub = stubber.stub(); //[ aStub(1, 2, 3); aStub(4, 5, 6); //]const aStub = stubber.stub(); aStub(1, 2, 3); aStub(4, 5, 6); assert.isTrue(aStub.calledWithExactly(1, 2, 3)); assert.isTrue(aStub.calledWithExactly(4, 5, 6)); assert.isFalse(aStub.calledWithExactly(1, 2)); assert.isFalse(aStub.calledWithExactly(1, 2, 3, 4)); assert.isFalse(aStub.calledWithExactly(4, 5));
Stub#cancelYields()
remove automatic yield
Parameters
Returns | Stub | the stub |
Example
const Stubber = require("koru/test/stubber");const aStub = stubber.stub(); aStub.yields(1); aStub.cancelYields(); let result = 0; aStub(() => {result = 1}); assert.same(result, 0);
Stub#getCall(index)
Get the details of a particular call to stub.
Example
const Stubber = require("koru/test/stubber");aStub = stubber.stub(); //[ aStub(1, 2, 3); aStub(4, 5, 6); //]assert.equals(aStub.getCall(1).args, [4, 5, 6]);
Stub#invokes(callback)
Invoke callback
when the stub is called. The callback's result will be returned to the
stub caller.
Example
const Stubber = require("koru/test/stubber");function callback(call) { return this === call && call.args[0] === 1; } const aStub = stubber.stub(); aStub.invokes(callback); assert.isFalse(aStub(2)); assert.isTrue(aStub(1)); assert.same(aStub.firstCall.returnValue, false); assert.same(aStub.lastCall.returnValue, true);
Stub#onCall(count)
Create a refined stub for a particular call repetition.
Parameters
count | number | the |
Returns | Stub | the new stub controlling the |
Example
const Stubber = require("koru/test/stubber");const aStub = stubber.stub().returns(null); const stub0 = aStub.onCall(0).returns(0); aStub.withArgs(1) .onCall(0).returns(1) .onCall(1).returns(2); assert.same(aStub(), 0); assert.same(aStub(1, 4), 1); assert.same(aStub(1), 2); assert.calledOnce(stub0); assert.same(stub0.subject, aStub); assert.same(aStub.callCount, 3); const call = aStub.getCall(1); assert(call.calledWith(1, 4));
Stub#removeSelected(selector)
Search calls using selector
function to select which call to remove from calls.
Parameters
selector | function | the function given a |
Returns | Call | the call that is removed. |
Example
const Stubber = require("koru/test/stubber");const obj = stub(); obj(1); obj(3); obj(2); assert.same( obj.removeSelected((c) => c.args[0] == 4), void 0, ); assert.same(obj.callCount, 3); const call = obj.removeSelected((c) => c.args[0] < 3); assert.same(call.args[0], 2); assert.same(obj.callCount, 2); assert.same(obj.firstCall.args[0], 1); assert.same(obj.lastCall.args[0], 3);
Stub#returns(arg)
Control what value a stub returns.
Example
const Stubber = require("koru/test/stubber");const aStub = stubber.stub().returns(null); aStub.returns('default').onCall(2).returns('two'); assert.same(aStub(), 'default'); assert.same(aStub(), 'default'); assert.same(aStub(), 'two'); assert.same(aStub(), 'default');
Stub#withArgs(...args)
Create a refined stub (or spy) that relates to a particular list of call arguments. This stub will only be invoked if the subject is called with a list of arguments that match.
Parameters
args | any-type | each arg is tested against the call to determine if this stub should be used. Matchers can be used as arguments. |
Returns | Stub | the new sub-stub |
Example
const Stubber = require("koru/test/stubber");const stub = new Stub();stub.withArgs("foo");// returns spystub.withArgs("bar");// returns spystub.withArgs(match.number, match.string);// returns spy
Stub#yield(...args)
Call the first callback of the first call to stub.
Parameters
args | any-type | the arguments to pass to the callback. |
Returns | any-type | the result from the callback. |
Example
const Stubber = require("koru/test/stubber");const aStub = stubber.stub(); let result; aStub((arg1, arg2) => result = arg2); assert.same( aStub.yield(1, 2), 2); assert.same(result, 2);
Stub#yieldAll(...args)
Like yield but yields for all calls to stub; not just the first.
Example
const Stubber = require("koru/test/stubber");const stub = new Stub();stub.yieldAll(1, 2);// returns stub
Stub#yieldAndRemoveSelected(match, ...args)
Like yieldAndReset but search calls using selector
function to select which call to
yield and remove from calls.
Parameters
match | function | |
args | any-type | the arguments to pass to the callback. |
Returns | any-type | the result from the callback. |
Example
const Stubber = require("koru/test/stubber");const func1 = stub(); const func2 = (...args) => args.join(','); const func3 = stub(); const obj = stub(); const matches = (c) => c.args[0] < 3; assert.exception( () => obj.yieldAndRemoveSelected(matches, 6, 7, 8), {message: 'No matching call found'}, ); obj(1, func1); obj(3, func3); obj(2, func2); assert.same(obj.callCount, 3); const ans = obj.yieldAndRemoveSelected(matches, 6, 7, 8); assert.equals(ans, '6,7,8'); refute.called(func1); refute.called(func3); assert.same(obj.callCount, 2);
Stub#yieldAndReset(...args)
Like yield but also calls reset on the stub.
Parameters
[args] | any-type | the arguments to pass to the callback. |
Returns | any-type | the result from the callback. |
Example
const Stubber = require("koru/test/stubber");const obj = {run(arg) {}}; const aStub = stub(obj, 'run'); let arg2; obj.run((a1, a2) => arg2 = a2); assert.equals(aStub.yieldAndReset(1, 2), 2); assert.same(arg2, 2); refute.called(aStub); obj.run((_) => 3); assert.equals(aStub.yieldAndReset(), 3);
Stub#yields(...args)
Trigger the stub automatically to call the first callback.
Example
const Stubber = require("koru/test/stubber");const aStub = stubber.stub(); aStub.yields(1, 2, 3); let result; aStub(1, (a, b, c) => result = [c, b, a], () => 'not me'); assert.equals(result, [3, 2, 1]);
Call
Details of a Stub call.
Properties
#args | Array | the args of the call |
#globalCount | number | The index in the sequence of all stub calls since the start of the program |
#thisValue | object | the |
Methods
Call#calledWith(...args)
Was this call called with the given args
.
Parameters
args | number | the args to test were passed; extra args in the call will be ignored. |
Returns | boolean | the call matches the arg list. |
Example
const Stubber = require("koru/test/stubber");const aStub = stubber.stub(); aStub(1, 2, 3); aStub(4, 5, 6); const {firstCall} = aStub; assert.isTrue(firstCall.calledWith(1, 2)); assert.isTrue(firstCall.calledWith(1, 2, 3)); assert.isFalse(firstCall.calledWith(1, 2, 3, 4)); assert.isFalse(firstCall.calledWith(4));
Call#yield(...args)
Trigger the stub automatically to call the first callback.
Example
const Stubber = require("koru/test/stubber");const aStub = stubber.stub(); let arg2; aStub(1, 2, (a, b) => arg2 = b); assert.equals(aStub.firstCall.yield(4, 5), 5); assert.same(arg2, 5);
TestCase
A group of tests. Most methods are for internal purposes and are not documented here.
See TestHelper
Properties
#moduleId | string | the id of the module this test-case is defined in |
Methods
TestCase#fullName(name)
Get the name of the test-case prefixed by the parent test-cases (if any).
Example
const TestCase = require("koru/test/test-case");const testCase = new TestCase();testCase.fullName();// returns "koru/test/test-case sub-test-case"testCase.fullName("sub-test-case");// returns "koru/test/test-case sub-test-case"
TestCase#topTestCase()
Retrieve the top most TestCase
Parameters
Returns | test-case-test |
Example
const TestCase = require("koru/test/test-case");const testCase = new TestCase();testCase.topTestCase();// returns koru/test/test-case-test
Test
The Test facilitator responsible for running an individual test. Use
TestHelper.test to access it. It can also be accessed from
koru._TEST_.test
.
Most methods are for internal purposes and are not documented here. But the name is useful for debugging.
TestHelper
The Base TestHelper for all tests.
The standard layout for a test file of say: my-module.js
is called my-module-test.js
and is
structured as follows:
define((require, exports, module)=>{
'use strict';
const TH = require('test-helper'); // prefix test-helper with path to helper
const {stub, spy, util, match: m} = TH;
const MyModule = require('./my-module');
TH.testCase(module, ({before, after, beforeEach, afterEach, group, test})=>{
beforeEach(()=>{
});
afterEach(()=>{
});
test("foo", ()=>{
assert.equals(MyModule.foo(), "bar");
});
});
});
See the Test Guide for more details about writing tests.
Properties
match | function | A copy of the match module for using with tests.
This is normally assigned to |
test | Test | The current test that is running |
util | util-base | A convenience reference to util |
Methods
TestHelper.after(callback)
Run callback
after the test/test-case has completed.
Aliases
onEnd
[deprecated]
Example
const TestHelper = require("koru/test-helper");const Library = { onAdd() {//... return {//... stop() {//... }}; }, removeAllBooks() {//... }, }; const listener = Library.onAdd(); TH.after(listener); TH.after(()=> {Library.removeAllBooks()});
TestHelper.intercept(...args)
A wrapper around Stubber.intercept that automatically restores after the test/test-case has completed.
Parameters
args | same as for Stubber.intercept | |
Returns | function |
Example
const TestHelper = require("koru/test-helper");TestHelper.intercept({bar: bar}, "bar");// returns replacement
TestHelper.spy(...args)
A wrapper around Stubber.spy that automatically restores after the test/test-case has completed.
Parameters
args | same as for Stubber.spy | |
Returns | Stub |
Example
const TestHelper = require("koru/test-helper");TestHelper.spy({bar: bar}, "bar");// returns stub
TestHelper.stub(...args)
A wrapper around Stubber.stub that automatically restores after the test/test-case has completed.
Parameters
args | same as for Stubber.stub | |
Returns | Stub |
Example
const TestHelper = require("koru/test-helper");TestHelper.stub({bar: bar}, "bar");// returns stub
TestHelper.stubProperty(object, prop, newValue)
A wrapper around util.setProperty that automatically restores after the test/test-case has completed.
Parameters
object | object | |
prop | string | |
newValue | object | |
Returns | function | a function to restore property to original setting. |
Example
const TestHelper = require("koru/test-helper");const foo = {get bar() {return 'orig'}}; const restore = TH.stubProperty(foo, 'bar', {value: 'new'}); assert.equals(foo.bar, 'new'); restore(); assert.equals(foo.bar, 'orig');
TestHelper.testCase(module, body)
Parameters
module | object | the module for the test file. |
body | function | A function that will add tests. It is passed an object with the following properties:
|
Returns | TestCase | a test-case instance. |
Example
const TestHelper = require("koru/test-helper");TestHelper.testCase({id: "my-module-test"}, body);// returns {body(){}, level: 0, name: 'my-module', tc: undefined}
MockModule
For when you want to mock a module
App
App wide features
Methods
App.flashUncaughtErrors()
Use flash to display uncaught errors
Parameters
Returns | function |
Example
const App = require("koru/ui/app");after(App.flashUncaughtErrors()); stub(Flash, 'error'); stub(Flash, 'notice'); stub(koru, 'error'); koru.unexpectedError("user message", "log message"); assert.calledWith(Flash.error, "unexpected_error:user message"); assert.calledWith(koru.error, "Unexpected error", 'log message'); Flash.error.reset(); koru.error.reset(); koru.globalErrorCatch(new koru.Error(400, {name: [['is_invalid']]})); refute.called(Flash.error); assert.calledWith(Flash.notice, "Update failed: name: is not valid"); assert.calledWith(koru.error, m(/400/)); koru.globalErrorCatch(new koru.Error(500, "Something went wrong")); assert.calledWith(Flash.error, "Something went wrong"); assert.calledWith(koru.error, m(/500/)); koru.globalCallback(new koru.Error(404, "Not found")); assert.calledWith(Flash.notice, 'Not found');
AutoList
Automatically manage a list of Elements matching a Query. The list observers changes in the query model and updates the list accordingly.
Properties
#limit | number | A limit of When visible entries are removed non-visible entries are added to keep list length at |
Methods
constructor({ template, container, query, limit=Infinity, compare=query?.compare ?? compareAddOrder, compareKeys=compare.compareKeys ?? query?.compareKeys, observeUpdates, overLimit, removeElement=Dom.remove, parentCtx=$.ctx, })
Build a new AutoList
Parameters
template | Template | to render each row |
container | Element / Node | to render into. Can be a start |
[query] | Query | A Query or at least has methods |
[limit] | maximum number of elements to show. (see | |
[compare] | function to order data. Defaults to | |
[compareKeys] | Array of keys used for ordering data. Defaults to
| |
[observeUpdates] | The list can be monitored for changes by passing an
| |
[overLimit] | ||
[removeElement] | Method used to remove an element. Defaults to dom-client.remove | |
[parentCtx] | The Ctx to use for rendering elements. Defaults to the current context. |
Example
const AutoList = require("koru/ui/auto-list");const book1 = Book.create({title: 'The Eye of the World'}); const book2 = Book.create({title: 'The Great Hunt'}); const container = Dom.h({}); const list = new AutoList({query: Book.query.sort('title'), template: Row, container}); assert.dom(container, () => { assert.dom(':first-child', 'The Eye of the World'); assert.dom(':last-child', 'The Great Hunt'); });const container = Dom.h({div: [ 'before', {$comment$: 'start'}, {$comment$: 'end'}, 'after', ]}); const startComment = container.childNodes[1]; startComment[endMarker$] = container.childNodes[2]; const list = new AutoList({ query: Book.query.sort('title'), template: Row, container: startComment}); assert.dom(container, (pn) => { createBook(4); createBook(1); createBook(5); assert.equals(util.map( pn.childNodes, (n) => `${n.nodeType}:${n.data ?? n.textContent}`), ['3:before', '8:start', '1:b1', '1:b4', '1:b5', '8:end', '3:after']); });
AutoList#changeOptions({ query, compare, compareKeys, limit, updateAllTags=false, }={})
Rebuild list based on a different options. It trys to preserve DOM elements where possible.
Parameters
query | Query / object | |
[compare] | function | |
[compareKeys] | ||
[limit] | ||
[updateAllTags] | boolean | call updateAllTags on each element that is already rendered. Defaults
to |
Example
const AutoList = require("koru/ui/auto-list");const autoList = new AutoList();const book1 = Book.create({title: 'The Eye of the World', pageCount: 782}); const book2 = Book.create({title: 'The Great Hunt', pageCount: 681}); const container = Dom.h({}); let query = Book.query.sort('title'); const list = new AutoList({query, template: Row, container}); assert.equals(list.query, query); assert.dom(container, () => { let book1Elm; assert.dom(':first-child', {data: m.model(book1)}, (elm) => { book1Elm = elm; }); list.changeOptions({query: Book.where((d) => ! /Shadow/.test(d.title)).sort('pageCount')}); assert.dom(':first-child', 'The Great Hunt'); assert.dom(':last-child', 'The Eye of the World', (elm) => { assert.same(elm, book1Elm); }); Book.create({title: 'The Fires of Heaven', pageCount: 963}); assert.dom(':last-child', 'The Fires of Heaven'); // reverse sort const b4 = Book.create({title: 'The Shadow Rising', pageCount: 1001}); refute(list.elm(b4)); // filtered out });
AutoList#elm(doc, force)
Return the elm for a document.
Parameters
doc | BaseModel / null / object | |
[force] | string | if set to |
Returns | Element / null |
Example
const AutoList = require("koru/ui/auto-list");const list = new AutoList({ query: Book.where((n) => n.title !== 'b2').sort('title'), template: Row, container, limit: 1, parentCtx}); const [book1, book2, book3] = createBooks(1, 2, 3); assert.same(list.elm(book1), container.firstChild); assert.same(Dom.myCtx(list.elm(book1)).parentCtx, parentCtx); assert.same(list.elm(book2), null); assert.same(list.elm(book3), null); assert.same(list.elm(null), null); book1.$remove(); assert(list.elm(book3)); book3.title = 'b2'; // we haved taged this doc so we know we have it assert(list.elm({title: 'b3', _id: book3._id})); refute(list.elm({title: 'b2', _id: book3._id})); assert(list.elm(book3));
AutoList#nodeElm(node, force)
Return the elm for a node.
Parameters
node | object | |
[force] | string | if set to |
Returns | Element / null |
Example
const AutoList = require("koru/ui/auto-list");const autoList = new AutoList();autoList.nodeElm({value: {_id: 'b1', title: 'b1'}});// returns Node`<div>b1</div>`autoList.nodeElm({value: {_id: 'b3', title: 'b3'}});// returns nullautoList.nodeElm({value: {_id: 'b3', title: 'b3'}}, "render");// returns Node`<div>b3</div>`
AutoList#stop()
Stop observing model changes. Removing the container via Dom.remove also stops observering model
Example
const AutoList = require("koru/ui/auto-list");const autoList = new AutoList();autoList.stop();autoList.stop();
AutoList#thisElm(doc)
Return element associated with this instance of doc
Parameters
doc | BaseModel | a document that might be in the list |
Returns | Element / null | the element associated with this list and document; otherwise null |
Example
const AutoList = require("koru/ui/auto-list");const list = new AutoList({ query: Book.where((n) => n.title !== 'b2').sort('title'), template: Row, container, limit: 1, parentCtx}); const [book1, book2] = createBooks(1, 2); assert.equals(list.thisElm(book1).textContent, 'b1'); assert.same(list.thisElm(book2), null);
AutoList#thisNode(doc)
Return the BTree node associated with this instance of doc
Parameters
doc | BaseModel | a document that might be in the list |
Returns | object | the node associated with this list and document; otherwise undefined |
Example
const AutoList = require("koru/ui/auto-list");const list = new AutoList({ query: Book.where((n) => n.title !== 'b2').sort('title'), template: Row, container, limit: 1, parentCtx}); const [book1, book2] = createBooks(1, 2); assert.equals(list.thisNode(book1).value, {title: 'b1', _id: 'b1'}); assert.same(list.thisNode(new Book()), void 0);
AutoList#updateEntry(doc, action)
Explicitly update an entry in the list. This method is called automatically when the query.onChange callback is is used; i.e. when an entry is changed.
If observeUpdates
is set then it is called after the update.
Example
const AutoList = require("koru/ui/auto-list");const container = Dom.h({}); const observeUpdates = stub(); const list = new AutoList({ template: Row, container, query: { forEach() {}, }, compare: util.compareByField('title'), observeUpdates, }); assert.dom(container, () => { const b1 = {_id: 'b1', title: 'Book 1'}, b2 = {_id: 'b1', title: 'Book 2'}; list.updateEntry(b1); list.updateEntry(b2); assert.dom('div:last-child', 'Book 2'); b2.title = 'A book 2'; list.updateEntry(b2); assert.dom('div:first-child', 'A book 2'); assert.calledWith(observeUpdates, list, b1, 'added'); assert.calledWith(observeUpdates, list, b2, 'added'); assert.calledWith(observeUpdates, list, b2, 'changed'); list.updateEntry(b1, 'remove'); assert.dom('div', {count: 1}); assert.calledWith(observeUpdates, list, b1, 'removed'); });
ListSelector
Helper for build a selectable list.
Methods
ListSelector.attach({ ul, ctx=Dom.ctx(ul), keydownElm=ul, onClick, onHover, })
Attach events for highlighting and selecting a list element.
Parameters
ul | Element | the parent element for list items (usally a |
[ctx] | Ctx | The events are detached when this |
[keydownElm] | HTMLDocument | the element to listen for |
onClick | function | called with the current element and the causing event when clicked or enter pressed on a selectable element. |
[onHover] | function | called with the |
Example
const ListSelector = require("koru/ui/list-selector");const ul = Dom.h({ tabindex: 0, class: 'ui-ul', ul: [ {li: ['one']}, {li: ['two'], class: 'disabled'}, {li: ['sep'], class: 'sep'}, {li: ['hidden'], class: 'hide'}, {li: ['three']}, ] }); const ctx = Dom.setCtx(ul); document.body.append(ul); const onClick = stub(); ListSelector.attach({ ul, onClick, }); ul.focus(); assert.dom(ul, ()=>{ // select via keyboard TH.keydown(ul, 40); // down assert.dom('.selected', 'one'); TH.keydown(ul, 40); // down assert.dom('.selected', 'three'); TH.keydown(ul, 38); // up assert.dom('.selected', 'one'); // onClick via keyboard refute.called(onClick); TH.keydown(ul, 13);// enter assert.calledWith(onClick, ul.firstChild, m(e => e.type === 'keydown')); // pointerover selects too TH.trigger(ul.lastChild, 'pointerover'); assert.dom('.selected', 'three'); TH.trigger(ul.firstChild, 'pointerover'); assert.dom('.selected', 'one'); TH.trigger(ul.firstChild.nextSibling, 'pointerover'); assert.dom('.selected', 'one'); onClick.reset(); // onClick via pointer TH.click(ul.lastChild); assert.calledWith(onClick, ul.lastChild, m(e => e.type === 'click')); }); ul.querySelector('.selected').classList.remove('selected'); Dom.remove(ul); // destroy event listeners// override defaults const div = Dom.h({div: ul}); const divCtx = Dom.setCtx(div); document.body.append(div); const onHover = stub(); ListSelector.attach({ ul, ctx: divCtx, keydownElm: document, onClick, onHover, }); assert.dom(ul, ()=>{ TH.keydown(document, 38); // up assert.dom('.selected', 'three'); // select from bottom of list TH.trigger(ul.firstChild.firstChild, 'pointerover'); assert.calledWith(onHover, ul.firstChild, m(e => e.type === 'pointerover')); });
ListSelector.keydownHandler(event, ul, selected=ul.getElementsByClassName('selected'), onClick)
Used by attach to listen for Up/Down
events to change the selected item and Enter
events to choose the selected item.
Parameters
event | object | the |
ul | Element | the list contains |
[selected] | HTMLCollection | used to determine current selected |
[onClick] | function | callback for when |
Example
const ListSelector = require("koru/ui/list-selector");const ul = Dom.h({ tabindex: 0, class: 'ui-ul', ul: [ {li: ['one']}, {li: ['two'], class: 'disabled'}, {li: ['sep'], class: 'sep'}, {li: ['hidden'], class: 'hide'}, {li: ['three']}, ] }); const selected = ul.getElementsByClassName('selected'); const onClick = stub(); ListSelector.keydownHandler( Dom.buildEvent('keydown', {which: 40}), ul ); const event2 = Dom.buildEvent('keydown', {which: 13}); ListSelector.keydownHandler( event2, ul, selected, onClick ); assert.calledOnceWith(onClick, ul.firstChild, event2);
Ripple
Provide a ripple effect when a button is pressed
Methods
Ripple.start()
Start the ripple effect
Example
const Ripple = require("koru/ui/ripple");const button = Dom.h({button: 'press me', style: 'width:100px;height:30px'}); document.body.appendChild(button); const dim = button.getBoundingClientRect(); Ripple.start(); TH.trigger(button, 'pointerdown', { clientX: dim.left + 45, clientY: dim.top + 24, }); assert.dom('button>.ripple', ripple =>{ assert.cssNear(ripple, 'width', 100); assert.cssNear(ripple, 'height', 30); assert.dom('>div', ({style}) =>{ assert.near(style.getPropertyValue('transform'), "translate(-50%, -50%) translate(45px, 24px) scale(0.0001, 0.0001)"); }); });
Ripple.stop()
Stop the ripple effect
Example
const Ripple = require("koru/ui/ripple");const button = Dom.h({button: 'press me', style: 'width:100px;height:30px'}); document.body.appendChild(button); const dim = button.getBoundingClientRect(); Ripple.start(); Ripple.stop(); TH.trigger(button, 'pointerdown', { clientX: dim.left + 45, clientY: dim.top + 24, }); refute.dom('button>.ripple');
Route
Route is a paginging system within a one page app. It manages creating and destroying pages and recording history.
Methods
Route.gotoPage(page, pageRoute)
Goto the specified page
and record in window.history
.
Example
const Route = require("koru/ui/route");Route.gotoPage(Template(Test.AdminProfile), {append: "my/id"});
Route.pathToPage(path, pageRoute)
Example
const Route = require("koru/ui/route");const pageRoute = {}; const page = Route.pathToPage('/baz/an-id/root-bar?search=data#tag', pageRoute); assert.equals(page, v.RootBar); assert.equals(pageRoute, {bazId: 'an-id', pathname: '/baz/an-id/root-bar', search: '?search=data', hash: '#tag'});
Route.replacePage(page, pageRoute)
Like gotoPage but replaces to window.history
rather than adding to it.
Example
const Route = require("koru/ui/route");Route.replacePage(Template(Test.MyPage), {append: "myId"});
Route.setTitle(title)
Set the document.title
for the current page.
Parameters
title | string |
Example
const Route = require("koru/ui/route");Route.setTitle("my title");
SvgIcons
SvgIcons: is a helper for managing svg icons
By convention the document.body
contains an svg
with a defs
section which contains the
body of a list of icons. Each icon has a id which is prefixed by "icon-"
; for example
"icon-account"
. These defs are then used by a use
element with an xlink:href
to the id of
the icon required.
Methods
DomHelper:svgIcon(name, attributes)
{{svgIcon "name" attributes...}}
inserts an svg into an html document. The icon is only built
once.
Example
<button>{{svgIcon "person_add" class="addUser"}}<span>Add user</span></button>
Parameters
name | string | the name of the icon to use (See use) |
attributes | object | name/value pairs to set as attributes on the svg |
Returns | Element |
Example
const SvgIcons = require("koru/ui/svg-icons");SvgIcons.svgIcon("close", {class: "svgClose"});// returns Node`<svg class="svgClose" name="close"><use xlink:href="#icon-close"></use></svg>`
SvgIcons.add(id, symbol)
Add an svg to the asset library under id "icon-"+id
.
Note: all icons should be drawn for use with a viewBox of "0 0 24 24"
Parameters
id | string | the id of the icon. It will be prefixed with |
symbol | object | An |
Example
const SvgIcons = require("koru/ui/svg-icons");document.body.appendChild(Dom.h({ id: 'SVGIcons', style: 'display:hidden', svg: {defs: []}, })); const d = 'M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z'; SVGIcons.add('hamburger-menu', {path: [], d}); assert.dom('#SVGIcons>defs>symbol#icon-hamburger-menu', (sym) => { assert.same(sym.getAttribute('viewBox'), '0 0 24 24'); assert.dom('path[d="' + d + '"]'); });
SvgIcons.createIcon(icon, title)
Create an svg element that uses a icon definition.
Note: all icons should be drawn for use with a viewBox of "0 0 24 24"
Parameters
icon | string | the name of an icon to use. the |
[title] | string | adds a title element to the SVG. |
Returns | Element |
Example
const SvgIcons = require("koru/ui/svg-icons");document.body.appendChild(Dom.h({ style: 'display:hidden', svg: {defs: { id: 'icon-hamburger-menu', viewBox: '0 0 24 24', symbol: { path: [], d: 'M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z', }, }}, })); const svg = SVGIcons.createIcon('hamburger-menu', 'Main menu'); assert(svg.classList.contains('icon')); assert.same(svg.namespaceURI, Dom.SVGNS); assert.equals(svg.querySelector(':scope>title:first-child').textContent, 'Main menu'); const use = svg.querySelector('use'); assert.same(use.getAttributeNS(Dom.XLINKNS, 'href'), '#icon-hamburger-menu');
SvgIcons.selectMenuDecorator({icon}, elm)
selectMenuDecorator can be used as a decorator
function option to
select-menu.popup. Any list item with a icon attribute will be given a svg icon with
a <use xlink:href="#icon"+item.icon />
body.
Example
const SvgIcons = require("koru/ui/svg-icons");const name = document.createTextNode('Close'); const li = Dom.h({li: [name]}); const item = {id: 'close', name: 'Close', icon: 'close-outline'}; SVGIcons.selectMenuDecorator(item, name); const svg = name.previousSibling; assert.same(svg.querySelector('use').getAttributeNS(Dom.XLINKNS, 'href'), '#icon-close-outline');
SvgIcons.setIcon(useElm, icon)
set the icon in a svg use element
Example
const SvgIcons = require("koru/ui/svg-icons");const use = SVGIcons.use('hamburger-menu'); SVGIcons.setIcon(use, 'circle'); assert.same(use.namespaceURI, Dom.SVGNS); assert.same(use.getAttributeNS(Dom.XLINKNS, 'href'), '#icon-circle');
SvgIcons.use(icon, attrs)
create a svg use element for an icon
Example
const SvgIcons = require("koru/ui/svg-icons");const use = SVGIcons.use('hamburger-menu'); assert.same(use.namespaceURI, Dom.SVGNS); assert.same(use.getAttributeNS(Dom.XLINKNS, 'href'), '#icon-hamburger-menu');
Time
Utility methods for displaying date and time and for updating times relative to now
Properties
[object Object] | TZ |
Methods
Time.fromNow(date)
Print relative time in locale format. Also can be used as a template helper:
{{fromNow updatedAt}}
Example
const Time = require("koru/ui/time");assert.same(Time.fromNow(util.dateNow() + util.DAY), 'tomorrow'); assert.same(Time.fromNow(new Date(util.dateNow() - 2*util.DAY)), '2 days ago');
Time.relTime(time)
Print date and time in shortish locale format with times with 24 hours from now also showing relative time.. Also can be used as a template helper:
{{relTime updatedAt}}
Example
const Time = require("koru/ui/time");assert.equals(Time.relTime(util.dateNow() - 20*60*60*1000), 'Aug 16, 2018 9:44 PM; 20 hours ago'); assert.same(Time.relTime(Date.UTC(2014, 3, 4, 6, 5)), 'Apr 4, 2014 6:05 AM'); uDate.defaultLang = 'fr'; assert.same(Time.relTime(util.dateNow() + 120*1000), '17 août 2018 17:46; dans 2 minutes'); Time.TZ = 'America/Chicago'; uDate.defaultLang = 'en-US'; assert.same(Time.relTime(util.dateNow() + 120*1000), 'Aug 17, 2018 12:46 PM; in 2 minutes');
Time.startDynTime()
Start a recurring minute timer to update all elements of class dynTime
. Updating is done
by calling ctx#updateElement(element) for each Dom.ctx(element)
Example
const Time = require("koru/ui/time");const stopTimeout = stub(); stub(koru, 'afTimeout').returns(stopTimeout); let now = +Date.UTC(2019, 10, 3, 14, 35, 12); intercept(util, 'dateNow', ()=>now); Time.startDynTime(); const html = ` <ul> <li class="dynTime">{{relTime time1}}</li> <li class="dynTime">{{fromNow time2}}</li> </ul>`; const Tpl = Dom.newTemplate(TemplateCompiler.toJavascript(html, 'DynTimeTest').toJson()); document.body.appendChild(Tpl.$autoRender({ time1: new Date(now +5*60*1000), time2: new Date(now -2*60*1000), })); const li1 = Dom('ul>li:first-child'); const li2 = Dom('ul>li:last-child'); assert.equals(li1.textContent, "Nov 3, 2019 2:40 PM; in 5 minutes"); assert.equals(li2.textContent, "2 minutes ago"); now += 60*1000; koru.afTimeout.yieldAndReset(); assert.equals(li1.textContent, "Nov 3, 2019 2:40 PM; in 4 minutes"); assert.equals(li2.textContent, "3 minutes ago"); now += 10*60*1000; koru.afTimeout.yieldAndReset(); assert.equals(li1.textContent, "Nov 3, 2019 2:40 PM; 6 minutes ago"); assert.equals(li2.textContent, "13 minutes ago");
Time.stopDynTime()
Cancel dynamic time updates (See startDynTime)
Example
const Time = require("koru/ui/time");const stopTimeout = stub(); stub(koru, 'afTimeout').returns(stopTimeout); Time.startDynTime(); assert.called(koru.afTimeout); refute.called(stopTimeout); Time.stopDynTime(); assert.calledOnce(stopTimeout);
Uint8ArrayBuilder
Build an Uint8Array with dynamic sizing.
Properties
#dataView | object | Use a dataView over the interal ArrayBuffer |
#length | number | The length of |
#subarray | function | The Uint8Array containing appended data |
Methods
constructor(initialCapacity=4)
Create a Uint8ArrayBuilder
Parameters
[initialCapacity] | number |
Example
const Uint8ArrayBuilder = require("koru/uint8-array-builder");const b1 = new Uint8ArrayBuilder(); b1.set(0, 1); assert.same(b1.initialCapacity, 4); assert.same(b1.currentCapacity, 4); b1.set(4, 2); const b2 = new Uint8ArrayBuilder(2); b2.set(0, 1); assert.same(b2.initialCapacity, 2); assert.same(b2.currentCapacity, 2);
Uint8ArrayBuilder#append(data)
Append a Uint8Array to the builder
Parameters
data | Array / Uint8Array | |
Returns | Uint8ArrayBuilder |
Example
const Uint8ArrayBuilder = require("koru/uint8-array-builder");const b = new Uint8ArrayBuilder(); b.append([]); b.set(0, 16); b.append(new Uint8Array([1, 2, 3, 4])); assert.equals(b.subarray(), new Uint8Array([16, 1, 2, 3, 4])); b.append(new Uint8Array([5, 6, 7, 8, 9, 10, 11, 12, 13, 14])); assert.equals(b.subarray(), new Uint8Array([16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])); assert.same(b.currentCapacity, 30);
Uint8ArrayBuilder#get(index)
get an existing byte in array.
Example
const Uint8ArrayBuilder = require("koru/uint8-array-builder");const b1 = new Uint8ArrayBuilder(); b1.push(1, 2); assert.same(b1.get(1), 2);
Uint8ArrayBuilder#push(...bytes)
Push byte(s) to end of array;
Parameters
bytes | number | |
Returns | Uint8ArrayBuilder |
Example
const Uint8ArrayBuilder = require("koru/uint8-array-builder");const b = new Uint8ArrayBuilder(); b.push(1, 2, 3); b.push(4, 5); assert.equals(b.subarray(), new Uint8Array([1, 2, 3, 4, 5]));
Uint8ArrayBuilder#set(index, byte)
Set an existing byte in array.
Example
const Uint8ArrayBuilder = require("koru/uint8-array-builder");const b1 = new Uint8ArrayBuilder(); b1.push(1, 2); b1.set(1, 3); assert.equals(Array.from(b1.subarray()), [1, 3]); b1.set(2, 4); assert.equals(Array.from(b1.subarray()), [1, 3, 4]);
Uint8ArrayBuilder#setArray(data, offset = 0)
Set a Uint8Array into the builder at offset
Parameters
data | Uint8Array | |
[offset] | number | |
Returns | Uint8ArrayBuilder |
Example
const Uint8ArrayBuilder = require("koru/uint8-array-builder");const b = new Uint8ArrayBuilder(); b.append([]); b.set(0, 16); b.setArray(new Uint8Array([1, 2, 3, 4]), 6); assert.equals(b.subarray(), new Uint8Array([16, 0, 0, 0, 0, 0, 1, 2, 3, 4])); b.setArray(new Uint8Array([5, 6, 7, 8, 9]), 3); assert.equals(b.subarray(), new Uint8Array([16, 0, 0, 5, 6, 7, 8, 9, 3, 4])); b.setArray(b.subarray(4)); assert.equals(b.subarray(), new Uint8Array([6, 7, 8, 9, 3, 4, 8, 9, 3, 4])); b.setArray(b.subarray(0, 4), 2); assert.equals(b.subarray(), new Uint8Array([6, 7, 6, 7, 8, 9, 8, 9, 3, 4])); assert.same(b.currentCapacity, 20);
Uint8ArrayBuilder#subarray(spos=0, epos=this.length)
Return the built array. It contains the same ArrayBuffer
store as the interal Uint8Array
.
Parameters
[spos] | number | |
[epos] | number | |
Returns | Uint8Array |
Example
const Uint8ArrayBuilder = require("koru/uint8-array-builder");const b1 = new Uint8ArrayBuilder(); assert.equals(Array.from(b1.subarray()), []); assert.same(b1.subarray(), b1.subarray()); b1.push(1, 2, 3, 4, 5); assert.equals(Array.from(b1.subarray()), [1, 2, 3, 4, 5]); assert.equals(Array.from(b1.subarray(2, 4)), [3, 4]); assert(b1.subarray() instanceof Uint8Array); refute.same(b1.subarray(), b1.subarray()); assert.same(b1.subarray().buffer, b1.subarray().buffer);
util
The util
module provides commonly performed utility functions.
Properties
DAY | number | The number of milliseconds in a day. |
EMAIL_RE | RegExp | RegExp to match email addresses |
idLen | number |
|
thread | object | An object associated with the current AsyncLocalStorage. |
Methods
util.addItem(list, item)
Add item
to list
if list
does not already contain item
.
If item
is added to list
, return undefined
. If list
already
contains item
, return the index of item
.
Parameters
list | Array | the list to add |
item | any-type | the item to add to |
Returns | undefined / number | Returns |
Example
const util = require("koru/util");const list = ['a', 'b']; assert.same(util.addItem(list, 'b'), 1); assert.same(util.addItem(list, 'a'), 0); assert.equals(list, ['a', 'b']); assert.same(util.addItem(list, {aa: 123}), undefined); assert.equals(list, ['a', 'b', {aa: 123}]); assert.same(util.addItem(list, {aa: 123}), 2); assert.equals(list, ['a', 'b', {aa: 123}]);
util.arrayToMap(list)
convert an array of strings to an object
.
Parameters
[list] | [String] | the array to convert |
Returns | object | with its properties named the |
Example
const util = require("koru/util");assert.equals(util.arrayToMap(), {}); assert.equals(util.arrayToMap(['a', 'b', 'd']), {a: true, b: true, d: true});
util.assignOption(obj = {}, options, name, def)
Assign an option to an object with optional default.
Parameters
obj | undefined / object | |
options | object | the options to select from. |
name | string | the name of the option to set. |
[def] | function | the default to use if |
Returns | object | the object. |
Example
const util = require("koru/util");const options = {a: 123, b: 456, e: void 0}; assert.equals(util.assignOption(void 0, options, 'a'), {a: 123}); const obj = {a: 1, c: 777}; const ao = util.assignOption.bind(null, obj, options); ao('a'); ao('c'); ao('d', () => 444); ao('d', () => 555); assert.equals(obj, {a: 123, c: 777, d: 444}); obj.e = 222; ao('e'); assert.equals(obj, {a: 123, c: 777, d: 444, e: void 0});
util.async asyncArrayFrom(asyncIterator)
Convert an async iterator into an Array
Parameters
asyncIterator | Function | |
Returns | Promise(Array) |
Example
const util = require("koru/util");async function *asyncIter() { yield await 1; yield await 2; } assert.equals(await util.asyncArrayFrom(asyncIter()), [1, 2]);
util.binarySearch(list, compare, start = list.length >> 1, lower = 0, upper = list.length)
Perform a binary search over a sorted list
and return the closest index with a <= 0
compare
result.
Example
const util = require("koru/util");assert.same(util.binarySearch([], (row) => assert(false)), -1); const list = [1, 3, 6, 8, 10, 13, 15]; assert.same(util.binarySearch([1, 2, 3], (row) => 1, 0), -1); assert.same(util.binarySearch([1, 2, 3], (row) => -1, 0), 2); assert.same(util.binarySearch(list, (row) => row - 0), -1); assert.same(util.binarySearch(list, (row) => row - 1), 0); assert.same(util.binarySearch(list, (row) => row - 16), 6); assert.same(util.binarySearch(list, (row) => row - 5), 1); assert.same(util.binarySearch(list, (row) => row - 6, 0), 2); assert.same(util.binarySearch(list, (row) => row - 10), 4); assert.same(util.binarySearch(list, (row) => row - 14), 5); assert.same(util.binarySearch(list, (row) => row - 8, -1), 3); assert.same(util.binarySearch(list, (row) => row - 8, 7), 3);
util.compareNumber(a, b)
Example
const util = require("koru/util");const array = [3, 5, 2, 1]; assert.equals(array.sort(util.compareNumber), [1, 2, 3, 5]);
util.createDictionary()
Create an object that hints to the VM that it will be used as a dynamic dictionary rather than as a class.
Parameters
Returns | object | a new object with no prototype |
Example
const util = require("koru/util");const dict = util.createDictionary(); assert.same(Object.getPrototypeOf(dict), null); assert(util.isObjEmpty(dict)); assert(dict && typeof dict === 'object');
util.diff(list1, list2)
Create a list of all the elements of list1
that are not also elements of list2
.
Parameters
[list1] | Array | a list |
[list2] | Array | another list |
Returns | Array | a list of all the elements of |
Example
const util = require("koru/util");assert.equals(util.diff(), []); assert.equals(util.diff([1, 2]), [1, 2]); assert.equals(util.diff([1, '2', 3, null], ['2', 4]), [1, 3, null]);
util.diffString(oldstr, newstr)
Find the difference between oldstr
and newstr
. Return undefined
if oldstr
and
newstr
are the same; otherwise return an array of three numbers:
- the index of the first non-matching character in oldstr and newstr
- the length of the segment of
oldstr
that doesn't matchnewstr
- the length of the segment of
newstr
that doesn't matcholdstr
Parameters
oldstr | string | a string |
newstr | string | another string |
Returns | undefined / Array |
|
Example
const util = require("koru/util");assert.equals(util.diffString('it1', 'it21'), [2, 0, 1]); assert.equals(util.diffString('it1', 'zit21z'), [0, 3, 6]); assert.equals(util.diffString('it21', 'it1'), [2, 1, 0]); assert.equals(util.diffString('cl 123.2', 'cl 123'), [6, 2, 0]); assert.equals(util.diffString('h💣el💣 world', 'h💣elo wor💣ld'), [5, 6, 7]); assert.equals(util.diffString('h💣el💣 world', 'h💣el💤 wor💣ld'), [5, 6, 8]); assert.equals(util.diffString('helo worlld', 'hello world'), [3, 6, 6]); assert.equals(util.diffString('hello world', 'helo worlld'), [3, 6, 6]); assert.equals(util.diffString('hello world', 'hello world'), undefined);
util.diffStringLength(oldstr, newstr)
In the longer of oldstr
and newstr
, find the length of the segment that doesn't
match the other string.
Parameters
oldstr | string | a string |
newstr | string | another string |
Returns | number |
|
Example
const util = require("koru/util");assert.equals(util.diffStringLength('h💣elo wor💣ld', 'h💣el💣 world'), 7); assert.equals(util.diffStringLength('h💣el💣 world', 'h💣elo wor💣ld'), 7); assert.equals(util.diffStringLength('hello world', 'hello world'), 0);
util.extractError(err)
Extract the error message and normalized stack trace from an exception. If the error has a
property called toStringPrefix
it will prefix the error message
See Stacktrace
Parameters
err | AssertionError / string | |
Returns | string |
Example
const util = require("koru/util");const inner1 = () => inner2(); const inner2 = () => { return new AssertionError('Testing 123'); }; const err = inner1(); err.toStringPrefix = 'A string to prefix the message\n'; assert.equals(util.extractError(err).split('\n'), [ 'A string to prefix the message', 'AssertionError: Testing 123', // the "at - " is to distinguish the first frame for editors m(/ at - inner2 \(koru\/util-test.js:\d+:16\)/), m(/ at inner1 \(koru\/util-test.js:\d+:28\)/), m(/ at .* \(koru\/util-test.js:\d+:19\)/), ]); assert.equals(util.extractError('plain string'), 'plain string');
util.extractKeys(obj, keys)
Create an object made up of the properties in obj
whose keys are named in keys
.
Parameters
obj | object | the object from which to collect properties |
keys | Array / object | a collection of keys or properties whose names identify which properties to collect
from |
Returns | object | an object made up of the properties in |
Example
const util = require("koru/util");assert.equals( util.extractKeys({a: 4, b: 'abc', get c() {return {value: true}}}, ['a', 'c', 'e']), {a: 4, c: {value: true}}, ); assert.equals( util.extractKeys({a: 4, b: 'abc', get c() {return {value: true}}}, {a: true, c: false, e: null}), {a: 4, c: {value: true}}, );
util.extractNotKeys(obj, keys)
Create an object made up of the properties in obj
whose keys are not named in
keys
.
Parameters
obj | object | the object from which to collect properties |
keys | object | a collection of properties whose names identify which properties not to
collect from |
Returns | object | an object made up of the properties in |
Example
const util = require("koru/util");assert.equals( util.extractNotKeys({a: 4, b: 'abc', get c() {return {value: true}}}, {a: true, e: true}), {b: 'abc', c: {value: true}}, );
util.firstParam(obj)
Return the value of the first property in obj
. Or if obj
is empty return
undefined
.
Parameters
[obj] | object | an object |
Returns | any-type | the value of the first property in |
Example
const util = require("koru/util");assert.same(util.firstParam({a: 1, b: 2}), 1); assert.same(util.firstParam({}), undefined); assert.same(util.firstParam(), undefined);
util.forEach(list, visitor)
Execute visitor
once for each element in list
.
Parameters
list | Array / null | a list |
visitor | function | a function taking two arguments: the value of the current element in |
Example
const util = require("koru/util");const results = []; util.forEach([1, 2, 3], (val, index) => {results.push(val + '.' + index)}); assert.equals(results, ['1.0', '2.1', '3.2']); // ignores null list const callback = stub(); util.forEach(null, callback); refute.called(callback);
util.hasOnly(obj, keyMap)
Determine if obj
has only keys that are also in keyMap
.
Parameters
obj | object | an object |
keyMap | object | a set of key-value pairs |
Returns | boolean |
|
Example
const util = require("koru/util");assert.isFalse(util.hasOnly({a: 1}, {b: true})); assert.isFalse(util.hasOnly({a: 1, b: 1}, {b: true})); assert.isTrue(util.hasOnly({b: 1}, {b: true})); assert.isTrue(util.hasOnly({}, {b: true})); assert.isTrue(util.hasOnly({b: 1, c: 2}, {b: true, c: false}));
util.ifPromise(object, trueCallback, falseCallbase=trueCallback)
If object
is a promise return object.then(trueCallback)
otherwise return falseCallbase(object)
.
This function is also on globalThis for convenience.
Parameters
object | Promise(number) / Array | |
trueCallback | function | |
[falseCallbase] | function | |
Returns | Promise(string) |
Example
const util = require("koru/util");const trueCallback = stub().returns('true called'); const falseCallback = stub().returns('false called'); const promise = Promise.resolve(123); const ans = util.ifPromise(promise, trueCallback, falseCallback); assert.isPromise(ans); refute.called(trueCallback); await ans; assert.calledWith(trueCallback, 123); assert.same(util.ifPromise(456, trueCallback), 'true called'); assert.calledWith(trueCallback, 456); refute.called(falseCallback); trueCallback.reset(); assert.same(util.ifPromise([789], trueCallback, falseCallback), 'false called'); refute.called(trueCallback); assert.calledWith(falseCallback, [789]);
util.indexOfRegex(list, value, fieldName)
Return the index of the first item in list
that has a property fieldName
that contains a match for the regular expression value
. Or if no match is
found return -1.
Parameters
list | Array | the list to search |
value | RegExp | the regular expression to search |
fieldName | string | the property name to search for in each item in |
Returns | number | the index of the first item in |
Example
const util = require("koru/util");const list = [{foo: 'a'}, {foo: 'cbc'}]; assert.same(util.indexOfRegex(list, /a/, 'foo'), 0); assert.same(util.indexOfRegex(list, /ab/, 'foo'), -1); assert.same(util.indexOfRegex(list, /b/, 'foo'), 1);
util.indexTolineColumn(text, index)
Convert index
of text
to a line number and a column number.
Parameters
text | string | |
index | number | |
Returns | Array | item 0 is the line number (starting from 1) and item 1 is the column number (starting from 0) |
Example
const util = require("koru/util");const text = 'line 1\nline 2\nline 3'; assert.equals(util.indexTolineColumn(text, 0), [1, 0]); assert.equals(util.indexTolineColumn(text, 4), [1, 4]); assert.equals(util.indexTolineColumn(text, 10), [2, 3]); assert.equals(util.indexTolineColumn(text, text.length), [3, 6]);
util.inspect(o, count=4, len=1000)
Convert any type to a displayable string. The symbol inspect$
can be used to override
the value returned.
Example
const util = require("koru/util");const obj = {'': 0, 123: 1, 'a"b"`': 2, "a`'": 3, "a\"'`": 4, '\\a': 5}; assert.equals( util.inspect(obj), `{123: 1, '': 0, 'a"b"\`': 2, "a\`'": 3, "a\\"'\`": 4, '\\\\a': 5}`); const array = [1, 2, 3]; assert.equals( util.inspect(array), '[1, 2, 3]', ); array[inspect$] = () => 'overridden'; assert.equals(util.inspect(array), 'overridden');
util.intersectp(list1, list2)
Determine if two lists intersect.
Parameters
list1 | Array | a list |
list2 | Array | a second list |
Returns | boolean |
|
Example
const util = require("koru/util");assert(util.intersectp([1, 4], [4, 5])); refute(util.intersectp([1, 2], ['a']));
util.isObjEmpty(obj)
Determine whether obj
is empty.
Example
const util = require("koru/util");assert.isTrue(util.isObjEmpty()); assert.isTrue(util.isObjEmpty({})); assert.isFalse(util.isObjEmpty({a: 1}));
util.isPromise(object)
Return true is object
is an object with a then
function.
This function is also on globalThis for convenience.
Parameters
object | Promise(undefined) / object / null | |
Returns | boolean |
Example
const util = require("koru/util");assert.isTrue(util.isPromise(Promise.resolve())); assert.isTrue(util.isPromise({then() {}})); assert.isFalse(util.isPromise({then: true})); assert.isFalse(util.isPromise(null)); assert.isFalse(util.isPromise(void 0));
util.itemIndex(list, item)
Return the index of the first element in list
that matches
item
. If item
is an object, itemIndex
returns the index of the first object in
list
that contains all the key-value pairs that item
contains. If no match is
found, -1 is returned.
Parameters
list | Array | the list to search |
item | any-type | the item to search |
Returns | number | the index of |
Example
const util = require("koru/util");const list = ['a', 'b', {one: 'c', two: 'd'}]; assert.same(util.itemIndex(list, 'b'), 1); assert.same(util.itemIndex(list, 'd'), -1); assert.same(util.itemIndex(list, {one: 'c', two: 'd'}), 2); assert.same(util.itemIndex(list, {two: 'd'}), 2); assert.same(util.itemIndex(list, {one: 'e', two: 'd'}), -1);
util.keyMatches(obj, regex)
Search for a property name in obj
that matches regex
. Test each enumerable
property name against regex
util a match is found. Return the result array
from regex.exec()
if a match is found, or null
if not.
Parameters
obj | object | the object to search |
regex | RegExp | the regular expression to match |
Returns | Array / null | the result array from |
Example
const util = require("koru/util");assert.same(util.keyMatches({ab: 0, bc: 0, de: 0}, /^b(.)/)[1], 'c'); assert.isNull(util.keyMatches({ab: 0, bc: 0, de: 0}, /^dee/));
util.keyStartsWith(obj, str)
Determine whether obj
has a key that starts with str
. Case sensitive.
Parameters
obj | null / object | the object to search |
str | string | the string to search for |
Returns | boolean |
|
Example
const util = require("koru/util");assert.isFalse(util.keyStartsWith(null, 'foo')); assert.isFalse(util.keyStartsWith({foz: 1, fizz: 2}, 'foo')); assert.isTrue(util.keyStartsWith({faz: true, fooz: undefined, fizz: 2}, 'foo')); assert.isTrue(util.keyStartsWith({foo: 1, fizz: 2}, 'foo'));
util.merge(dest, source)
Merge source
into dest
. That is, add each enumerable property in source
to dest
, or where a
property of that name already exists in dest
, replace the property in dest
with the
property from source
. Return the modified dest
.
Aliases
extend
deprecated
Parameters
dest | object | an object to modify |
[source] | object | the properties to be added or modified |
Returns | object |
|
Example
const util = require("koru/util");util.merge({}, {a: 1, b: 2});// returns {a: 1, b: 2}util.merge({a: 1});// returns {a: 1}util.merge({a: 1, b: 2}, {b: 3, c: 4});// returns {a: 1, b: 3, c: 4}util.merge({a: 1, b: 2}, {b: 3, c: 4});// returns {b: 3, c: 4, a: 1}util.merge({d: 5}, {b: 3, c: 4, a: 1});// returns {d: 5, b: 3, c: 4}
util.mergeExclude(obj, properties, exclude)
Merge properties
into obj
, excluding properties in exclude
.
That is, add each property in properties
, excluding those that are in
exclude
, to obj
, or where a property of that name already exists in obj
, replace
the property in obj
with the property from properties
. Return the modified obj
.
Parameters
obj | object | an object to modify |
properties | object | properties to be added to or modified in |
exclude | object | properties which are excluded from being added to or modified in |
Returns | object | the modified |
Example
const util = require("koru/util");let item = 5, sub = {a: 1, b: 2}, sup = {b: 3, get c() {return item}, d: 4, e: 5}; util.mergeExclude(sub, sup, {d: void 0, e: false}); item = 6; assert.same(sub.a, 1); assert.same(sub.b, 3); assert.same(sub.c, 6); refute(sub.hasOwnProperty('d'));
util.mergeInclude(obj, properties, include)
Merge the properties from properties
that are named in include
into obj
. That is, add each
property in properties
that is named in include
to obj
, or where a property of that name
already exists in obj
, replace the property in obj
with the property from properties
.
Return the modified obj
.
Parameters
obj | object | an object to modify |
properties | object | properties to be added to or modified in |
include | object / array | properties, or a list of property names, whose names identify
which properties from |
Returns | object |
|
Example
const util = require("koru/util");const obj = {a: 1, c: 2}; const ans = util.mergeInclude(obj, {b: 2, c: 3, d: 4}, {c: true, d: true, z: true}); assert.equals(ans, {a: 1, c: 3, d: 4}); assert.same(obj, ans); assert.equals(util.mergeInclude({a: 1, c: 2}, {b: 2, c: 3, d: 4}, ['c', 'd', 'z']), {a: 1, c: 3, d: 4});
util.mergeNoEnum(dest, source)
Merge source
into dest
and set enumerable
to false
for each added or modified
property. That is, add each enumerable property in source
to dest
, or where a
property of that name already exists in dest
, replace the property in dest
with the
property from source
, and set enumerable
to false
for each. Return the modified dest
.
Parameters
dest | object | an object to modify |
source | object | the properties to be added or modified |
Returns | object |
|
Example
const util = require("koru/util");const book = {author: 'Austen'}; let pages = 0; util.mergeNoEnum(book, { published: 1813, get pages() {return pages}, }); pages = 432; assert.equals(Object.keys(book), ['author']); assert.same(book.published, 1813); assert.same(book.pages, 432);
util.mergeOwnDescriptors(dest, source)
Merge source
into dest
, including non-enumerable properties from source
. That is, add
each property in source
, including non-enumerable properties, to dest
, or where a property
of that name already exists in dest
, replace the property in dest
with the property from
source
. Return the modified dest
.
Parameters
dest | object | an object to modify |
source | object | the properties to be added or modified |
Returns | object |
|
Example
const util = require("koru/util");const a = {a: 1, b: 2}; const b = util.mergeNoEnum({__proto__: a, b: 3, c: 4}, {e: 6}); const c = {d: 5}; const ans = util.mergeOwnDescriptors(c, b); assert.same(ans, c); assert.equals(ans, {d: 5, b: 3, c: 4}); assert.same(ans.e, 6);
util.pc(fraction)
Return a string comprised of the percent form of fraction
, with the percent symbol, %.
Parameters
fraction | number | a fraction |
Returns | string | a string comprised of the percent form of |
Example
const util = require("koru/util");assert.same(util.pc(1.2345678), '123.45678%');
util.qlabel(id)
Quote label. Add quotes only if needed to be used as a property name.
Example
const util = require("koru/util");assert.equals(util.qlabel('1234'), '1234'); assert.equals(util.qlabel('abc123'), 'abc123'); assert.equals(util.qlabel("a'234"), `"a'234"`); assert.equals(util.qlabel('123a'), `'123a'`); assert.equals(util.qlabel('ab\nc'), `'ab\\nc'`);
util.qstr(s)
Quote string
Example
const util = require("koru/util");assert.equals(util.qstr('1234'), "'1234'"); assert.equals(util.qstr("1'234"), `"1'234"`); assert.equals(util.qstr('12"3a'), `'12"3a'`); assert.equals(util.qstr("\r"), '"\\r"'); assert.equals(util.qstr('12\n3a'), `'12\\n3a'`); assert.equals(util.qstr('12\\n3a'), `'12\\\\n3a'`);
util.removeItem(list, item)
Remove item
from list
and return it. list
is modified. If item
is an object,
removeItem
removes the first object in list
that contains all the key-value
pairs that item
contains. If list
does not contain item
, undefined
is
returned.
Parameters
list | Array | the list from which to remove |
[item] | any-type | the item to remove from |
Returns | any-type | the removed item, or |
Example
const util = require("koru/util");const foo = [1, 2, 3]; assert.same(util.removeItem(foo, 2), 2); assert.equals(foo, [1, 3]); util.removeItem(foo); assert.equals(foo, [1, 3]); assert.same(util.removeItem(foo, 4), undefined); assert.equals(foo, [1, 3]); util.removeItem(foo, 1); assert.equals(foo, [3]); util.removeItem(foo, 3); assert.equals(foo, []); const bar = [{id: 4, name: 'foo'}, {id: 5, name: 'bar'}, {x: 1}]; assert.same(util.removeItem(bar, {name: 'bar', x: 1}), undefined); assert.equals(bar, [{id: 4, name: 'foo'}, {id: 5, name: 'bar'}, {x: 1}]); assert.equals(util.removeItem(bar, {name: 'bar'}), {id: 5, name: 'bar'}); assert.equals(bar, [{id: 4, name: 'foo'}, {x: 1}]); assert.equals(util.removeItem(bar, {id: 4, name: 'foo'}), {id: 4, name: 'foo'}); assert.equals(bar, [{x: 1}]);
util.reverseForEach(list, visitor)
Visit list
in reverse order, executing visitor
once for each list element.
Parameters
list | Array / null | a list |
visitor | function | a function taking two arguments: the value of the current element in |
Example
const util = require("koru/util");const results = []; util.reverseForEach(v.list = [1, 2, 3], (val, index) => { results.push(val + '.' + index); }); assert.equals(results, ['3.2', '2.1', '1.0']); // ignores null list const callback = stub(); util.reverseForEach(null, callback); refute.called(callback);
util.sansPc(value)
Return value
converted to a number; the suffix '%' is removed.
Parameters
[value] | string / number | a value to be converted |
Returns | number |
|
Example
const util = require("koru/util");assert.same(util.sansPc('123.23%'), 123.23); assert.same(util.sansPc(), 0); assert.same(util.sansPc(234), 234);
util.sansPx(value)
Return value
converted to a number; the suffix 'px' is removed.
Parameters
[value] | string / number | a value to be converted |
Returns | number |
|
Example
const util = require("koru/util");assert.same(util.sansPx('123.23px'), 123.23); assert.same(util.sansPx(), 0); assert.same(util.sansPx(234), 234);
util.splitKeys(obj, includeKeys)
Create an object containing two properties, include
and exclude
. The former
is made up of the properties in obj
whose keys are named in includeKeys
, and the later
is made up of the other properties in obj
.
Parameters
obj | object | the object from which to collect properties |
includeKeys | object | a collection of properties whose names identify which objects to include in the first object returned |
Returns | object | the |
Example
const util = require("koru/util");const {include, exclude} = util.splitKeys( {a: 4, b: 'abc', get c() {return {value: true}}}, {a: true, e: true}); assert.equals(include, {a: 4}); assert.equals(exclude, {b: 'abc', c: {value: true}});
util.symDiff(list1, list2)
Create a list of all the elements of list1
and list2
that belong only to list1
or list2
, not to both lists.
Parameters
[list1] | Array | a list |
[list2] | Array | a second list |
Returns | Array | a list of all the elements of |
Example
const util = require("koru/util");assert.equals(util.symDiff(), []); assert.equals(util.symDiff([1, 2]), [1, 2]); assert.equals(util.symDiff([1, 2, 3], [2, 4]).sort(), [1, 3, 4]); assert.equals(util.symDiff([2, 4], [1, 2, 3]).sort(), [1, 3, 4]);
util.titleize(value)
Example
const util = require("koru/util");assert.same(util.titleize(''), ''); assert.same(util.titleize('abc'), 'Abc'); assert.same(util.titleize('abc-def_xyz.qqq+foo%bar'), 'Abc Def Xyz Qqq Foo Bar'); assert.same(util.titleize('CarlySimon'), 'Carly Simon');
util.toDp(number, dp, zeroFill = false)
Return number
to dp
decimal places, converted to a string, padded with
zeros if zeroFill
is true
.
Parameters
number | number | a number to be converted |
dp | number | the number of decimal places to display |
[zeroFill] | boolean | pad with zeros; |
Returns | string |
|
Example
const util = require("koru/util");assert.same(util.toDp(10.7, 0), '11'); assert.same(util.toDp(2.6, 1), '2.6'); assert.same(util.toDp(1.2345, 3, true), '1.235'); assert.same(util.toDp(1.2, 3, true), '1.200'); assert.same(util.toDp(10, 3), '10');
util.toHex(array)
Convert a byte array to a hex string
Parameters
array | Uint8Array | |
Returns | string |
Example
const util = require("koru/util");assert.equals(util.toHex(new Uint8Array([3, 6, 8, 129, 255])), '03060881ff');
util.toMap(keyName, valueName /* lists */)
convert to a object
;
Parameters
[keyName] | null / Array / string / number | |
[valueName] | boolean / null / string / number / function | |
Returns | object |
Example
const util = require("koru/util");assert.equals(util.toMap(), {}); assert.equals(util.toMap(null), {}); assert.equals(util.toMap(['a', 'b']), {a: true, b: true}); assert.equals(util.toMap('foo', true, [{foo: 'a'}, {foo: 'b'}]), {a: true, b: true}); assert.equals(util.toMap('foo', null, [{foo: 'a'}, {foo: 'b'}]), {a: {foo: 'a'}, b: {foo: 'b'}}); assert.equals(util.toMap('foo', null, [{foo: 'a'}], [{foo: 'b'}]), {a: {foo: 'a'}, b: {foo: 'b'}}); assert.equals(util.toMap('foo', 'baz', [{foo: 'a', baz: 1}, {foo: 'b', baz: 2}]), {a: 1, b: 2}); assert.equals(util.toMap(0, 1, [['foo', 'bar'], ['a', 1]]), {foo: 'bar', a: 1}); assert.equals(util.toMap(1, 0, [['foo', 'bar'], ['a', 1]]), {1: 'a', bar: 'foo'}); assert.equals(util.toMap('foo', (c, i) => c.foo + i, [{foo: 'a'}, {foo: 'b'}]), {a: 'a0', b: 'b1'});
util.trimMatchingSeq(a, b)
Trim the matching start and ends of two sequences
Example
const util = require("koru/util");assert.equals(util.trimMatchingSeq('acd'.split(''), 'abcd'.split('')), [[], ['b']]); assert.equals(util.trimMatchingSeq('string', 'substring'), ['', 'ubs']); assert.equals(util.trimMatchingSeq('substring', 'sub'), ['string', '']); assert.equals(util.trimMatchingSeq('1a23', '1d23'), ['a', 'd']); assert.equals(util.trimMatchingSeq('123abc456', '123def456'), ['abc', 'def']); assert.equals(util.trimMatchingSeq('diffStart', 'myStart'), ['diff', 'my']); assert.equals(util.trimMatchingSeq('diffEnd', 'diffEnds'), ['', 's']); assert.equals(util.trimMatchingSeq('diff', 'strings'), ['diff', 'strings']); assert.equals(util.trimMatchingSeq('same', 'same'), ['', '']); assert.equals(util.trimMatchingSeq('', ''), ['', '']); assert.equals(util.trimMatchingSeq('a', ''), ['a', '']); assert.equals(util.trimMatchingSeq('', 'b'), ['', 'b']);
util.union(first, ...rest)
Create a shallow copy of first
and add items to the new list, in each case only if the
item does not already exist in the new list.
Parameters
first | Array / null | a list to be copied |
[rest] | Array / null | one or more lists of elements to be added to the new list if they do not already exist in the new list |
Returns | Array | a list containing all the elements in |
Example
const util = require("koru/util");assert.equals(util.union([1, 2, 2, 3], [3, 4, 4, 5], [3, 6]), [1, 2, 2, 3, 4, 5, 6]); assert.equals(util.union([1, 2]), [1, 2]); assert.equals(util.union([1, 2], null), [1, 2]); assert.equals(util.union(null, [1, 2]), [1, 2]); assert.equals(util.union(null, null), []);
util.values(map)
Create a list of the values of the enumerable properties of map
.
Parameters
map | object | an object |
Returns | Array | a list made up of the values of the enumerable properties of |
Example
const util = require("koru/util");assert.equals(util.values({a: 1, b: 2}), [1, 2]);
util.voidToNull(object)
Convert all void values in Object to null values
Example
const util = require("koru/util");const changes = {a: void 0, b: 123, c: '', d: null}; assert.same(util.voidToNull(changes), changes); assert.equals(changes, {a: null, b: 123, c: '', d: null});
util.withId(object, _id, key = withId$)
Associate object
with _id
.
Parameters
object | object | an object to associate with |
_id | number | an id to associate with |
[key] | symbol | defaults to Symbol.withId$ |
Returns | object | an associated object which has the given |
Example
const util = require("koru/util");const jane = {name: 'Jane', likes: ['Books']}; const myKey$ = Symbol(); const assoc = util.withId(jane, 123, myKey$); assert.same(assoc.likes, jane.likes); assert.same(Object.getPrototypeOf(assoc), jane); assert.same(assoc._id, 123); assert.same(util.withId(jane, 456, myKey$), assoc); assert.same(assoc._id, 456); refute.same(util.withId(jane, 456), assoc); assert.same(util.withId(jane, 456).likes, jane.likes);
UtilDate
Utility date processing methods
Properties
defaultLang | string | The default locale language. On the browser it is |
Methods
UtilDate.atUTCDowHour(date, dow, hour)
Find the time for a UTC dow
, hour
(day of week and hour) after date
Parameters
date | number | the date to start from |
dow | number | the desired UTC day of week. |
hour | number | the desired UTC hour of day |
Returns | Date | a date which is >= |
Example
const UtilDate = require("koru/util-date");const THU = 4; let date = uDate.atUTCDowHour(Date.UTC(2014,4,5), THU, 9); assert.same(date.toISOString(), '2014-05-08T09:00:00.000Z'); assert.same(uDate.atUTCDowHour(123 + +date, THU, 8).toISOString(), '2014-05-15T08:00:00.123Z'); date = uDate.atUTCDowHour(Date.UTC(2014,4,10), THU, 9); assert.same(date.toISOString(), '2014-05-15T09:00:00.000Z');
UtilDate.compileFormat(format, lang=defaultLang)
Make a function that converts a date to text given a specified format.
Parameters
format | string / object | If an if a
|
lang | string | A string with a BCP 47 language tag. Defaults to the browser's or nodejs's default locale. |
Returns | function | A function that will format a given Date or epoch
|
Example
const UtilDate = require("koru/util-date");const d = new Date(2017, 0, 4, 14, 3, 12); const format = uDate.compileFormat('D MMM YYYY h:mma', 'en'); assert.same(format(d), '4 Jan 2017 2:03pm'); assert.same(format(12*HOUR + +d), '5 Jan 2017 2:03am');const format = uDate.compileFormat({weekday: 'long'}, 'en'); assert.same(format(d), 'Wednesday');
UtilDate.format(date, format, lang=defaultLang)
Format time with a specified format
Parameters
date | Date / number | format this date |
format | string / object | the format use. See compileFormat |
[lang] | string | A string with a BCP 47 language tag. Defaults to the browser's or nodejs's default locale. |
Returns | string |
Example
const UtilDate = require("koru/util-date");const d = new Date(2017, 0, 4, 14, 3, 12); assert.same(uDate.format(d, 'D MMM YYYY h:mma'), '4 Jan 2017 2:03pm'); assert.same(uDate.format(12*HOUR + +d, 'D MMM YYYY h:mma', 'en'), '5 Jan 2017 2:03am'); assert.same(uDate.format(d, `DD'YY hh`), `04'17 02`); assert.same(uDate.format(d, `m[m]`), `3m`); assert.same(uDate.format(d, `MM`), `01`); assert.same(uDate.format(d, `ss`), `12`); assert.same(uDate.format(d, `s`), `12`); d.setSeconds(9); assert.same(uDate.format(d, `ss`), `09`); assert.same(uDate.format(d, `s`), `9`); assert.same(uDate.format(d, 'MMM'), Intl.DateTimeFormat(void 0, {month: 'short'}).format(d));const d = new Date(2017, 0, 4, 14, 3, 12); const format = uDate.compileFormat({weekday: 'long'}, 'en'); assert.same(format(d), 'Wednesday'); if (isClient) { assert.same( uDate.format(d, {}, 'de'), `4.1.2017`); assert.same( uDate.format(d, {}, 'en-us'), `1/4/2017`); } assert.same( uDate.format(d, {}), Intl.DateTimeFormat(uDate.defaultLang).format(d));
UtilDate.parse(dateStr)
Convert string to date. Like Date.parse
except assumes local timezone if none given
Parameters
dateStr | string | A string representing a simplification of the ISO 8601 calendar date extended format (other formats may be used, but results are implementation-dependent). |
Returns | Date | parsed |
Example
const UtilDate = require("koru/util-date");assert.equals(uDate.parse('2017-12-26'), new Date(2017, 11, 26)); assert.equals(uDate.parse('2017-12-26T00:00:00Z'), new Date("2017-12-26T00:00:00Z"));
UtilDate.relative(delta, minTime=60000, lang=defaultLang)
convert time to a relative text
Example
const UtilDate = require("koru/util-date");assert.same(uDate.relative(0, 0), 'now'); assert.same(uDate.relative(20000, 10000, 'en'), 'in 20 seconds'); assert.same(uDate.relative(499, 0), 'now'); assert.same(uDate.relative(45000,), 'in 1 minute'); assert.same(uDate.relative(-45000), '1 minute ago'); assert.same(uDate.relative(61*MIN), 'in 1 hour'); assert.same(uDate.relative(90*MIN), 'in 2 hours'); assert.same(uDate.relative(24*HOUR), 'tomorrow'); assert.same(uDate.relative(48*HOUR), 'in 2 days'); assert.same(uDate.relative(26*DAY), 'in 26 days'); assert.same(uDate.relative(46*DAY), 'in 2 months'); assert.same(uDate.relative(-319*DAY), '10 months ago'); assert.same(uDate.relative(530*DAY), 'in 17 months'); assert.same(uDate.relative(548*DAY), 'in 2 years'); assert.same(uDate.relative(5000*DAY), 'in 14 years'); assert.same(uDate.relative(-5000*DAY), '14 years ago'); // Not all browsers do the same thing const thismin = uDate.relative(0) === 'this minute' ? 'this minute' : 'in 0 minutes'; assert.same(uDate.relative(0), thismin); assert.same(uDate.relative(29000), thismin);
UtilDate.shiftToLocale(date)
Treat UTC time as locale time; move the timezone without changing the locale time
Example
const UtilDate = require("koru/util-date");assert.equals(uDate.shiftToLocale(new Date('2017-12-26T14:00Z')), new Date(2017, 11, 26, 14));
UtilDate.shiftToUTC(date)
Treat locale time as UTC time; move the timezone without changing the locale time
Example
const UtilDate = require("koru/util-date");assert.equals(uDate.shiftToUTC(new Date(2017, 11, 26, 14)), new Date('2017-12-26T14:00Z'));
UtilDate.toDiscrete(date, unit)
Return the date
at the start of the unit
Parameters
date | Date | to discretize |
unit | number | the part to set to zero (and the parts below) |
Returns | Date | with parts set to zero |
Example
const UtilDate = require("koru/util-date");const dt = new Date(2017, 2, 6, 13, 17, 36, 123); assert.equals(uDate.toDiscrete(dt, uDate.DAY), new Date(2017, 2, 6)); assert.equals(uDate.toDiscrete(dt, uDate.HOUR), new Date(2017, 2, 6, 13)); assert.equals(uDate.toDiscrete(dt, uDate.MIN), new Date(2017, 2, 6, 13, 17)); assert.equals(uDate.toDiscrete(dt, uDate.SEC), new Date(2017, 2, 6, 13, 17, 36));
UtilDate.toSunday(date)
Get the last start of sunday.
Example
const UtilDate = require("koru/util-date");const dt = uDate.toSunday(new Date(2017, 5, 2, 15)); assert.equals(dt, new Date(2017, 4, 28)); assert.equals(uDate.toSunday(dt), new Date(2017, 4, 28)); assert.equals(uDate.toSunday(new Date(2017, 4, 28)), new Date(2017, 4, 28));
WebServer
The default web-server created from WebServerFactory. IdleCheck is used to keep track of active requests.
Config | |
defaultPage | defaults to |
extras | any extra root level file mappings in |
host | listen on the specified address |
indexcss | the file to serve for |
indexhtml | the file to serve for |
indexjs | the file to serve for |
indexjsmap | the file to serve for |
port | listen on the specified port |
Methods
WebServer.start()
Parameters
Returns | Promise(object) |
Example
const WebServer = require("koru/web-server");const {Server} = requirejs.nodeRequire('http'); const listen = stub(Server.prototype, 'listen').yields(); webServer.start(); assert.calledWith(listen, webServerModule.config().port);
WebServerFactory
Factory for creating web-servers.
Methods
WebServerFactory(host, port, root, DEFAULT_PAGE='/index.html', SPECIALS={}, transform)
Create a new web server. The npm package send is used to serve files.
Parameters
host | string | |
port | string | |
root | string | Serve files relative to path. |
[DEFAULT_PAGE] | string | |
[SPECIALS] | object | |
[transform] | ||
Returns | object |
Example
const WebServerFactory = require("koru/web-server-factory");const http = requirejs.nodeRequire('http'); stub(http, 'createServer'); webServer = WebServerFactory( '0.0.0.0', '80', '/rootDir/', '/index2.html', {gem: (match) => [match[0], '/path-to-gems']}); assert.calledWith(http.createServer, webServer.requestListener);
WebServerFactory#start()
Parameters
Returns | Promise(object) |
Example
const WebServerFactory = require("koru/web-server-factory");const {Server} = requirejs.nodeRequire('http'); const listen = stub(Server.prototype, 'listen').yields(); webServer.start(); assert.calledWith(listen, '9876', 'localhost');
WebServerFactory#stop()
Example
const WebServerFactory = require("koru/web-server-factory");const {Server} = requirejs.nodeRequire('http'); const close = stub(Server.prototype, 'close'); webServer.stop(); assert.called(close);