koru/main

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.

Parameters

pathstring

source path of resource.

Returns

string

build path for 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.

Parameters

moduleOrIdobject / string
callbackobject / function

Example

const koru = require("koru/main");
const myModule = {id: 'myModule', onUnload: stub()}; const callback = {stop() {}}; koru.onunload(myModule, callback); assert.calledWith(myModule.onUnload, callback);
koru/btree

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 Array#sort) defaults to

(a, b) => a == b ? 0 : a < b ? -1 : 1
[unique]boolean

if true do not add entry if add called with key already in tree;

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

valuenumber

find entry with same keys as value

Returns

undefined / object

undefined if not found otherwise the value added to the tree

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

valuenumber

contains the keys to find.

Returns

undefined / object

undefined if node can't be found otherwise the matching node.

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

Parameters

valuenumber

Returns

object / null

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

Parameters

valuenumber

Returns

object / null

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);
koru/changes

Changes

Methods

Changes.applyAll(attrs, changes)

Apply all commands to an attributes object. Commands can have:

  1. a $match object which assert the supplied fields match the attribute
  2. a $partial object which calls applyPartial for each field; OR
  3. be a top level replacement value

Parameters

attrsobject
changesobject

Returns

object

undo command which when applied to the updated attributes reverts it to its original content. Calling original on undo will return the original commands 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

Parameters

attrsobject
keystring
changesobject

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)

Parameters

fieldstring
fromobject
toobject

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

Parameters

changesobject
fieldstring

Returns

boolean

Example

const Changes = require("koru/changes");
Changes.has({foo: undefined}, "foo");
// returns true
Changes.has({foo: false}, "foo");
// returns true
Changes.has({foo: undefined}, "bar");
// returns false
Changes.has({$partial: {foo: undefined}}, "foo");
// returns true
Changes.has({$partial: {foo: undefined}}, "bar");
// returns false

Changes.merge(to, from)

Merge one set of changes into another.

Parameters

toobject

the destination for the merges

fromobject

the source of the merges. Values may be assignment (not copied) to to so use deepCopy if that is not intended.

Returns

object

to

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

fromobject
toobject
[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

Parameters

undoobject

Returns

object

Example

const Changes = require("koru/changes");
Changes.original({foo: 123});
// returns {foo: 456}
koru/client/mock-cache-storage

MockCacheStorage

A Mock version of CacheStorage

Limitations

match only supports strictly checking the url.

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')));
koru/client/uncache

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);
koru/crypto/acc-sha256

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

textstring

The message to add to hash

[hash]Array

should be an array of 8 32bit integers. The default is the standard sha256 initial hash values.

Returns

Array

the modified hash; not a copy.

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

hashArray

an array of 32bit integers. Usually produced from add.

Returns

string

the hex equivalent of the hash

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

textstring / 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');
koru/css/loader

CssLoader

css/loader allows dynamic replacement of css and less files when their contents change.

Methods

constructor(session)

Construct a css loader

Parameters

sessionobject

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

dirstring

Example

const CssLoader = require("koru/css/loader");
const cssLoader = new CssLoader();
cssLoader.loadAll("koru/css");
koru/dir-watcher

DirWatcher

Methods

constructor(dir, callback, callOnInit=false)

Watch recursively for file and directory changes

Parameters

dirstring

the top level directory to watch

callbackfunction

called with the path that changed and a fs.Stats object or undefined if

callOnInitboolean

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()));
koru/dlinked-list

DLinkedList

A double linked list. The list is iterable. Nodes can be deleted without needing this list.

Properties

#headnull / object

Retrieve the node that is the head of the list

#tailnull / 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

  • value the object passed to add
  • delete a function to delete the entry

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

  • value the object passed to add
  • delete a function to delete the entry

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

callbackfunction

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)]);
koru/dom

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

elmNode / Element

the node to start the search from

parentElement

the parent of the node being looked for.

Returns

Element / null

null if elm is not a descendant of parent

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.

See insertStartEndMarkers

Parameters

startMarkerNode

Returns

Node

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.

Parameters

elmnull / Element

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

objectElement / object

The object to calculate for.

Returns

Object

the rect parameters contains left, top, width, height and aliases.

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

elmElement / Node

the element to start from. Text elements start from their parent node.

selectorstring

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

elmElement

the element to start from. Text elements start from their parent node.

selectorstring

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

parentElement

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

elmElement
regionhtml-doc::Element / object

either a Dom Element or a boundingClientRect

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 true
Dom.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 true
Dom.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 false
Dom.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 false
Dom.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.

Parameters

elmElement
regionOrNodeElement / object

Returns

boolean

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 false
Dom.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 true
Dom.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 false
Dom.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

optsobject

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.

Parameters

callbackfunction

Returns

function

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

Parameters

elmElement / null

Returns

boolean

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

startNode

the start marker; see insertStartEndMarkers

Example

const Dom = require("koru/dom");
Dom.removeInserts({});

Dom.reposition(pos='below', options)

Align element with an origin

Parameters

posstring
optionsobject

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)))

Attach a Ctx to an Element

Parameters

elmElement

the element to attache the ctx to.

[ctx]Ctx

the context to attach. By default will create a new ctx with no template and a parent ctx of the element.

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);
koru/dom/ctx

Ctx

Ctx (Context) is used to track DOM elements

Properties

#dataobject

The data associated with an element via this context

#parentCtxundefined / 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'.

Parameters

elmElement
typestring
callbackfunction
[opts]object

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');
koru/dom/html

Html

Utilities for building and converting Nodes

Methods

Html.escapeHTML(text)

Escape special html characters

Parameters

textstring

Returns

string

Example

const Html = require("koru/dom/html");
assert.same(Html.escapeHTML('<Testing>&nbsp;'), '&lt;Testing&gt;&amp;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

bodyobject

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

Parameters

nodeElement
[ns]

Returns

object / Array

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

Parameters

hrefstring
[attrs]object

Returns

Element

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');
koru/dom/template

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.

Parameters

parentTemplate
blueprintobject

Returns

Template

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 module is unloaded

blueprintobject

A blue print is usually built by template-compiler which is called automatically on html files loaded using require('koru/html!path/to/my-template.html')

[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

Parameters

dataobject
[parentCtx]

Returns

Element / Node

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>`
koru/enumerable

Enumerable

Enumerable wraps iterables with Array like methods.

Methods

constructor(iter)

Create new Enumerable instance

Parameters

iterobject / Function

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

tonumber
[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

Parameters

objectobject
mapperfunction

Returns

Function

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

Parameters

objectobject
mapperfunction

Returns

Array

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

Parameters

iterobject
mapperfunction

Returns

Array

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

objectobject

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

Parameters

objectArray

Returns

Function

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

testfunction

a function called for each iteration with the argument: currentValue - the current value of the iterator. Should return true or false.

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

testfunction

a function called for each iteration with the argument: currentValue - the current value of the iterator. Return true to keep the element, otherwise false.

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

testfunction

a function called for each iteration with the argument: currentValue - the current value of the iterator. Should return true or false.

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

mapperfunction

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

Parameters

reducerfunction
[seed]number

Returns

number

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

testfunction

a function called for each iteration with the argument: currentValue - the current value of the iterator. Should return true or false.

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));
koru/format

format

Text formatter with language translation

Methods

format(fmt, ...args)

Format a string with parameters

Parameters

fmtstring / Array
[args]string / number / object / undefined

Returns

string

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&lt;&#x27;he llo&quot;&#x60;&amp;&gt;"
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&lt;fnord&gt;"
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

Parameters

fmtstring

Returns

Array

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')

Parameters

textnull / string / Array
[lang]string

Returns

null / string

Example

const format = require("koru/format");
format.translate(null);
// returns null
format.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"
koru/fs-tools

FsTools

Convenience wrapper around some node fs functions

Methods

FsTools.appendData(path, data)

Parameters

pathstring
datastring

Returns

Promise(string)

Example

const FsTools = require("koru/fs-tools");
const ans = fst.appendData('/my/file.txt', 'extra data');

FsTools.readlinkIfExists(path, options)

Parameters

pathstring
[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'});
koru/future

Future

Future is a utility class for waiting and resolving promises.

Properties

isResolved

true when promise is resolved

#promisePromise(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);
koru/geometry

Geometry

Methods

Geometry.bezierBox(ps, curve)

Calculate the boundry box for a cubic bezier curve

Parameters

psArray

start point

curveArray

of the form [csx,csy, cex,cey, pex,pey] where:

  • csx,csy is the control point for the start
  • cex,cey is the control point for the end
  • pex,pey is the end point

Returns

object

boundryBox in format {left, top, right, bottom}

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

pointArray

the point to project on to curve

psArray

start point

curveArray

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 0
Geometry.closestT([100, 70], [300, 30], [100, 70]);
// returns 1
Geometry.closestT([-15, 50], [300, 30], [100, 70]);
// returns 1
Geometry.closestT([230, 150], [300, 30], [100, 70]);
// returns 0.4519230769230769
Geometry.closestT([-1000, -3000], [300, 30], [100, 70]);
// returns 1
Geometry.closestT([2500, 43], [300, 30], [100, 70]);
// returns 0
Geometry.closestT([300, 130], [300, 130], [-400, -200, 1140, 500, 200, 100]);
// returns 0
Geometry.closestT([200, 100], [300, 130], [-400, -200, 1140, 500, 200, 100]);
// returns 1
Geometry.closestT([-15, 50], [300, 130], [-400, -200, 1140, 500, 200, 100]);
// returns 0.1970015204601111
Geometry.closestT([195, 100], [300, 130], [-400, -200, 1140, 500, 200, 100]);
// returns 1
Geometry.closestT([600, 200], [300, 130], [-400, -200, 1140, 500, 200, 100]);
// returns 0.7498621750479387
Geometry.closestT([-1000, -3000], [300, 130], [-400, -200, 1140, 500, 200, 100]);
// returns 0.20026090643926941
Geometry.closestT([-14, 0], [0, 0], [0, 0, 20, 25, 20, 25]);
// returns 0
Geometry.closestT([10, 12.5], [0, 0], [0, 0, 20, 25, 20, 25]);
// returns 0.49999913899703097
Geometry.closestT([20, 25], [0, 0], [0, 0, 20, 25, 20, 25]);
// returns 1
Geometry.closestT([25978, 28790], [10000, 20000], [-5000, -10000, 57500, 70000, 40000, 30000]);
// returns 0.5005837534888542

Geometry.combineBox(a, b)

Combine two boundry boxes

Parameters

aobject

the first boundry box which is modified to include the second

bobject

the second boundry box

Returns

object

a

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

boxobject

the boundry box which is modified to include the point

xnumber

the x coordinate of the point

ynumber

the y coordinate of the point

Returns

object

box

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)

Parameters

pointsArray
anglenumber

Returns

Array

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

tnumber

0 < t < 1 where 0 is start point and 1 is end point

psArray

start point

curveArray

bezier curve (See bezierBox)

Returns

Array

the second curves in form [cs, ce, pe]

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

tnumber

0 < t < 1 where 0 is start point and 1 is end point

psArray

start point

curveArray

bezier curve (See bezierBox) or end point for line

Returns

Array

the midpoint in form [x, y]

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

tnumber

0 < t < 1 where 0 is start point and 1 is end point

psArray

start point

curveArray

bezier curve (See bezierBox) or end point for line

Returns

Array

the tangent normalized vector in form [xd, yd]

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]
koru/idle-check

IdleCheck

IdleCheck keeps count of usage and notifies when idle.

Properties

singletonIdleCheck

The default IdleCheck. It is used by WebServerFactory and server-connection-factory

Methods

constructor()

Example

const IdleCheck = require("koru/idle-check");
new IdleCheck();

IdleCheck#waitIdle(func)

waitIdle waits until this.count drops to zero.

Parameters

funcfunction

Example

const IdleCheck = require("koru/idle-check");
const check = new IdleCheck(); const callback = stub(); check.waitIdle(callback); assert.called(callback);
koru/koru-error

koru.Error

Main error class for koru errors.

Properties

#errornumber

The http error code for the error

#reasonstring / object

The textual reason or an object with specific details

Methods

constructor(error, reason)

Create a new koru.Error

Parameters

errornumber

an http error code

reasonstring / 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']]});
koru/linked-list

LinkedList

A single linked list. The list is iterable.

Properties

#backobject

the back node in the list

#backValuenumber

the back value in the list

#frontobject

the front node in the list

#frontValuenumber

the front value in the list

#sizenumber

The number of nodes in the list

Methods

LinkedList#addBack(value)

Add value to back of list.

Parameters

valuenumber

Returns

object

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

callbackfunction

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

Parameters

valuenumber

Returns

object

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

nodeobject

the node to remove

[prev]undefined / object

where to start the search from. Defaults to front

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)]);
koru/make-subject

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

subjectobject

the object observe

[observeName]string

method name to call to start observing subject (defaults to OnChange)

[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);
koru/match

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

anyMatch

match anything (or nothing)

arrayMatch

match any object that is true for Array.isArray

baseObjectMatch

match any object where constructor is Object

dateMatch

match any date

errorMatch

match any error

funcMatch

match any function

idMatch

match a valid model _id

integerMatch

match any integer

matchMatch

match any matcher

nilMatch

match undefined or null

nullMatch

match null

objectMatch

match anything of type object except null

optionalfunction

match a standard matcher or null or undefined; match.optional.date

stringMatch

match any string

symbolMatch

match any symbol

undefinedMatch

match undefined

Methods

match.equal(expected, name='match.equal')

Match expected using util.deepEqual

Parameters

expectedArray
[name]

Returns

Match

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

Parameters

expectedobject
[name]

Returns

Match

Example

const match = require("koru/match");
match.is({foo: 123});
// returns match.is({foo: 123})

match(test, name)

Build a custom matcher.

Parameters

testFunction / 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.

Parameters

argMatch

the matcher to invert.

Returns

Match

Example

const match = require("koru/match");
assert.isTrue(match.not(match.string).test(1)); assert.isFalse(match.not(match.number).test(1));
koru/match::Match

Match

Class of matchers.

Methods

Match#$throwTest(value)

Throw if test fails

Parameters

valuenull / number

Returns

boolean

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.

Parameters

actualnull / number
[$throwTest]

Returns

boolean

Example

const match = require("koru/match");
assert.isTrue(match.any.test(null)); assert.isFalse(match.func.test(123));
koru/math-util

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);
koru/migrate/migration

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

dirPathstring

the directory containing the migration files

posstring

Migration files contained in the dirPath directory which have not yet been processed and their names are <= pos are applied; names > pos which have already been applied are reverted.

[verbose]boolean

print messages to console.log as files are processed.

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 undefined
migration.migrateTo("koru/migrate/test-migrations", "2015-06-19T17-49-31~");
// returns undefined
migration.migrateTo("koru/migrate/test-migrations", " ", true);
// returns undefined
koru/migrate/migration::Commander

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

tableNamestring

the name of the table to add the columns to.

argsobject / string

either a list of column:type strings or an object with column keys and either; type as values; or an object value containing a type key and optional default key.

Example

const Migration = require("koru/migrate/migration");
// file contents
define(() => (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

tableNamestring

The name of the table to add the index to.

specobject

The specification of the index containing:

name type desc
columns Array a list of column names with optional ' DESC' suffix.
[name] String The name of index. default is made from table name and column names.
[unique] Boolean true if index entries are unique.
[where] String An SQL expression to determine if a record is included in the index.

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

namestring

the name of the table

fieldsobject

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:

indexesArray

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

addFunction

a function to call when adding a migration (migrate up) to the DB. Is passed a pg::Client instance.

revertFunction

a function to call when reverting a migration (migrate down) to the DB. Is passed a pg::Client instance.

resetTablesArray

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'], }); });
koru/model/main

Model

Object persistence manager. Defines application models.

koru/model/base-model

BaseModel

The base class for all models

Methods

onChange(callback)

Observe changes to model records.

Parameters

callbackfunction

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

Parameters

docnull / BaseModel

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.

Parameters

attributesobject
[allow_id]

Returns

BaseModel

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 pseudo_field synced to the db-broker.dbId. It defines a model getter like belongs_to does. It can only be defined once per model.

Book.defineFields({ publisher_id: {type: 'belongs_to_dbId'}, title: 'text', }); const book = await Book.create({title: 'White Fang'}); assert.equals((await book.$reload(true)).attributes, {title: 'White Fang', _id: m.id}); assert.same(book.publisher_id, dbBroker.dbId); await Publisher.create({name: 'Macmillan', _id: 'default'}); assert.same(book.publisher.name, 'Macmillan');

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

fieldsobject

an key-value object with keys naming the fields and values defining the field type, the validators and any of the following generic options:

option usage
default The field's default value to assign in a new document
pseudo_field The field does not have accessors and is not set in <Model>.$fields property
accessor Use accessor.get and accessor.set for this field.
An accessor of false means no accessors
readOnly Saving a change to the field should not be allowed
changesOnly

Only changes to the field are validated

Book.defineFields({ pages: {type: 'number', number: {'>': 0}, changesOnly: true}, }); const book = Book.build(); book.attributes.pages = -1; assert(book.$isValid()); // wrong but no change book.pages = 'alsoWrong'; refute(book.$isValid()); // wrong and changed assert.equals(book[error$], {pages: [['not_a_number']]}); book.pages = 2; assert(book.$isValid()); // changed and okay

model An associated model for the field

Types and validators may also make use of other specific options.

The field _id is added by default with a type of id. It can be given an option of auto to specify that the DB will generate an id for this field if none is supplied.

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.

Parameters

idstring

Returns

BaseModel

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.

Parameters

idstring

usally the id of a DB record.

Returns

boolean

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

idstring

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.

Parameters

funcsobject

Returns

BaseModel

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.

Parameters

funcsobject

Returns

BaseModel

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 "assert" calls $assertValid otherwise calls $isValid. If "force" will save even if validation fails.

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.

Parameters

[changes]string / object

defaults to this.changes

Returns

BaseModel / null

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);
koru/model/db-broker-client

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

DBRunnerobject
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);
koru/model/db-broker-server

dbBroker

dbBroker allows for multiple databases to be connected to one nodejs instance

Properties

dbClient / null

The database for the current thread

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

DBRunnerobject
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);
koru/model/doc-change

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" when type is "add", "add" when type 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

#changesobject

Retrieve the changes that were made to doc. See BaseModel#$invertChanges

#isAddboolean

Is this change a add

#isChangeboolean

Is this change from a {#.change

#isDeleteboolean

Is this change from a delete

#modelobject

Retrieve the model of the doc.

#wasBaseModel

Retrieve the doc with the undo set as changes. See BaseModel#$withChanges

Methods

DocChange.add(doc, flag)

Create a model change representing an add.

Parameters

docBaseModel
flagstring

Returns

DocChange

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.

Parameters

docBaseModel
undoobject
flagstring

Returns

DocChange

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.

Parameters

docBaseModel
flagstring

Returns

DocChange

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.

Parameters

fieldstring

Returns

boolean

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

Parameters

fieldsstring

Returns

boolean

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.

Parameters

fieldstring

Returns

Function

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 DocChanges for each property that is different between two objects

Parameters

fieldstring
[flag]

Returns

Function

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);
koru/model/main-client

Model

koru/model/mq-factory

MQFactory

Manage durable Message queues.

Methods

MQFactory#getQueue(name)

Get a message queue for current database

Parameters

namestring

the name of the queue

Returns

MQ

the message queue

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

namestring

the name of the queue

actionfunction

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
koru/model/mq-factory::MQ

MQ

The class for queue instances.

See MQFactory#getQueue

Methods

MQ#async add({dueAt=util.newDate(), message})

Add a message to the queue. The message is persisted.

Parameters

dueAtDate
messageobject

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

_idnumber

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);
koru/model/ps-sql

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

queryStrstring

The sql query string, with symbolic parameters, to prepare

modelBaseModel

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

paramsobject

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

paramsobject

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

paramsobject

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

paramsobject
[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');
koru/model/query

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

docChangeDocChange

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

callbackfunction

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

Parameters

callbackfunction

called with one argument DocChange detailing the change.

Returns

object

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)

Parameters

paramsstring / object
[value]object / Array

Returns

Query

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

paramsstring / object

field or directive to match on. If is object then whereNot is called for each key.

[value]object / primitive

corresponding to params

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:

  1. whereSql(queryString, params) queryString is a sql where-clause where $n parameters corresponds to the nth-1 position in params.
  2. whereSql(queryString, properties) queryString is a sql where-clause where {$varName} expressions within the string get converted to parameters corresponding the the properties.
  3. whereSql(sqlStatement, properties) sqlStatment is a pre-compiled SQLStatement and properties are referenced in the statement.
  4. `` whereSqlqueryTemplate queryTemplate a sql where-clause where ${varName} expressions within the template get converted to parameters.

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');
koru/model/query-idb

QueryIDB

Support client side persistence using indexedDB

For testing one can use mockIndexedDB in replacement of indexedDB

Properties

#isIdleboolean

true if db has completed initializing and is not closed and has no outstanding updates, otherwise false

#isReadyboolean

true if db has completed initializing and is not closed, otherwise false

Methods

constructor({name, version, upgrade, catchAll})

Open a indexedDB database

Parameters

namestring

the name of the database

[version]number

expected version of database

[upgrade]function

function({db, oldVersion}) where db is the QueryIDB instance and oldVersion is the current version of the database

[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

namestring

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

modelNamestring
queryobject

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

Parameters

modelNamestring
queryobject
directionnull
actionfunction

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

Parameters

modelNamestring
idstring

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

namestring

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

modelNamestring
_idstring

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

modelNamestring
[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

Parameters

modelNamestring
namestring

Returns

object

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.

Parameters

modelNamestring
recobject

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

Parameters

nstring
recsArray

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

Parameters

bodyfunction

the returns an IDBRequest

Returns

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

Parameters

modelNamestring
recobject

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

dcDocChange

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

Parameters

tablesstring
modestring
oncompletefunction
onabortfunction

Returns

object

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
koru/model/rich-text-validator

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.

Parameters

docBaseModel
fieldstring
optionsboolean / string

use 'filter' to remove any invalid markup.

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.

Parameters

docBaseModel
fieldstring
optionsboolean / string

use 'filter' to remove any invalid markup.

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']]);
koru/model/sql-query

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

modelobject

models used to resolve symbolic parameter types

queryStrstring

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

paramsobject

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

paramsobject

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

paramsobject
callbackfunction

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.

Parameters

paramsobject

Returns

Function

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']);
koru/model/test-factory

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

modelsobject

Each function in models has the name of a model, takes an options parameter and returns a Builder. The options parameter will contains an object with field property values and other useful options.

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

Parameters

keystring

Returns

number

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);
koru/model/test-factory::Builder

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

modelNamestring

The name of the model to build.

attributesobject

The field values to assign to the model

defaultsobject

The field values to use if the field is not in attributes

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

fieldstring

The name of the field to add.

valuestring / 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

pPromise(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

refstring
[doc]Promise(BaseModel) / function

The default document for the field. if-and-only-if a default value is needed then:

  • if doc is a function the function will be executed and its return result will be used to replace doc. Then;
  • if undefined use the last document created for the reference model.
  • otherwise create a factory default document for the reference.

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

Parameters

fieldstring
keystring

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', 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

valueboolean / string

false is for the (default) method of bypassing the model validation and inserting document directly into the DB. true or "assert" is for using BaseModel#$save("assert") and "force" is for using BaseModel#$save("force")

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]);
koru/model/trans-queue

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

callbackfunction

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

callbackfunction

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

callbackfunction

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 transaction method.

[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);
koru/model/validation

Val

Utilities to help validate models.

Methods

Val.addError(doc, field, ...args)

Add an error to an object; usually a model document.

Parameters

docobject
fieldstring
argsstring

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.

Parameters

docBaseModel
specobject
[new_spec]object / null

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

moduleModule
mapobject

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);
koru/model/validation::Error

Error

Methods

Error.msgFor(doc, field, other_error)

Extract the translated error message for a field

Parameters

docobject / string / Array
[field]string
[other_error]

Returns

string

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

Parameters

docobject

Returns

string

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');
koru/model/validators/main

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

koru/model/validators/associated-validator

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

docBaseModel
fieldstring
optionsboolean / object

true is the same as an empty object ({}). The following properties are supported:

  • filter will remove any invalid ids rather than invalidating the document.

    addAuthors(['a123', 'a789']); Book.defineFields({ author_ids: {type: 'has_many', associated: {filter: true}}, }); const book = Book.build({author_ids: ['a123', 'a456', 'a789']}); assert(book.$isValid()); assert.equals(book.author_ids, ['a123', 'a789']);

  • finder is a function that is called with the document as this and the ids to check as the first argument.

    addAuthors(['a123', 'a789'], {unpublished: ['a456']}); Book.defineFields({ author_ids: {type: 'has_many', associated: {finder(values) { return Author.where('published', this.published).where('_id', values); }}}, published: {type: 'boolean'}, }); const book = Book.build({author_ids: ['a123', 'a456']}); assert(book.$isValid()); const book2 = Book.build({author_ids: ['a123', 'a456'], published: true}); refute(book2.$isValid());

    If the document has a prototype method named <field-name-sans/_ids?/>Find then it will be used to scope the query unless finder is present.

    addAuthors(['a123', 'a789'], {unpublished: ['a456']}); function authorFind(values) { return Author.where('published', this.published).where('_id', values); } Book.defineFields({ author_ids: {type: 'has_many', associated: true}, published: {type: 'boolean'}, }); const book = Book.build({author_ids: ['a123', 'a456']}); assert(book.$isValid()); book.authorFind = authorFind; assert(book.$isValid()); const book2 = Book.build({author_ids: ['a123', 'a456'], published: true}); assert(book2.$isValid()); book2.authorFind = authorFind; refute(book2.$isValid());

typestring

When the field type is 'belongs_to' the field value must be a string containing the id of an association document.

addAuthors(['a123']); Book.defineFields({ author_id: {type: 'belongs_to', associated: true}, }); const book = Book.build({author_id: 'wrongId'}); refute(book.$isValid()); assert.equals(book[error$].author_id, [['not_found']]); book.author_id = 'a123'; assert(book.$isValid());

Otherwise the field is expected to be an array of ids

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']]);

[changesOnly]boolean

When this fieldOption is true association only checks for ids which have changed.

addAuthors(['a123', 'a789']); Book.defineFields({ author_ids: {type: 'has_many', associated: true, changesOnly: true}, }); const book = Book.build(); book.attributes.author_ids = ['a123', 'badId1', 'badId2']; book.author_ids = ['badId1', 'a123', 'a789']; assert(book.$isValid()); // new id 'a789' exists book.author_ids = ['badId3', 'a123', 'a789']; refute(book.$isValid()); // new id 'badId3' not found assert.equals(book[error$].author_ids, [['not_found']]);

[model]BaseModel

A model will override the associated model.

addAuthors(['a123', 'a789']); Book.defineFields({ authors: {type: 'has_many', associated: true, model: Author}, }); const book = Book.build({authors: ['a123']}); assert(book.$isValid()); const book2 = Book.build({authors: ['a456']}); refute(book2.$isValid());

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']]);
koru/model/validators/length-validator

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.

Parameters

docBaseModel
fieldstring
lengthnumber

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]]);
koru/model/validators/text-validator

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.

Parameters

docBaseModel
fieldstring
boolTypestring / boolean

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

docBaseModel
fieldstring
optionsstring

"upcase" to convert field to upper case. "downcase" to convert field to lower case. Other values are ignored.

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());
koru/model/validators/validate-validator

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

docBaseModel
fieldstring
validatorfunction

A function which has the document as this and takes one argument field name. If an error is found use Val.addError to invalidate document or return a string containing the error message.

Returns

Promise(undefined)

an optional string which is an error message if invalid. The message will be added to the documents errors for the field.

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');
koru/module-graph

ModuleGraph

Methods

ModuleGraph.findPath(start, goal)

Finds shortest dependency path from one module to another module that it (indirectly) requires.

Parameters

startModule

the module to start from

goalModule

the module to look for

Returns

[Module,...]

from start to goal

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

supplierModule

the module to look for

userModule

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 true
ModuleGraph.isRequiredBy({Module:koru/module-graph-test}, {Module:koru/util-base});
// returns false
koru/mutex

Mutex

Mutex implements a semaphore lock.

Properties

#isLockedboolean

true if mutex is locked

#isLockedByMeboolean

true if mutex is locked by the current thread. On client there is only one thread so same result as isLocked

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);
koru/observable

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

callbackfunction

called with the arguments sent by notify

Returns

object

a handle that has the methods

  • callback the function passed to add
  • stop a function to stop observing -- stop can be called without a this

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

callbackfunction

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);
koru/parse/html-parser

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

codestring

the HTML markup to parse

[filename]string

A filename to use if a markup error is discovered.

onopentagfunction

called when a start tag is found. onopentag(<string> name, <object> attrs, <string> code, <int> spos, <int> epos)

ontextfunction

called when plain text is found ontext(<string> code, <int> spos, <int> epos)

oncommentfunction

called when a comment is found oncomment(<string> code, <int> spos, <int> epos)

onclosetagfunction

called when a tag scope has concluded (even when no end tag) onclosetag(<string> name, <string> type, <string> code, <int> spos, <int> epos)

  • type is; end for an end tag, self for self closing start tag or missing for no end tag.

Example

const HtmlParser = require("koru/parse/html-parser");
const code = ` <div id="top" class="one two" disabled> <input name="title" value=" &quot;hello"> <!-- I'm a comment --> <p> <b>bold</b> <p> <br> <span>content &lt;inside&gt;</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&quot;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);
koru/parse/js-printer

JsPrinter

Methods

constructor({input, write, ast=parse(input)})

Create a new javascript printer

Parameters

inputstring
writefunction
[ast]

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());
koru/pg/driver

pg

Interface to PostgreSQL.

Config

url

The default url to connect to; see libpq - Connection
Strings
for examples. Note ssl is not support at this time.

Properties

defaultDbClient

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

urlstring
[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")
koru/pg/driver::Client

Client

A connection to a database.

See pg.connect

Properties

#inTransactionboolean

determine if client is in a transaction

Methods

constructor(url, name, formatOptions)

Parameters

urlstring / undefined
namestring
[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

argsstring

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)

Parameters

colstring
[type]string / object
[conn]

Returns

string

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):

  1. query(text) where no parameters are in the query text
  2. query(text, params) where parameters correspond to array position (1 is first position)
  3. query(sqlStatment, params) where sqlStatment is a pre-compiled SQLStatement and params is a key-value object.
  4. query(text, params) where params is a key-value object
  5. query`queryTemplate`

Aliases

exec

Parameters

argsstring / 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

argsstring

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'); }
koru/pg/sql-statement

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

#textstring

The converted query text with inserted numeric parameters; changes when convertArgs is called.

Methods

constructor(text='')

Compile a SQLStatement.

Parameters

textstring

a SQL statement with embedded parameters in the form {$name} where name will be replaced by a corresponding key-value object entry.

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

valueSQLStatement

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

textstring

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

paramsobject
[initial]Array

An existing list of parameters. Defaults to empty list

Returns

Array

initial

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");
koru/promise-queue

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

callbackfunction

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);
koru/pubsub/main

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
// Step 1 - Register the subscription class LibrarySub extends Subscription { constructor(args) { super(args); const {shelf} = this.args; this.match(Book, (doc) => doc.shelf === self); } async connect() { // step 2 from below calls this this.lastSubscribed = await lookupLastSubscribed(this); // connect to server super.connect(); } reconnecting() { Book.query.forEach(Subscription.markForRemove); } } LibrarySub.pubName = 'Library';
// Step 1 - Register the publication class LibraryPub extends Publication { constructor(options) { super(options); } async init({shelf}) { // step 3 from below calls this Val.ensureString(shelf); // Send relavent documents to client await Book.where({shelf}) .forEach((doc) => conn.added('Book', doc.attributes)); // Listen for relavent document changes this.listeners = [ Book.onChange(Publication.buildUpdate)]; } stop() { super.stop(); for (const listener of this.listeners) listener.stop(); } } LibraryPub.pubName = 'Library'; // Server listens from Library Subscriptions // Can also use: LibraryPub.module = module
// Step 2 - subscribe const sub = LibrarySub.subscribe({shelf: 'mathematics'}, (error) => { // Step 4 - server has sent responses if (error) { // handle error } else { assert(Book.where('shelf', 'mathematics').exists()); } });
// Step 3 receive connect request from client // with data: 's123', 1, 'Library', args, lastSubscribed; const sub = new LibraryPub({id: 's123', conn, lastSubscribed}); await sub.init(...args); // after init, server informs client of success and updates lastSubscribed // ['s123', 1, 200, new Date(thirtyDaysAgo)]
// Listening for book changes const bookHandle = Book.onChange((dc) => { // Step 6 - receive book change if (dc.doc.name === "Euclid's Elements") { dc.doc.readBook(); } });
// Step 5 - another client adds a book await Book.create({shelf: 'mathematics', name: "Euclid's Elements"}); // book is sent to subscribed clients
// Step 7 - send close to server sub.stop();
// Step 8 close recieved from client sub.stop();

The example above is deficient in several ways:

  1. It does not send updated related only to the shelf requested
  2. It does not coordinate with other publications which may be publishing the same documents.
  3. It does not use lastSubscribed to reduce server-to-client traffic.

It is non-trivial to fix these deficiencies; however:

  1. {../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.
  2. Union can be used to combine subscriptions to reduce traffic.
koru/pubsub/all-pub

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

requireUserIdboolean

require user to be signed in

Methods

AllPub.excludeModel(...names)

Exclude Models from being published. UserLogin is always excluded. This will clear includeModel

Parameters

namesstring

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

namesstring

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);
koru/pubsub/all-sub

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

namesstring

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

namesstring

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();
koru/pubsub/model-match

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

docBaseModel

the document to test if matches a matcher

Returns

boolean

true, false, or undefined.

  1. true if the doc matches any matcher. No further matchers are tested.
  2. false if at least one matcher returns false and no matcher returns true. false indicates that this document should be removed both from memory and client persistent storage.
  3. undefined if all matchers return undefined or no matchers are registered. undefined indicates that this document should be removed from memory but not from client persistent storage. Incoming changes from a server which match undefined will notify model observers with a DocChange.delete flag of "stopped".

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

modelNamestring

or model

comparatorfunction

The comparator(doc){} should take one argument docto test if matches. Return true to match the document, false to explicitly not match the document and undefined if no opinion about the document. See has

Returns

object

handle with delete method to unregister the comparator.

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);
koru/pubsub/preload-subscription

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()

Override this method to return a QueryIDB like instance or undefined (if none). The idb instance is passed to the preload method. If no idb is returned preload is not called.

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 "skipServer" the server will be called with the subscription request.

If return is "waitServer" Subscription#onConnect observers will be called after server request completes; otherwise observers are called immediately.

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

errnull / koru.Error

null for success else error from server

idbobject / 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]);
koru/pubsub/publication

Publication

A Publication is a abstract interface for handling subscriptions.

See also Subscription

Properties

lastSubscribedIntervalnumber

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.

See discreteLastSubscribed

lastSubscribedMaximumAgenumber

Any subscription with a lastSubscribed older than this is aborted with error 400, reason {lastSubscribed: "too_old"}. Specified in milliseconds. Defaults to 180 days.

Client subscriptions should not send a lastSubscribed value if it is not later than this value, by at least 10000. It should also mark any current documents as simulated.

#lastSubscribednumber

The subscriptions last successful subscription time in ms

#userIdstring

userId is a short cut to this.conn.userId. See ServerConnection

Methods

constructor({id, conn, lastSubscribed})

Build an incomming subscription.

Parameters

idstring

The id of the subscription

connServerConnection

The connection of the subscription

lastSubscribednumber

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.

Parameters

timenumber

the time (in ms) to convert

Returns

number

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.

See Subscription#postMessage

Parameters

messageobject

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

messageany-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.

Parameters

newUIDstring / undefined
oldUIDundefined / string

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);
koru/pubsub/subscription

Subscription

A Subscription is a abstract interface for subscribing to publications.

See also Publication

Properties

lastSubscribedMaximumAgenumber

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

docBaseModel

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

argsany-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

Parameters

docobject

the document to test if matches a matcher

Returns

boolean

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

modelNamesstring

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.

Parameters

modelNameBaseModel / string

the name of the model or the model itself

testfunction

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

callbackfunction

called with an error argument.

Returns

object

handler with a stop method to cancel the onConnect.

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

messageany-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.

See Publication#onMessage

Parameters

messageany-type

the message to send

callbackfunction

a function with the arguments error, result. This function will be called when the message has finished or; if connection lost of not yet started, will be called when the subscription is active with no result (via the onConnect queue).

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

unmatchfunction

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.

Parameters

modelNameBaseModel / string

the name of the model or the model itself

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.

Parameters

newUIDstring / undefined
oldUIDundefined / string

Example

const Subscription = require("koru/pubsub/subscription");
const subscription = new Subscription();
subscription.userIdChanged("uid123", undefined);
subscription.userIdChanged("uid456", "uid123");
subscription.userIdChanged(undefined, "uid456");
koru/pubsub/union

Union

Union is an interface used to combine server subscriptions to minimise work on the server.

Properties

#handlesArray

An array to store any handles that should be stopped when the union is empty. A handle is anything that has a stop method.

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

subPublication

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

subPublication
tokenUnion

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.

Parameters

dcDocChange

Returns

Array

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)

Parameters

dcDocChange

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 addDoc, remDoc and push:

  • addDoc is a function to call with a doc to be added to the subscribers.
  • chgDoc is a function to call with a doc and change object (listing the fields that have changed) to be changed to the subscribers.
  • remDoc a function to call with a doc (and optional flag) to be removed from the subscribers. The flag is sent to the client as a DocChange#flag which defaults to "serverUpdate". A Useful value is "stopped" which a client persistence manager can used to decide to not remove the persitent document.
  • push is for adding any message to the batch. The message format is: [type, data] (see ServerConnection.buildUpdate for examples).
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

subPublication

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.

See ServerConnection#sendEncoded

Parameters

msgstring

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

msgstring

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]);
koru/random

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

globalRandom

a instance of a CSPRNG

Methods

constructor(...tokens)

Create a new PRNG.

Aliases

create a deprecated alternative static method.

Parameters

[tokens]number / string

a list of tokens to seed a PRNG or empty for a CSPRNG.

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)

Like id but generate a hexString instead.

Parameters

valuenumber

Returns

string

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.

Parameters

digitsnumber

the number of digits to generate.

Returns

string

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.

Parameters

Returns

string

token of util.idLen characters from the set [0-9A-Za-z]

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");
koru/server-pages/main

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 the book.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

WebServerWebServer

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();
koru/server-pages/base-controller

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

viewTemplate

Templete for index action. All other action templates are children of this template.

Example request

GET /foo
view.EditTemplate

Templete for edit action.

Example request

GET /foo/:id/edit
view.NewTemplate

Templete for new action.

Example request

GET /foo/new
view.ShowTemplate

Templete for show action.

Example request

GET /foo/:id
#body

The body of the request. If the content type is: application/json or application/x-www-form-urlencoded then the body is converted to an object map otherwise the raw string is returned.

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

callbackFunction

call this function to run the this.action

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;

Parameters

codenumber

the statusCode to send.

[message]string

the response body to send.

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.

Parameters

urlstring

the location to redirect to.

[code]number

the statusCode to send.

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

contentElement

usually rendered html but can be whatever the layout requires.

[layout]object

The layout View to wrap the content. defaults to ServerPages.defaultLayout or a very basic html page.

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

htmlElement

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'})); } }
koru/server-pages/inline-script

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.

Parameters

dirstring

The local directory used by require to normalize the id.

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

idstring

the id to reference the object with a require

objectstring

the object to add (will be converted to string using #toString)

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.

Parameters

idstring

Returns

number

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'");
koru/session/main

Session

The main or active session for client server communication. See webSocketSenderFactory

Properties

_idstring

The Session _id: "default"

Methods

Session.defineRpc(name, func=util.voidFunc)

Define a remote proceedure call

Parameters

namestring
funcfunction

Returns

base

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

Parameters

namestring
funcfunction

Returns

base

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

{push, encode}. Use push to append a message to the batch. Use encode to return the encoded batch.

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']]]);
koru/session/client-rpc-base

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

Parameters

msgIdstring

Returns

boolean

Example

const init = require("koru/session/client-rpc-base");
const init = new init();
init.cancelRpc("1rid1");
// returns true
init.cancelRpc("1rid1");
init.cancelRpc("2rid1");
// returns true

init#checkMsgId(msgId)

Ensure than next msgId will be greater than this one

Parameters

msgIdstring

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

valueobject

Example

const init = require("koru/session/client-rpc-base");
const init = new init();
init.replaceRpcQueue({push: EMPTY_FUNC});
koru/session/conn-th-server

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

connServerConnection
typestring
expArray

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

connServerConnection
callCall

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

sessIdstring
[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

connServerConnection

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);
koru/session/reverse-rpc-receiver

ReverseRpcReceiver

Methods

constructor(session, cmd='F')

Create an reverse rpc handler

Parameters

sessionobject

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

namestring
funcfunction

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);
koru/session/reverse-rpc-sender

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

sessionobject

the session that initiates the callbacks

cmdstring

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

namestring

of the method to call

argsstring

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.

Parameters

connundefined / object

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']);
koru/session/rpc-idb-queue

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

qdbobject

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

sessionobject

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
koru/session/rpc-queue

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

sessionobject

Example

const RPCQueue = require("koru/session/rpc-queue");
const rPCQueue = new RPCQueue();
rPCQueue.resend({sendBinary: sendBinary, checkMsgId: checkMsgId});
koru/session/server-connection

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

dcDocChange

for the document that has been updated

Returns

Array

an update command. Where

index 0 index 1
A `[modelName, doc._id, doc.attributes]
C `[modelName, doc._id, dc.changes]
R `[modelName, doc._id]

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

docobject

the document to be filtered.

filterobject

an Object who properties will override the document.

Returns

object

an object suitable for sending to client; namely it has an _id, a constructor model, and a attributes field.

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

Parameters

namestring
attrsobject
[filter]object

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

typestring

the one character type for the message. See base#provide.

dataArray

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.

Parameters

namestring
idstring
attrsobject
[filter]object

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

Parameters

namestring
idstring
[flag]string

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

typestring

the one character type for the message. See base#provide.

datastring

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

typestring

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

msgstring

Example

const ServerConnection = require("koru/session/server-connection");
conn.sendEncoded('myMessage'); assert.calledWith(conn.ws.send, 'myMessage', {binary: true});
koru/session/web-socket-sender-factory

webSocketSenderFactory

Build WebSocket clients (senders).

Methods

webSocketSenderFactory( _session, sessState, execWrapper=koru.fiberConnWrapper, base=_session)

Parameters

_sessionobject
sessStateobject
[execWrapper]function
[base]object

Returns

object

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');
koru/stacktrace

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

errorAssertionError

the Error to elide from

countnumber

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.

See util.extractError

Parameters

errorAssertionError

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

errorError

the Error who's frame is to be replaced.

replacementErrorError

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();
koru/symbols

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

koru/test/api

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 Class

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

commentstring

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

funcfunction

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 func

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

objectobject

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'; //]

Parameters

bodyfunction / string

Returns

object / primitive

the result of running body

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

subjectobject / 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 string). Alias 'intro'

[options.abstract]function

introduction to the subject. If abstract is a function then the initial doc comment is used.

[options.initExample]string

code that can be used to initialize subject

[options.initInstExample]string

code that can be used to initialize an instance of subject

Returns

API

an API instance for the given subject. Subsequent API calls should be made directly on this API instance and not the API Class itself

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

Parameters

methodNamestring
[options]object

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 subject

[initInstExample]string

code that can be used to initialize an instance of subject

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)

Parameters

namestring
[options]object / function

Returns

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 module.exports.prototype

[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

namestring

the property to document

[options]object
[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); });
koru/test/core

Core

The heart of the test framework.

Properties

AssertionErrorAssertionError

the AssertionError class

__elidePointAssertionError

The current elidePoint in effect (if any) is an AssertionError used to replace the stack trace of the actual error. Access can be useful to save and restore in custom assertion methods.

matchfunction

A clone of the match framework. This is conventionally assigned to the constant m because of its widespread use.

testTest

The currently running test

Methods

assert.elide(body, adjust=0)

Elide stack starting from caller

Parameters

bodyfunction

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

Parameters

[message]string

the error message

[elidePoint]number

the number of stack frames to elide

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

truthboolean

!! truth === true otherwise an AssertionError is thrown.

[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

actualnull / object / number / Array
expectednull / undefined / string / object / number / Array
[hint]object
[hintField]string
[maxLevel]

Returns

boolean

Example

const Core = require("koru/test/core");
Core.deepEqual(null, null);
// returns true
Core.deepEqual(null, undefined);
// returns true
Core.deepEqual(null, "");
// returns false
Core.deepEqual({}, {});
// returns true
Core.deepEqual(0, 0);
// returns false
Core.deepEqual({a: 0}, {a: 0});
// returns false
Core.deepEqual({a: null}, {b: null}, {}, "keyCheck");
// returns false
Core.deepEqual([1, 2, null], [1, match((v) => v % 2 === 0), match.any]);
// returns true
Core.deepEqual([1, 1], [1, match((v) => v % 2 === 0)]);
// returns false
Core.deepEqual([2, 2], [1, match((v) => v % 2 === 0)]);
// returns false
Core.deepEqual({a: 1, b: {c: 1, d: [1, {e: [false]}]}}, {a: 1, b: {c: 1, d: [1, {e: [false]}]}});
// returns true
Core.deepEqual({a: 1, b: {c: 1, d: [1, {e: [false]}]}}, {a: 1, b: {c: 1, d: [1, {e: [true]}]}});
// returns false
Core.deepEqual({a: 1, b: {c: 0, d: [1, {e: [false]}]}}, {a: 1, b: {c: 0, d: [1, {e: [false]}]}});
// returns false
Core.deepEqual({a: 1, b: {c: 1, d: [1, {e: [false]}]}}, {a: 1, b: {c: 1, d: [1, {e: [false], f: undefined}]}});
// returns false
Core.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

truthboolean

!! truth === false otherwise an AssertionError is thrown.

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');
koru/test/core::AssertionError

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

messagestring

the message for the error

[elidePoint]number / AssertionError

if a number the number of stack frames to elide otherwise will show elidePoint's normalized stack instead. See util.extractError and Stacktrace

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();
koru/test/mock-promise

MockPromise

Synchronous replacement for native promises. Delays running promises until _poll called rather than waiting for native event loop.

See Promise for API

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);
koru/test/stubber

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.

Parameters

objectobject
propstring
replacementfunction
[restore]

Returns

function

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.

Parameters

funcfunction / Stub

Returns

boolean

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.

Parameters

objectobject
propertystring

Returns

Stub

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.

Parameters

[object]object
[property]string / symbol
[repFunc]function

Returns

Stub

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);
koru/test/stubber::Stub

Stub

Stub and Spy methods.

Properties

#callCountnumber

How many times the stub has been called

#calledboolean

Has the stub been called at least once

#calledOnceboolean

Has the stub been called exactly once

#calledThriceboolean

Has the stub been called exactly 3 times

#calledTwiceboolean

Has the stub been called exactly twice

#firstCallCall

The first call to the stub

#lastCallCall

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.

Parameters

beforeStub

a stub to test against

Returns

boolean

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.

Parameters

afterStub

a stub to test against

Returns

boolean

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

argsnumber

the args to test were passed; extra args in the call will be ignored.

Returns

boolean

true when the stub was called with 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.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.

Parameters

argsnumber

the args to test were passed.

Returns

boolean

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.

Parameters

indexnumber

the nth call to stub; 0 is first call.

Returns

Call

the call object.

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.

Parameters

callbackfunction

the function to run

Returns

Stub

the stub

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

countnumber

the countth call to refine.

Returns

Stub

the new stub controlling the countth call.

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

selectorfunction

the function given a call instance that returns true on the call to yield. It can return true more than once in which case the last call returned true is selected.

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.

Parameters

argany-type

the value to return

Returns

Stub

the stub

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

argsany-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 spy
stub.withArgs("bar");
// returns spy
stub.withArgs(match.number, match.string);
// returns spy

Stub#yield(...args)

Call the first callback of the first call to stub.

Parameters

argsany-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.

Parameters

argsany-type

the arguments to pass to the callback.

Returns

Stub

the stub.

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

matchfunction
argsany-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.

Parameters

argsany-type

will be passed to the callback.

Returns

Stub

the stub

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]);
koru/test/stubber::Stub::Call

Call

Details of a Stub call.

Properties

#argsArray

the args of the call

#globalCountnumber

The index in the sequence of all stub calls since the start of the program

#thisValueobject

the this value of the call

Methods

Call#calledWith(...args)

Was this call called with the given args.

Parameters

argsnumber

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.

Parameters

argsany-type

will be passed to the callback.

Returns

number

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);
koru/test/test-case

TestCase

A group of tests. Most methods are for internal purposes and are not documented here.

See TestHelper

Properties

#moduleIdstring

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).

Parameters

[name]string

Returns

string

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
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.

Properties

#moduleIdstring

the id of the module this test is defined in

#namestring

the full name of the test

#tcTestCase

the test-case containing this test

koru/test-helper

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

matchfunction

A copy of the match module for using with tests. This is normally assigned to m because it is use so often. The reason it is a copy is so that the main match module can be stubbed without interfering with test asserts

testTest

The current test that is running

utilutil-base

A convenience reference to util

Methods

TestHelper.after(callback)

Run callback after the test/test-case has completed.

Aliases

onEnd [deprecated]

Parameters

callbackobject / function

the function to run or an object with a stop function to run.

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

objectobject
propstring
newValueobject

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)

Create a TestCase and set the exports for module to the testCase.

See example above

Parameters

moduleobject

the module for the test file.

bodyfunction

A function that will add tests. It is passed an object with the following properties:

  • before(body) - a function to run before all tests in the test case.

  • after(body) - a function to run after all tests in the test case.

  • beforeEach(body) - a function to run before each test in the test case.

  • afterEach(body) - a function to run after each test in the test case.

  • group(name, body) - adds a sub TestCase named name. body is again called with this list of properties: (before, after, beforeEach, afterEach, group, test).

  • test(name, body) - adds a Test named name. body is called when the test is run.

    If the body has a done argument then the test will be asynchronous and done should be called with no argument on success and with a error on failure.

    body may also be an async function

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}
koru/test-helper::MockModule

MockModule

For when you want to mock a module

Methods

constructor(id, exports={})

Create a MockModule.

Parameters

idstring
exportsobject

Example

const TestHelper = require("koru/test-helper");
const myMod = new MockModule("my-id", {my: 'content'}); assert.same(myMod.id, 'my-id'); assert.equals(myMod.exports, {my: 'content'});
koru/ui/app

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');
koru/ui/auto-list

AutoList

Automatically manage a list of Elements matching a Query. The list observers changes in the query model and updates the list accordingly.

Properties

#limitnumber

A limit of n can be given to only display the first (ordered) n entries.

When visible entries are removed non-visible entries are added to keep list length at n when n or more rows still match the query. Defaults to Infinity

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

templateTemplate

to render each row

containerElement / Node

to render into. Can be a start Comment node with symbol endMarker$ (see Symbols) pointing to end Comment in which case the rows will be rendered between the comments.

[query]Query

A Query or at least has methods compare and forEach. The method onChange will be used to auto-update the list if present.

[limit]

maximum number of elements to show. (see limit property)

[compare]

function to order data. Defaults to query.compare or if no query then order items are added

[compareKeys]

Array of keys used for ordering data. Defaults to compare.compareKeys

[observeUpdates]

The list can be monitored for changes by passing an observeUpdates function which is called for each change with the arguments (list, doc, action) where:

  • list is this AutoList
  • doc is the document being added, changed, or removed
  • action is added, changed or removed
[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

queryQuery / object
[compare]function
[compareKeys]
[limit]
[updateAllTags]boolean

call updateAllTags on each element that is already rendered. Defaults to false

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

docBaseModel / null / object
[force]string

if set to "render" then raise the limit in order for node to be visible

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

nodeobject
[force]string

if set to "render" then raise the limit in order for node to be visible

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 null
autoList.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

docBaseModel

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

docBaseModel

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.

Parameters

docobject

the entry to update

[action]string

if value is "remove" then remove entry

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'); });
koru/ui/list-selector

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

ulElement

the parent element for list items (usally a <ul class="ui-ul"> element)

[ctx]Ctx

The events are detached when this ctx is destroyed.

[keydownElm]HTMLDocument

the element to listen for keydown events from.

onClickfunction

called with the current element and the causing event when clicked or enter pressed on a selectable element.

[onHover]function

called with the target and the causing event when pointerover selectable element.

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

eventobject

the keydown event to process

ulElement

the list contains li items to select

[selected]HTMLCollection

used to determine current selected

[onClick]function

callback for when Enter is pressed with a selected item.

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);
koru/ui/ripple

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');
koru/ui/route

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.

Parameters

pageTemplate
pageRouteobject

Example

const Route = require("koru/ui/route");
Route.gotoPage(Template(Test.AdminProfile), {append: "my/id"});

Route.pathToPage(path, pageRoute)

Parameters

pathstring
pageRouteobject

Returns

object

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.

Parameters

pageTemplate
pageRouteobject

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

titlestring

Example

const Route = require("koru/ui/route");
Route.setTitle("my title");
koru/ui/svg-icons

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

namestring

the name of the icon to use (See use)

attributesobject

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

idstring

the id of the icon. It will be prefixed with "icon-".

symbolobject

An SVGSymbolElement or an object to pass to Dom.html as the contents of a symbol. id will be set on the symbol.

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

iconstring

the name of an icon to use. the use element xlink:href is set to "#icon-"+icon.

[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.

Parameters

iconstring
elmobject

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

Parameters

useElmElement
iconstring

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

Parameters

iconstring
[attrs]

Returns

Element

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');
koru/ui/time

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}}

Parameters

datenumber / Date

the date to print relatively

Returns

string

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}}

Parameters

timenumber

Returns

string

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);
koru/uint8-array-builder

Uint8ArrayBuilder

Build an Uint8Array with dynamic sizing.

Properties

#dataViewobject

Use a dataView over the interal ArrayBuffer

#lengthnumber

The length of #subarray. It can only be reduced; increasing throws an error.

#subarrayfunction

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

dataArray / 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.

Parameters

indexnumber

Returns

number

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

bytesnumber

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.

Parameters

indexnumber
bytenumber

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

dataUint8Array
[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);
koru/util

util

The util module provides commonly performed utility functions.

Properties

DAYnumber

The number of milliseconds in a day.

EMAIL_RERegExp

RegExp to match email addresses

idLennumber
  1. The length of an _id in characters.
threadobject

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

listArray

the list to add item to

itemany-type

the item to add to list

Returns

undefined / number

Returns undefined if item is added to list. Returns the index of item if list already contains item.

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 list elements and values of true.

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

objundefined / object
optionsobject

the options to select from.

namestring

the name of the option to set.

[def]function

the default to use if name not in options. If def is a function it will be called to get the value.

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

asyncIteratorFunction

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.

Parameters

listArray
comparefunction
[start]number
[lower]
[upper]

Returns

number

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)

Parameters

anumber
bnumber

Returns

number

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 list1 that are not also elements of list2

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:

  1. the index of the first non-matching character in oldstr and newstr
  2. the length of the segment of oldstr that doesn't match newstr
  3. the length of the segment of newstr that doesn't match oldstr

Parameters

oldstrstring

a string

newstrstring

another string

Returns

undefined / Array

undefined if oldstr and newstr are the same, otherwise an array of three numbers:

  1. the index of the first non-matching character in oldstr and newstr
  2. the length of the segment of oldstr that doesn't match newstr
  3. the length of the segment of newstr that doesn't match oldstr

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

oldstrstring

a string

newstrstring

another string

Returns

number

0 if oldstr and newstr are the same, otherwise the length of the segment, in the longer of oldstr and newstr, that doesn't match the other string

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

errAssertionError / 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

objobject

the object from which to collect properties

keysArray / object

a collection of keys or properties whose names identify which properties to collect from obj

Returns

object

an object made up of the properties in obj whose keys are named in keys

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

objobject

the object from which to collect properties

keysobject

a collection of properties whose names identify which properties not to collect from obj

Returns

object

an object made up of the properties in obj whose keys are not named in keys

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 obj, or undefined if obj is empty

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

listArray / null

a list

visitorfunction

a function taking two arguments: the value of the current element in list, and the index of that current element

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

objobject

an object

keyMapobject

a set of key-value pairs

Returns

boolean

true if obj has only keys also in keyMap, otherwise false

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

objectPromise(number) / Array
trueCallbackfunction
[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

listArray

the list to search

valueRegExp

the regular expression to search fieldName for a match

fieldNamestring

the property name to search for in each item in list

Returns

number

the index of the first item in list that has a property fieldName that contains a match for the regular expression value, or -1 if list does not contain such an item

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

textstring
indexnumber

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.

Parameters

oobject / Array
[count]
[len]

Returns

string

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

list1Array

a list

list2Array

a second list

Returns

boolean

true if list1 and list2 have any element in common, otherwise false

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.

Parameters

[obj]object

an object

Returns

boolean

true if obj is empty, otherwise false

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

objectPromise(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

listArray

the list to search

itemany-type

the item to search list for

Returns

number

the index of item if item is in list, otherwise -1

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

objobject

the object to search

regexRegExp

the regular expression to match

Returns

Array / null

the result array from regex.exec() if a match is found, or null if not

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

objnull / object

the object to search

strstring

the string to search for

Returns

boolean

true if a key is found that starts with str, otherwise false

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

destobject

an object to modify

[source]object

the properties to be added or modified

Returns

object

dest modified: each enumerable property in source has been added to dest, or where a property of that name already existed in dest, the property in dest has been replaced with the property from source

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

objobject

an object to modify

propertiesobject

properties to be added to or modified in obj, unless they are in exclude

excludeobject

properties which are excluded from being added to or modified in obj

Returns

object

the modified obj.

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

objobject

an object to modify

propertiesobject

properties to be added to or modified in obj if they are named in include

includeobject / array

properties, or a list of property names, whose names identify which properties from properties are to be added to or modified in obj

Returns

object

obj modified: each property in properties that is named in include has been added to obj, or where a property of that name already existed in obj, the property in obj has been replaced with the property from properties

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

destobject

an object to modify

sourceobject

the properties to be added or modified

Returns

object

dest modified: each enumerable property in source has been added to dest, or where a property of that name already existed in dest, the property in dest has been replaced with the property from source, in each case with enumerable set to false

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

destobject

an object to modify

sourceobject

the properties to be added or modified

Returns

object

dest modified: each property in source, including non-enumerable properties, has been added to dest, or where a property of that name already existed in dest, the property in dest has been replaced with the property from source

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

fractionnumber

a fraction

Returns

string

a string comprised of the percent form of fraction, with the percent symbol, %

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.

Parameters

idstring

Returns

string

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

Parameters

sstring

Returns

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

listArray

the list from which to remove item

[item]any-type

the item to remove from list

Returns

any-type

the removed item, or undefined if list does not contain item

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

listArray / null

a list

visitorfunction

a function taking two arguments: the value of the current element in list, and the index of that current element

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

value converted to a number; the suffix '%' has been removed

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

value converted to a number; the suffix 'px' has been removed

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

objobject

the object from which to collect properties

includeKeysobject

a collection of properties whose names identify which objects to include in the first object returned

Returns

object

the include and exclude objects.

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 list1 and list2 that belong only to list1 or list2, not to both lists

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)

Parameters

valuestring

Returns

string

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

numbernumber

a number to be converted

dpnumber

the number of decimal places to display number to

[zeroFill]boolean

pad with zeros; false by default

Returns

string

number to dp decimal places, converted to a string, padded with zeros if zeroFill is true

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

arrayUint8Array

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

Parameters

aArray / string
bArray / string

Returns

Array

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

firstArray / 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 first and one instance of each of the unique elements in rest that are not also in first

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

mapobject

an object

Returns

Array

a list made up of the values of the enumerable properties of map

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

Parameters

objectobject

Returns

object

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

objectobject

an object to associate with _id

_idnumber

an id to associate with object

[key]symbol

defaults to Symbol.withId$

Returns

object

an associated object which has the given _id and a prototype of object. If an association for key is already attached to the object then it is used otherwise a new one will be created.

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);
koru/util-date

UtilDate

Utility date processing methods

Properties

defaultLangstring

The default locale language. On the browser it is navigator.language. On the server it is env LC_ALL LC_TIME or the default for Intl.DateTimeFormat().resolvedOptions().locale

Methods

UtilDate.atUTCDowHour(date, dow, hour)

Find the time for a UTC dow, hour (day of week and hour) after date

Parameters

datenumber

the date to start from

downumber

the desired UTC day of week. sun=0, mon=1, ... sat=6

hournumber

the desired UTC hour of day

Returns

Date

a date which is >= date and its getUTCDay() equals dow and getUTCHours equals hour

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

formatstring / object

If an object then uses Intl.DateTimeFormat

if a string can contain the following mnemonics:

  • D 1 or 2 digit day of month
  • DD 2 digit day of month
  • s 1 digit seconds in minute
  • ss 2 digit seconds in minute
  • MM 2 digit month of year
  • MMM short name for month of year
  • YYYY 4 digit year
  • YY 2 digit year
  • h 1 or 2 digit hour of day
  • hh 2 digit hour of day
  • m 1 or 2 digit minute of hour
  • mm 2 digit minute of hour
  • a am or pm
  • A AM or PM
langstring

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

date => computeString()

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

dateDate / number

format this date

formatstring / 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

dateStrstring

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 dateStr with the local timezone.

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

Parameters

deltanumber
[minTime]number
[lang]string

Returns

string

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

Parameters

dateDate

The date to shift

Returns

Date

with the local timezone

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

Parameters

dateDate

The date to shift

Returns

Date

with the UTC timezone

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

dateDate

to discretize

unitnumber

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.

Parameters

dateDate

to look for sunday from

Returns

Date

start of sunday in locale timezone.

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));
koru/web-server

WebServer

The default web-server created from WebServerFactory. IdleCheck is used to keep track of active requests.

Config

defaultPage

defaults to /index.html: used when no path is supplied in the url.

extras

any extra root level file mappings in {[topPath]: [filename, root], ...} format

host

listen on the specified address

indexcss

the file to serve for /index.css; defaults to index.css

indexhtml

the file to serve for /index.html; defaults to index.html

indexjs

the file to serve for /index.js or require.js; defaults to amd-loader

indexjsmap

the file to serve for /index.js.map; defaults to index.js.map

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);
koru/web-server-factory

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

hoststring
portstring
rootstring

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);