├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .jsdoc.json ├── .npmignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── array.js ├── array.test.js ├── bin ├── 0ecdsa-generate-keypair.js ├── 0serve.js ├── gendocs.js └── gentesthtml.js ├── binary.js ├── binary.test.js ├── broadcastchannel.js ├── broadcastchannel.test.js ├── buffer.js ├── buffer.test.js ├── cache.js ├── cache.test.js ├── component.js ├── conditions.js ├── crypto.test.js ├── crypto ├── aes-gcm.js ├── common.js ├── ecdsa.js ├── jwt.js └── rsa-oaep.js ├── decoding.js ├── deno.json ├── deno.lock ├── diff.js ├── diff.test.js ├── dom.js ├── encoding.js ├── encoding.test.js ├── environment.js ├── error.js ├── eventloop.js ├── eventloop.test.js ├── function.js ├── function.test.js ├── hash ├── rabin-gf2-polynomial.js ├── rabin-uncached.js ├── rabin.js ├── rabin.test.js ├── sha256.js ├── sha256.node.js └── sha256.test.js ├── index.js ├── indexeddb.js ├── indexeddb.test.js ├── indexeddbV2.js ├── indexeddbV2.test.js ├── isomorphic.js ├── iterator.js ├── json.js ├── list.js ├── list.test.js ├── logging.common.js ├── logging.js ├── logging.node.js ├── logging.test.js ├── map.js ├── map.test.js ├── math.js ├── math.test.js ├── metric.js ├── metric.test.js ├── mutex.js ├── number.js ├── number.test.js ├── object.js ├── object.test.js ├── observable.js ├── observable.test.js ├── package-lock.json ├── package.json ├── pair.js ├── pair.test.js ├── performance.js ├── performance.node.js ├── pledge.js ├── pledge.test.js ├── prng.js ├── prng.test.js ├── prng ├── Mt19937.js ├── Xoroshiro128plus.js └── Xorshift32.js ├── promise.js ├── promise.test.js ├── queue.js ├── queue.test.js ├── random.js ├── random.test.js ├── rollup.config.js ├── set.js ├── set.test.js ├── sort.js ├── sort.test.js ├── statistics.js ├── statistics.test.js ├── storage.js ├── storage.test.js ├── string.js ├── string.test.js ├── symbol.js ├── symbol.test.js ├── test.html ├── test.js ├── testing.js ├── testing.test.js ├── time.js ├── time.test.js ├── traits.js ├── traits.test.js ├── tree.js ├── tree.test.js ├── tsconfig.json ├── url.js ├── url.test.js ├── webcrypto.deno.js ├── webcrypto.js ├── webcrypto.node.js ├── webcrypto.react-native.js └── websocket.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Testing Lib0 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .nyc_output 3 | coverage 4 | node_modules 5 | *.d.ts 6 | *.d.ts.map 7 | */*.d.ts 8 | */*.d.ts.map 9 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "plugins/markdown", 4 | "jsdoc-plugin-typescript" 5 | ], 6 | "recurseDepth": 10, 7 | "source": { 8 | "excludePattern": "/\\.test\\.js/" 9 | }, 10 | "sourceType": "module", 11 | "tags": { 12 | "allowUnknownTags": true, 13 | "dictionaries": ["jsdoc", "closure"] 14 | }, 15 | "typescript": { 16 | "moduleRoot": "./" 17 | } 18 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.test.js 2 | dist/test.* 3 | tsconfig.json 4 | rollup.config.js 5 | .nyc_output 6 | .travis.yml 7 | .git 8 | node_modules 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Gen Docs", 11 | "program": "${workspaceFolder}/bin/gendocs.js", 12 | "skipFiles": [ 13 | "/**" 14 | ], 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Kevin Jahns . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /array.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility module to work with Arrays. 3 | * 4 | * @module array 5 | */ 6 | 7 | import * as set from './set.js' 8 | 9 | /** 10 | * Return the last element of an array. The element must exist 11 | * 12 | * @template L 13 | * @param {ArrayLike} arr 14 | * @return {L} 15 | */ 16 | export const last = arr => arr[arr.length - 1] 17 | 18 | /** 19 | * @template C 20 | * @return {Array} 21 | */ 22 | export const create = () => /** @type {Array} */ ([]) 23 | 24 | /** 25 | * @template D 26 | * @param {Array} a 27 | * @return {Array} 28 | */ 29 | export const copy = a => /** @type {Array} */ (a.slice()) 30 | 31 | /** 32 | * Append elements from src to dest 33 | * 34 | * @template M 35 | * @param {Array} dest 36 | * @param {Array} src 37 | */ 38 | export const appendTo = (dest, src) => { 39 | for (let i = 0; i < src.length; i++) { 40 | dest.push(src[i]) 41 | } 42 | } 43 | 44 | /** 45 | * Transforms something array-like to an actual Array. 46 | * 47 | * @function 48 | * @template T 49 | * @param {ArrayLike|Iterable} arraylike 50 | * @return {T} 51 | */ 52 | export const from = Array.from 53 | 54 | /** 55 | * True iff condition holds on every element in the Array. 56 | * 57 | * @function 58 | * @template ITEM 59 | * @template {ArrayLike} ARR 60 | * 61 | * @param {ARR} arr 62 | * @param {function(ITEM, number, ARR):boolean} f 63 | * @return {boolean} 64 | */ 65 | export const every = (arr, f) => { 66 | for (let i = 0; i < arr.length; i++) { 67 | if (!f(arr[i], i, arr)) { 68 | return false 69 | } 70 | } 71 | return true 72 | } 73 | 74 | /** 75 | * True iff condition holds on some element in the Array. 76 | * 77 | * @function 78 | * @template S 79 | * @template {ArrayLike} ARR 80 | * @param {ARR} arr 81 | * @param {function(S, number, ARR):boolean} f 82 | * @return {boolean} 83 | */ 84 | export const some = (arr, f) => { 85 | for (let i = 0; i < arr.length; i++) { 86 | if (f(arr[i], i, arr)) { 87 | return true 88 | } 89 | } 90 | return false 91 | } 92 | 93 | /** 94 | * @template ELEM 95 | * 96 | * @param {ArrayLike} a 97 | * @param {ArrayLike} b 98 | * @return {boolean} 99 | */ 100 | export const equalFlat = (a, b) => a.length === b.length && every(a, (item, index) => item === b[index]) 101 | 102 | /** 103 | * @template ELEM 104 | * @param {Array>} arr 105 | * @return {Array} 106 | */ 107 | export const flatten = arr => fold(arr, /** @type {Array} */ ([]), (acc, val) => acc.concat(val)) 108 | 109 | /** 110 | * @template T 111 | * @param {number} len 112 | * @param {function(number, Array):T} f 113 | * @return {Array} 114 | */ 115 | export const unfold = (len, f) => { 116 | const array = new Array(len) 117 | for (let i = 0; i < len; i++) { 118 | array[i] = f(i, array) 119 | } 120 | return array 121 | } 122 | 123 | /** 124 | * @template T 125 | * @template RESULT 126 | * @param {Array} arr 127 | * @param {RESULT} seed 128 | * @param {function(RESULT, T, number):RESULT} folder 129 | */ 130 | export const fold = (arr, seed, folder) => arr.reduce(folder, seed) 131 | 132 | export const isArray = Array.isArray 133 | 134 | /** 135 | * @template T 136 | * @param {Array} arr 137 | * @return {Array} 138 | */ 139 | export const unique = arr => from(set.from(arr)) 140 | 141 | /** 142 | * @template T 143 | * @template M 144 | * @param {ArrayLike} arr 145 | * @param {function(T):M} mapper 146 | * @return {Array} 147 | */ 148 | export const uniqueBy = (arr, mapper) => { 149 | /** 150 | * @type {Set} 151 | */ 152 | const happened = set.create() 153 | /** 154 | * @type {Array} 155 | */ 156 | const result = [] 157 | for (let i = 0; i < arr.length; i++) { 158 | const el = arr[i] 159 | const mapped = mapper(el) 160 | if (!happened.has(mapped)) { 161 | happened.add(mapped) 162 | result.push(el) 163 | } 164 | } 165 | return result 166 | } 167 | 168 | /** 169 | * @template {ArrayLike} ARR 170 | * @template {function(ARR extends ArrayLike ? T : never, number, ARR):any} MAPPER 171 | * @param {ARR} arr 172 | * @param {MAPPER} mapper 173 | * @return {Array} 174 | */ 175 | export const map = (arr, mapper) => { 176 | /** 177 | * @type {Array} 178 | */ 179 | const res = Array(arr.length) 180 | for (let i = 0; i < arr.length; i++) { 181 | res[i] = mapper(/** @type {any} */ (arr[i]), i, /** @type {any} */ (arr)) 182 | } 183 | return /** @type {any} */ (res) 184 | } 185 | 186 | /** 187 | * This function bubble-sorts a single item to the correct position. The sort happens in-place and 188 | * might be useful to ensure that a single item is at the correct position in an otherwise sorted 189 | * array. 190 | * 191 | * @example 192 | * const arr = [3, 2, 5] 193 | * arr.sort((a, b) => a - b) 194 | * arr // => [2, 3, 5] 195 | * arr.splice(1, 0, 7) 196 | * array.bubbleSortItem(arr, 1, (a, b) => a - b) 197 | * arr // => [2, 3, 5, 7] 198 | * 199 | * @template T 200 | * @param {Array} arr 201 | * @param {number} i 202 | * @param {(a:T,b:T) => number} compareFn 203 | */ 204 | export const bubblesortItem = (arr, i, compareFn) => { 205 | const n = arr[i] 206 | let j = i 207 | // try to sort to the right 208 | while (j + 1 < arr.length && compareFn(n, arr[j + 1]) > 0) { 209 | arr[j] = arr[j + 1] 210 | arr[++j] = n 211 | } 212 | if (i === j && j > 0) { // no change yet 213 | // sort to the left 214 | while (j > 0 && compareFn(arr[j - 1], n) > 0) { 215 | arr[j] = arr[j - 1] 216 | arr[--j] = n 217 | } 218 | } 219 | return j 220 | } 221 | -------------------------------------------------------------------------------- /array.test.js: -------------------------------------------------------------------------------- 1 | import * as array from './array.js' 2 | import * as t from './testing.js' 3 | import * as prng from './prng.js' 4 | 5 | /** 6 | * @param {t.TestCase} _tc 7 | */ 8 | export const testIsarrayPerformance = _tc => { 9 | const N = 100000 10 | /** 11 | * @type {Array} 12 | */ 13 | const objects = [] 14 | for (let i = 0; i < N; i++) { 15 | if (i % 2 === 0) { 16 | objects.push([i]) 17 | } else { 18 | objects.push({ i }) 19 | } 20 | } 21 | const timeConstructor = t.measureTime('constructor check', () => { 22 | let collectedArrays = 0 23 | objects.forEach(obj => { 24 | if (obj.constructor === Array) { 25 | collectedArrays++ 26 | } 27 | }) 28 | t.assert(collectedArrays === N / 2) 29 | }) 30 | const timeIsarray = t.measureTime('Array.isArray', () => { 31 | let collectedArrays = 0 32 | objects.forEach(obj => { 33 | if (array.isArray(obj)) { 34 | collectedArrays++ 35 | } 36 | }) 37 | t.assert(collectedArrays === N / 2) 38 | }) 39 | t.assert(timeIsarray < timeConstructor * 2, 'Expecting that isArray is not much worse than a constructor check') 40 | } 41 | 42 | /** 43 | * @param {t.TestCase} _tc 44 | */ 45 | export const testAppend = _tc => { 46 | const arr = [1, 2, 3] 47 | array.appendTo(arr, array.copy(arr)) 48 | t.compareArrays(arr, [1, 2, 3, 1, 2, 3]) 49 | } 50 | 51 | /** 52 | * @param {t.TestCase} _tc 53 | */ 54 | export const testBasic = _tc => { 55 | const arr = array.create() 56 | array.appendTo(arr, array.from([1])) 57 | t.assert(array.last(arr) === 1) 58 | } 59 | 60 | /** 61 | * @param {t.TestCase} _tc 62 | */ 63 | export const testflatten = _tc => { 64 | const arr = [[1, 2, 3], [4]] 65 | t.compareArrays(array.flatten(arr), [1, 2, 3, 4]) 66 | } 67 | 68 | /** 69 | * @param {t.TestCase} _tc 70 | */ 71 | export const testFolding = _tc => { 72 | /** 73 | * @param {number} n 74 | */ 75 | const testcase = n => { 76 | // We mess with the seed (+/-1) to catch some edge cases without changing the result 77 | const result = -1 + array.fold(array.unfold(n, i => i), 1, (accumulator, item, index) => { 78 | t.assert(accumulator === index + 1) 79 | t.assert(accumulator === item + 1) 80 | return accumulator + 1 81 | }) 82 | t.assert(result === n) 83 | } 84 | testcase(0) 85 | testcase(1) 86 | testcase(100) 87 | } 88 | 89 | /** 90 | * @param {t.TestCase} _tc 91 | */ 92 | export const testEvery = _tc => { 93 | const arr = [1, 2, 3] 94 | t.assert(array.every(arr, x => x <= 3)) 95 | t.assert(!array.every(arr, x => x < 3)) 96 | t.assert(array.some(arr, x => x === 2)) 97 | t.assert(!array.some(arr, x => x === 42)) 98 | } 99 | 100 | /** 101 | * @param {t.TestCase} _tc 102 | */ 103 | export const testIsArray = _tc => { 104 | t.assert(array.isArray([])) 105 | t.assert(array.isArray([1])) 106 | t.assert(array.isArray(Array.from(new Set([3])))) 107 | t.assert(!array.isArray(1)) 108 | t.assert(!array.isArray(0)) 109 | t.assert(!array.isArray('')) 110 | } 111 | 112 | /** 113 | * @param {t.TestCase} _tc 114 | */ 115 | export const testUnique = _tc => { 116 | t.compare([1, 2], array.unique([1, 2, 1, 2, 2, 1])) 117 | t.compare([], array.unique([])) 118 | t.compare([{ el: 1 }], array.uniqueBy([{ el: 1 }, { el: 1 }], o => o.el)) 119 | t.compare([], array.uniqueBy([], o => o)) 120 | } 121 | 122 | /** 123 | * @param {t.TestCase} tc 124 | */ 125 | export const testBubblesortItemEdgeCases = tc => { 126 | // does not throw.. 127 | array.bubblesortItem([1], 0, (a, b) => a - b) 128 | array.bubblesortItem([2, 1], 1, (a, b) => a - b) 129 | array.bubblesortItem([2, 1], 0, (a, b) => a - b) 130 | } 131 | 132 | /** 133 | * @param {t.TestCase} tc 134 | */ 135 | export const testRepeatBubblesortItem = tc => { 136 | const arr = Array.from(prng.uint8Array(tc.prng, 10)) 137 | arr.sort((a, b) => a - b) 138 | const newItem = prng.uint32(tc.prng, 0, 256) 139 | const pos = prng.uint32(tc.prng, 0, arr.length) 140 | arr.splice(pos, newItem) 141 | const arrCopySorted = arr.slice().sort((a, b) => a - b) 142 | array.bubblesortItem(arr, pos, (a, b) => a - b) 143 | t.compare(arr, arrCopySorted) 144 | } 145 | 146 | /** 147 | * @param {t.TestCase} tc 148 | */ 149 | export const testRepeatBubblesort = tc => { 150 | const arr = Array.from(prng.uint8Array(tc.prng, 10)) 151 | const arrCopySorted = arr.slice().sort((a, b) => a - b) 152 | for (let i = arr.length - 1; i >= 0; i--) { 153 | while (array.bubblesortItem(arr, i, (a, b) => a - b) !== i) { /* nop */ } 154 | } 155 | t.compare(arr, arrCopySorted) 156 | } 157 | -------------------------------------------------------------------------------- /bin/0ecdsa-generate-keypair.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as ecdsa from 'lib0/crypto/ecdsa' 3 | import * as json from 'lib0/json' 4 | import * as env from 'lib0/environment' 5 | 6 | const prefix = env.getConf('name') 7 | 8 | const keypair = await ecdsa.generateKeyPair({ extractable: true }) 9 | const privateJwk = json.stringify(await ecdsa.exportKeyJwk(keypair.privateKey)) 10 | const publicJwk = json.stringify(await ecdsa.exportKeyJwk(keypair.publicKey)) 11 | 12 | console.log(` 13 | ${prefix ? prefix.toUpperCase() + '_' : ''}PUBLIC_KEY=${publicJwk} 14 | ${prefix ? prefix.toUpperCase() + '_' : ''}PRIVATE_KEY=${privateJwk} 15 | `) 16 | -------------------------------------------------------------------------------- /bin/0serve.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as http from 'http' 3 | import * as path from 'path' 4 | import * as fs from 'fs' 5 | import * as env from '../environment.js' 6 | import * as number from '../number.js' 7 | import * as logging from 'lib0/logging' 8 | 9 | const host = env.getParam('--host', 'localhost') 10 | const port = number.parseInt(env.getParam('--port', '8000')) 11 | const paramOpenFile = env.getParam('-o', '') 12 | 13 | /** 14 | * @type {Object} 15 | */ 16 | const types = { 17 | html: 'text/html', 18 | css: 'text/css', 19 | js: 'application/javascript', 20 | mjs: 'application/javascript', 21 | png: 'image/png', 22 | jpg: 'image/jpeg', 23 | jpeg: 'image/jpeg', 24 | gif: 'image/gif', 25 | json: 'application/json', 26 | xml: 'application/xml', 27 | wasm: 'application/wasm' 28 | } 29 | 30 | const root = path.normalize(path.resolve('./')) 31 | 32 | const server = http.createServer((req, res) => { 33 | const url = (req.url || '/index.html').split('?')[0] 34 | logging.print(logging.ORANGE, logging.BOLD, req.method || '', ' ', logging.GREY, logging.UNBOLD, url) 35 | const extension = path.extname(url).slice(1) 36 | /** 37 | * @type {string} 38 | */ 39 | const type = (extension && types[extension]) || types.html 40 | const supportedExtension = Boolean(type) 41 | if (!supportedExtension) { 42 | res.writeHead(404, { 'Content-Type': 'text/html' }) 43 | res.end('404: File not found') 44 | return 45 | } 46 | let fileName = url 47 | if (url === '/') fileName = 'index.html' 48 | else if (!extension) { 49 | try { 50 | fs.accessSync(path.join(root, url + '.html'), fs.constants.F_OK) 51 | fileName = url + '.html' 52 | } catch (e) { 53 | fileName = path.join(url, 'index.html') 54 | } 55 | } 56 | 57 | const filePath = path.join(root, fileName) 58 | const isPathUnderRoot = path 59 | .normalize(path.resolve(filePath)) 60 | .startsWith(root) 61 | 62 | if (!isPathUnderRoot) { 63 | res.writeHead(404, { 'Content-Type': 'text/html' }) 64 | res.end('404: File not found') 65 | logging.print(logging.RED, logging.BOLD, 'Not Found: ', logging.GREY, logging.UNBOLD, url) 66 | return 67 | } 68 | 69 | fs.readFile(filePath, (err, data) => { 70 | if (err) { 71 | logging.print(logging.RED, logging.BOLD, 'Cannot read file: ', logging.GREY, logging.UNBOLD, url) 72 | res.writeHead(404, { 'Content-Type': 'text/html' }) 73 | res.end('404: File not found') 74 | } else { 75 | res.writeHead(200, { 'Content-Type': type }) 76 | res.end(data) 77 | } 78 | }) 79 | }) 80 | 81 | server.listen(port, host, () => { 82 | logging.print(logging.BOLD, logging.ORANGE, `Server is running on http://${host}:${port}`) 83 | if (paramOpenFile) { 84 | const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open' 85 | import('child_process').then(cp => { 86 | cp.exec(`${start} http://${host}:${port}/${paramOpenFile}`) 87 | }) 88 | } 89 | }) 90 | -------------------------------------------------------------------------------- /bin/gendocs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-ignore 3 | import jsdoc from 'jsdoc-api' 4 | import * as fs from 'fs' 5 | 6 | const firstTagContentRegex = /<\w>([^<]+)<\/\w>([^]*)/ 7 | const jsdocReturnRegex = /\* @return {(.*)}/ 8 | const jsdocTypeRegex = /\* @type {(.*)}/ 9 | 10 | const files = fs.readdirSync('./').filter(file => /(?/g 14 | /** 15 | * @param {string} s 16 | */ 17 | const toSafeHtml = s => s.replace(_ltregex, '<').replace(_rtregex, '>') 18 | 19 | const READMEcontent = fs.readFileSync('./README.md', 'utf8') 20 | 21 | jsdoc.explain({ 22 | files, 23 | configure: '.jsdoc.json' 24 | }).then(/** @param {Array} json */ json => { 25 | const strBuilder = [] 26 | /** 27 | * @type {Object, name: string, description: string }>} 28 | */ 29 | const modules = {} 30 | json.forEach(item => { 31 | if (item.meta && item.meta.filename) { 32 | const mod = modules[item.meta.filename] || (modules[item.meta.filename] = { items: [], name: item.meta.filename.slice(0, -3), description: '' }) 33 | if (item.kind === 'module') { 34 | mod.name = item.name 35 | mod.description = item.description || '' 36 | } else { 37 | mod.items.push(item) 38 | } 39 | } 40 | }) 41 | /** 42 | * @type {Object} 43 | */ 44 | const classDescriptions = {} 45 | for (const fileName in modules) { 46 | const mod = modules[fileName] 47 | const items = mod.items 48 | const desc = firstTagContentRegex.exec(mod.description) 49 | const descHead = desc ? desc[1] : '' 50 | const descRest = desc ? desc[2] : '' 51 | strBuilder.push(`
[lib0/${mod.name}] ${descHead}`) 52 | strBuilder.push(`
import * as ${mod.name} from 'lib0/${fileName.slice(0, -3)}'
`) 53 | if (descRest.length > 0) { 54 | strBuilder.push(descRest) 55 | } 56 | strBuilder.push('
') 57 | for (let i = 0; i < items.length; i++) { 58 | const item = items[i] 59 | if (!item.ignore && item.scope !== 'inner' && item.name[0] !== '_' && item.longname.indexOf('~') < 0) { 60 | // strBuilder.push(JSON.stringify(item)) // output json for debugging 61 | switch (item.kind) { 62 | case 'class': { 63 | if (item.params == null) { 64 | classDescriptions[item.longname] = item.classdesc 65 | break 66 | } 67 | } 68 | // eslint-disable-next-line 69 | case 'constant': { 70 | if (item.params == null && item.returns == null) { 71 | const typeEval = jsdocTypeRegex.exec(item.comment) 72 | strBuilder.push(`${item.longname.slice(7)}${typeEval ? (': ' + toSafeHtml(typeEval[1])) : ''}
`) 73 | if (item.description) { 74 | strBuilder.push(`
${item.description}
`) 75 | } 76 | break 77 | } 78 | } 79 | // eslint-disable-next-line 80 | case 'function': { 81 | /** 82 | * @param {string} name 83 | */ 84 | const getOriginalParamTypeDecl = name => { 85 | const regval = new RegExp('@param {(.*)} \\[?' + name + '\\]?[^\\w]*').exec(item.comment) 86 | return regval ? regval[1] : null 87 | } 88 | if (item.params == null && item.returns == null) { 89 | break 90 | } 91 | const paramVal = (item.params || []).map(/** @param {any} ret */ ret => `${ret.name}: ${getOriginalParamTypeDecl(ret.name) || ret.type.names.join('|')}`).join(', ') 92 | const evalReturnRegex = jsdocReturnRegex.exec(item.comment) 93 | const returnVal = evalReturnRegex ? `: ${evalReturnRegex[1]}` : (item.returns ? item.returns.map(/** @param {any} r */ r => r.type.names.join('|')).join('|') : '') 94 | strBuilder.push(`${item.kind === 'class' ? 'new ' : ''}${item.longname.slice(7)}(${toSafeHtml(paramVal)})${toSafeHtml(returnVal)}
`) 95 | const desc = item.description || item.classdesc || classDescriptions[item.longname] || null 96 | if (desc) { 97 | strBuilder.push(`
${desc}
`) 98 | } 99 | break 100 | } 101 | case 'member': { 102 | if (item.type) { 103 | strBuilder.push(`${item.longname.slice(7)}: ${toSafeHtml(/** @type {RegExpExecArray} */ (jsdocTypeRegex.exec(item.comment))[1])}
`) 104 | if (item.description) { 105 | strBuilder.push(`
${item.description}
`) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | strBuilder.push('
') 113 | strBuilder.push('
') 114 | } 115 | const replaceReadme = READMEcontent.replace(/
[^]*<\/details>/, strBuilder.join('\n')) 116 | fs.writeFileSync('./README.md', replaceReadme) 117 | }) 118 | -------------------------------------------------------------------------------- /bin/gentesthtml.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as fs from 'fs' 3 | import * as object from '../object.js' 4 | import * as env from '../environment.js' 5 | 6 | const script = env.getParam('--script', './test.js') 7 | const includeDeps = env.getParam('--include-dependencies', '').split(',').filter(d => d.length) 8 | 9 | /** 10 | * @type {Object} 11 | */ 12 | const exports = {} 13 | /** 14 | * @type {Object>} 15 | */ 16 | const scopes = {} 17 | 18 | /** 19 | * @param {any} v 20 | * @param {string} k 21 | * @param {string} pkgName 22 | * @param {string} pathPrefix 23 | * @param {Object} importMap 24 | */ 25 | const extractModMap = (v, k, pkgName, pathPrefix, importMap) => { 26 | if (k[0] !== '.') return 27 | if (typeof v === 'object') { 28 | extractModMap(v.browser || v.import || v.module || v.default, k, pkgName, pathPrefix, importMap) 29 | } else if (v && v[0] === '.') { 30 | importMap[pkgName + k.slice(1)] = pathPrefix + v.slice(1) 31 | } 32 | } 33 | 34 | /** 35 | * @param {any} pkgJson 36 | * @param {string} pathPrefix 37 | * @param {Object} importMap 38 | */ 39 | const readPkg = (pkgJson, pathPrefix, importMap) => { 40 | object.forEach(pkgJson.exports, (v, k) => extractModMap(v, k, pkgJson.name, pathPrefix, importMap)) 41 | object.forEach(pkgJson.dependencies, (_v, depName) => { 42 | const nextImportMap = pathPrefix === '.' ? exports : (scopes[pathPrefix + '/'] = {}) 43 | const prefix = `./node_modules/${depName}` 44 | const depPkgJson = JSON.parse(fs.readFileSync(prefix + '/package.json', { encoding: 'utf8' })) 45 | readPkg(depPkgJson, prefix, nextImportMap) 46 | }) 47 | } 48 | 49 | const rootPkgJson = JSON.parse(fs.readFileSync('./package.json', { encoding: 'utf8' })) 50 | readPkg(rootPkgJson, '.', exports) 51 | includeDeps.forEach(depName => { 52 | const prefix = `./node_modules/${depName}` 53 | const depPkgJson = JSON.parse(fs.readFileSync(`${prefix}/package.json`, { encoding: 'utf8' })) 54 | readPkg(depPkgJson, prefix, exports) 55 | }) 56 | 57 | const testHtml = ` 58 | 59 | 60 | 61 | Testing ${rootPkgJson.name} 62 | 68 | 69 | 70 | 71 | 72 | 73 | ` 74 | 75 | console.log(testHtml) 76 | -------------------------------------------------------------------------------- /binary.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | /** 4 | * Binary data constants. 5 | * 6 | * @module binary 7 | */ 8 | 9 | /** 10 | * n-th bit activated. 11 | * 12 | * @type {number} 13 | */ 14 | export const BIT1 = 1 15 | export const BIT2 = 2 16 | export const BIT3 = 4 17 | export const BIT4 = 8 18 | export const BIT5 = 16 19 | export const BIT6 = 32 20 | export const BIT7 = 64 21 | export const BIT8 = 128 22 | export const BIT9 = 256 23 | export const BIT10 = 512 24 | export const BIT11 = 1024 25 | export const BIT12 = 2048 26 | export const BIT13 = 4096 27 | export const BIT14 = 8192 28 | export const BIT15 = 16384 29 | export const BIT16 = 32768 30 | export const BIT17 = 65536 31 | export const BIT18 = 1 << 17 32 | export const BIT19 = 1 << 18 33 | export const BIT20 = 1 << 19 34 | export const BIT21 = 1 << 20 35 | export const BIT22 = 1 << 21 36 | export const BIT23 = 1 << 22 37 | export const BIT24 = 1 << 23 38 | export const BIT25 = 1 << 24 39 | export const BIT26 = 1 << 25 40 | export const BIT27 = 1 << 26 41 | export const BIT28 = 1 << 27 42 | export const BIT29 = 1 << 28 43 | export const BIT30 = 1 << 29 44 | export const BIT31 = 1 << 30 45 | export const BIT32 = 1 << 31 46 | 47 | /** 48 | * First n bits activated. 49 | * 50 | * @type {number} 51 | */ 52 | export const BITS0 = 0 53 | export const BITS1 = 1 54 | export const BITS2 = 3 55 | export const BITS3 = 7 56 | export const BITS4 = 15 57 | export const BITS5 = 31 58 | export const BITS6 = 63 59 | export const BITS7 = 127 60 | export const BITS8 = 255 61 | export const BITS9 = 511 62 | export const BITS10 = 1023 63 | export const BITS11 = 2047 64 | export const BITS12 = 4095 65 | export const BITS13 = 8191 66 | export const BITS14 = 16383 67 | export const BITS15 = 32767 68 | export const BITS16 = 65535 69 | export const BITS17 = BIT18 - 1 70 | export const BITS18 = BIT19 - 1 71 | export const BITS19 = BIT20 - 1 72 | export const BITS20 = BIT21 - 1 73 | export const BITS21 = BIT22 - 1 74 | export const BITS22 = BIT23 - 1 75 | export const BITS23 = BIT24 - 1 76 | export const BITS24 = BIT25 - 1 77 | export const BITS25 = BIT26 - 1 78 | export const BITS26 = BIT27 - 1 79 | export const BITS27 = BIT28 - 1 80 | export const BITS28 = BIT29 - 1 81 | export const BITS29 = BIT30 - 1 82 | export const BITS30 = BIT31 - 1 83 | /** 84 | * @type {number} 85 | */ 86 | export const BITS31 = 0x7FFFFFFF 87 | /** 88 | * @type {number} 89 | */ 90 | export const BITS32 = 0xFFFFFFFF 91 | -------------------------------------------------------------------------------- /binary.test.js: -------------------------------------------------------------------------------- 1 | import * as binary from './binary.js' 2 | import * as t from './testing.js' 3 | 4 | /** 5 | * @param {t.TestCase} tc 6 | */ 7 | export const testBitx = tc => { 8 | for (let i = 1; i <= 32; i++) { 9 | // @ts-ignore 10 | t.assert(binary[`BIT${i}`] === (1 << (i - 1)), `BIT${i}=${1 << (i - 1)}`) 11 | } 12 | } 13 | 14 | /** 15 | * @param {t.TestCase} tc 16 | */ 17 | export const testBitsx = tc => { 18 | t.assert(binary.BITS0 === 0) 19 | for (let i = 1; i < 32; i++) { 20 | const expected = ((1 << i) - 1) >>> 0 21 | // @ts-ignore 22 | const have = binary[`BITS${i}`] 23 | t.assert(have === expected, `BITS${i}=${have}=${expected}`) 24 | } 25 | t.assert(binary.BITS32 === 0xFFFFFFFF) 26 | } 27 | -------------------------------------------------------------------------------- /broadcastchannel.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | /** 4 | * Helpers for cross-tab communication using broadcastchannel with LocalStorage fallback. 5 | * 6 | * ```js 7 | * // In browser window A: 8 | * broadcastchannel.subscribe('my events', data => console.log(data)) 9 | * broadcastchannel.publish('my events', 'Hello world!') // => A: 'Hello world!' fires synchronously in same tab 10 | * 11 | * // In browser window B: 12 | * broadcastchannel.publish('my events', 'hello from tab B') // => A: 'hello from tab B' 13 | * ``` 14 | * 15 | * @module broadcastchannel 16 | */ 17 | 18 | // @todo before next major: use Uint8Array instead as buffer object 19 | 20 | import * as map from './map.js' 21 | import * as set from './set.js' 22 | import * as buffer from './buffer.js' 23 | import * as storage from './storage.js' 24 | 25 | /** 26 | * @typedef {Object} Channel 27 | * @property {Set} Channel.subs 28 | * @property {any} Channel.bc 29 | */ 30 | 31 | /** 32 | * @type {Map} 33 | */ 34 | const channels = new Map() 35 | 36 | /* c8 ignore start */ 37 | class LocalStoragePolyfill { 38 | /** 39 | * @param {string} room 40 | */ 41 | constructor (room) { 42 | this.room = room 43 | /** 44 | * @type {null|function({data:ArrayBuffer}):void} 45 | */ 46 | this.onmessage = null 47 | /** 48 | * @param {any} e 49 | */ 50 | this._onChange = e => e.key === room && this.onmessage !== null && this.onmessage({ data: buffer.fromBase64(e.newValue || '') }) 51 | storage.onChange(this._onChange) 52 | } 53 | 54 | /** 55 | * @param {ArrayBuffer} buf 56 | */ 57 | postMessage (buf) { 58 | storage.varStorage.setItem(this.room, buffer.toBase64(buffer.createUint8ArrayFromArrayBuffer(buf))) 59 | } 60 | 61 | close () { 62 | storage.offChange(this._onChange) 63 | } 64 | } 65 | /* c8 ignore stop */ 66 | 67 | // Use BroadcastChannel or Polyfill 68 | /* c8 ignore next */ 69 | const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel 70 | 71 | /** 72 | * @param {string} room 73 | * @return {Channel} 74 | */ 75 | const getChannel = room => 76 | map.setIfUndefined(channels, room, () => { 77 | const subs = set.create() 78 | const bc = new BC(room) 79 | /** 80 | * @param {{data:ArrayBuffer}} e 81 | */ 82 | /* c8 ignore next */ 83 | bc.onmessage = e => subs.forEach(sub => sub(e.data, 'broadcastchannel')) 84 | return { 85 | bc, subs 86 | } 87 | }) 88 | 89 | /** 90 | * Subscribe to global `publish` events. 91 | * 92 | * @function 93 | * @param {string} room 94 | * @param {function(any, any):any} f 95 | */ 96 | export const subscribe = (room, f) => { 97 | getChannel(room).subs.add(f) 98 | return f 99 | } 100 | 101 | /** 102 | * Unsubscribe from `publish` global events. 103 | * 104 | * @function 105 | * @param {string} room 106 | * @param {function(any, any):any} f 107 | */ 108 | export const unsubscribe = (room, f) => { 109 | const channel = getChannel(room) 110 | const unsubscribed = channel.subs.delete(f) 111 | if (unsubscribed && channel.subs.size === 0) { 112 | channel.bc.close() 113 | channels.delete(room) 114 | } 115 | return unsubscribed 116 | } 117 | 118 | /** 119 | * Publish data to all subscribers (including subscribers on this tab) 120 | * 121 | * @function 122 | * @param {string} room 123 | * @param {any} data 124 | * @param {any} [origin] 125 | */ 126 | export const publish = (room, data, origin = null) => { 127 | const c = getChannel(room) 128 | c.bc.postMessage(data) 129 | c.subs.forEach(sub => sub(data, origin)) 130 | } 131 | -------------------------------------------------------------------------------- /broadcastchannel.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as bc from './broadcastchannel.js' 3 | 4 | /** 5 | * @param {t.TestCase} tc 6 | */ 7 | export const testBroadcastChannel = tc => { 8 | bc.publish('test', 'test1', tc) 9 | /** 10 | * @type {any} 11 | */ 12 | const messages = [] 13 | const sub = bc.subscribe('test', (data, origin) => { 14 | messages.push({ data, origin }) 15 | }) 16 | t.compare(messages, []) 17 | bc.publish('test', 'test2', tc) 18 | bc.publish('test', 'test3') 19 | t.compare(messages, [{ data: 'test2', origin: tc }, { data: 'test3', origin: null }]) 20 | bc.unsubscribe('test', sub) 21 | bc.publish('test', 'test4') 22 | t.compare(messages, [{ data: 'test2', origin: tc }, { data: 'test3', origin: null }]) 23 | } 24 | -------------------------------------------------------------------------------- /buffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions to work with buffers (Uint8Array). 3 | * 4 | * @module buffer 5 | */ 6 | 7 | import * as string from './string.js' 8 | import * as env from './environment.js' 9 | import * as array from './array.js' 10 | import * as math from './math.js' 11 | import * as encoding from './encoding.js' 12 | import * as decoding from './decoding.js' 13 | 14 | /** 15 | * @param {number} len 16 | */ 17 | export const createUint8ArrayFromLen = len => new Uint8Array(len) 18 | 19 | /** 20 | * Create Uint8Array with initial content from buffer 21 | * 22 | * @param {ArrayBuffer} buffer 23 | * @param {number} byteOffset 24 | * @param {number} length 25 | */ 26 | export const createUint8ArrayViewFromArrayBuffer = (buffer, byteOffset, length) => new Uint8Array(buffer, byteOffset, length) 27 | 28 | /** 29 | * Create Uint8Array with initial content from buffer 30 | * 31 | * @param {ArrayBuffer} buffer 32 | */ 33 | export const createUint8ArrayFromArrayBuffer = buffer => new Uint8Array(buffer) 34 | 35 | /* c8 ignore start */ 36 | /** 37 | * @param {Uint8Array} bytes 38 | * @return {string} 39 | */ 40 | const toBase64Browser = bytes => { 41 | let s = '' 42 | for (let i = 0; i < bytes.byteLength; i++) { 43 | s += string.fromCharCode(bytes[i]) 44 | } 45 | // eslint-disable-next-line no-undef 46 | return btoa(s) 47 | } 48 | /* c8 ignore stop */ 49 | 50 | /** 51 | * @param {Uint8Array} bytes 52 | * @return {string} 53 | */ 54 | const toBase64Node = bytes => Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64') 55 | 56 | /* c8 ignore start */ 57 | /** 58 | * @param {string} s 59 | * @return {Uint8Array} 60 | */ 61 | const fromBase64Browser = s => { 62 | // eslint-disable-next-line no-undef 63 | const a = atob(s) 64 | const bytes = createUint8ArrayFromLen(a.length) 65 | for (let i = 0; i < a.length; i++) { 66 | bytes[i] = a.charCodeAt(i) 67 | } 68 | return bytes 69 | } 70 | /* c8 ignore stop */ 71 | 72 | /** 73 | * @param {string} s 74 | */ 75 | const fromBase64Node = s => { 76 | const buf = Buffer.from(s, 'base64') 77 | return createUint8ArrayViewFromArrayBuffer(buf.buffer, buf.byteOffset, buf.byteLength) 78 | } 79 | 80 | /* c8 ignore next */ 81 | export const toBase64 = env.isBrowser ? toBase64Browser : toBase64Node 82 | 83 | /* c8 ignore next */ 84 | export const fromBase64 = env.isBrowser ? fromBase64Browser : fromBase64Node 85 | 86 | /** 87 | * Implements base64url - see https://datatracker.ietf.org/doc/html/rfc4648#section-5 88 | * @param {Uint8Array} buf 89 | */ 90 | export const toBase64UrlEncoded = buf => toBase64(buf).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '') 91 | 92 | /** 93 | * @param {string} base64 94 | */ 95 | export const fromBase64UrlEncoded = base64 => fromBase64(base64.replaceAll('-', '+').replaceAll('_', '/')) 96 | 97 | /** 98 | * Base64 is always a more efficient choice. This exists for utility purposes only. 99 | * 100 | * @param {Uint8Array} buf 101 | */ 102 | export const toHexString = buf => array.map(buf, b => b.toString(16).padStart(2, '0')).join('') 103 | 104 | /** 105 | * Note: This function expects that the hex doesn't start with 0x.. 106 | * 107 | * @param {string} hex 108 | */ 109 | export const fromHexString = hex => { 110 | const hlen = hex.length 111 | const buf = new Uint8Array(math.ceil(hlen / 2)) 112 | for (let i = 0; i < hlen; i += 2) { 113 | buf[buf.length - i / 2 - 1] = Number.parseInt(hex.slice(hlen - i - 2, hlen - i), 16) 114 | } 115 | return buf 116 | } 117 | 118 | /** 119 | * Copy the content of an Uint8Array view to a new ArrayBuffer. 120 | * 121 | * @param {Uint8Array} uint8Array 122 | * @return {Uint8Array} 123 | */ 124 | export const copyUint8Array = uint8Array => { 125 | const newBuf = createUint8ArrayFromLen(uint8Array.byteLength) 126 | newBuf.set(uint8Array) 127 | return newBuf 128 | } 129 | 130 | /** 131 | * Encode anything as a UInt8Array. It's a pun on typescripts's `any` type. 132 | * See encoding.writeAny for more information. 133 | * 134 | * @param {any} data 135 | * @return {Uint8Array} 136 | */ 137 | export const encodeAny = data => 138 | encoding.encode(encoder => encoding.writeAny(encoder, data)) 139 | 140 | /** 141 | * Decode an any-encoded value. 142 | * 143 | * @param {Uint8Array} buf 144 | * @return {any} 145 | */ 146 | export const decodeAny = buf => decoding.readAny(decoding.createDecoder(buf)) 147 | 148 | /** 149 | * Shift Byte Array {N} bits to the left. Does not expand byte array. 150 | * 151 | * @param {Uint8Array} bs 152 | * @param {number} N should be in the range of [0-7] 153 | */ 154 | export const shiftNBitsLeft = (bs, N) => { 155 | if (N === 0) return bs 156 | bs = new Uint8Array(bs) 157 | bs[0] <<= N 158 | for (let i = 1; i < bs.length; i++) { 159 | bs[i - 1] |= bs[i] >>> (8 - N) 160 | bs[i] <<= N 161 | } 162 | return bs 163 | } 164 | -------------------------------------------------------------------------------- /buffer.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as buffer from './buffer.js' 3 | import * as prng from './prng.js' 4 | 5 | /** 6 | * @param {t.TestCase} tc 7 | * @param {function(Uint8Array):string} encoder 8 | * @param {function(string):Uint8Array} decoder 9 | */ 10 | const testEncodingHelper = (tc, encoder, decoder) => { 11 | const gen = tc.prng 12 | const barr = prng.uint8Array(gen, prng.uint32(gen, 0, 47)) 13 | const copied = buffer.copyUint8Array(barr) 14 | const encoded = encoder(barr) 15 | t.assert(encoded.constructor === String) 16 | const decoded = decoder(encoded) 17 | t.assert(decoded.constructor === Uint8Array) 18 | t.assert(decoded.byteLength === barr.byteLength) 19 | for (let i = 0; i < barr.length; i++) { 20 | t.assert(barr[i] === decoded[i]) 21 | } 22 | t.compare(copied, decoded) 23 | } 24 | 25 | /** 26 | * @param {t.TestCase} tc 27 | */ 28 | export const testRepeatBase64urlEncoding = tc => { 29 | testEncodingHelper(tc, buffer.toBase64UrlEncoded, buffer.fromBase64UrlEncoded) 30 | } 31 | 32 | /** 33 | * @param {t.TestCase} tc 34 | */ 35 | export const testRepeatBase64Encoding = tc => { 36 | testEncodingHelper(tc, buffer.toBase64, buffer.fromBase64) 37 | } 38 | 39 | /** 40 | * @param {t.TestCase} tc 41 | */ 42 | export const testRepeatHexEncoding = tc => { 43 | testEncodingHelper(tc, buffer.toHexString, buffer.fromHexString) 44 | } 45 | 46 | /** 47 | * @param {t.TestCase} _tc 48 | */ 49 | export const testAnyEncoding = _tc => { 50 | const obj = { val: 1, arr: [1, 2], str: '409231dtrnä' } 51 | const res = buffer.decodeAny(buffer.encodeAny(obj)) 52 | t.compare(obj, res) 53 | } 54 | -------------------------------------------------------------------------------- /cache.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | /** 4 | * An implementation of a map which has keys that expire. 5 | * 6 | * @module cache 7 | */ 8 | 9 | import * as list from './list.js' 10 | import * as map from './map.js' 11 | import * as time from './time.js' 12 | 13 | /** 14 | * @template K, V 15 | * 16 | * @implements {list.ListNode} 17 | */ 18 | class Entry { 19 | /** 20 | * @param {K} key 21 | * @param {V | Promise} val 22 | */ 23 | constructor (key, val) { 24 | /** 25 | * @type {this | null} 26 | */ 27 | this.prev = null 28 | /** 29 | * @type {this | null} 30 | */ 31 | this.next = null 32 | this.created = time.getUnixTime() 33 | this.val = val 34 | this.key = key 35 | } 36 | } 37 | 38 | /** 39 | * @template K, V 40 | */ 41 | export class Cache { 42 | /** 43 | * @param {number} timeout 44 | */ 45 | constructor (timeout) { 46 | this.timeout = timeout 47 | /** 48 | * @type list.List> 49 | */ 50 | this._q = list.create() 51 | /** 52 | * @type {Map>} 53 | */ 54 | this._map = map.create() 55 | } 56 | } 57 | 58 | /** 59 | * @template K, V 60 | * 61 | * @param {Cache} cache 62 | * @return {number} Returns the current timestamp 63 | */ 64 | export const removeStale = cache => { 65 | const now = time.getUnixTime() 66 | const q = cache._q 67 | while (q.start && now - q.start.created > cache.timeout) { 68 | cache._map.delete(q.start.key) 69 | list.popFront(q) 70 | } 71 | return now 72 | } 73 | 74 | /** 75 | * @template K, V 76 | * 77 | * @param {Cache} cache 78 | * @param {K} key 79 | * @param {V} value 80 | */ 81 | export const set = (cache, key, value) => { 82 | const now = removeStale(cache) 83 | const q = cache._q 84 | const n = cache._map.get(key) 85 | if (n) { 86 | list.removeNode(q, n) 87 | list.pushEnd(q, n) 88 | n.created = now 89 | n.val = value 90 | } else { 91 | const node = new Entry(key, value) 92 | list.pushEnd(q, node) 93 | cache._map.set(key, node) 94 | } 95 | } 96 | 97 | /** 98 | * @template K, V 99 | * 100 | * @param {Cache} cache 101 | * @param {K} key 102 | * @return {Entry | undefined} 103 | */ 104 | const getNode = (cache, key) => { 105 | removeStale(cache) 106 | const n = cache._map.get(key) 107 | if (n) { 108 | return n 109 | } 110 | } 111 | 112 | /** 113 | * @template K, V 114 | * 115 | * @param {Cache} cache 116 | * @param {K} key 117 | * @return {V | undefined} 118 | */ 119 | export const get = (cache, key) => { 120 | const n = getNode(cache, key) 121 | return n && !(n.val instanceof Promise) ? n.val : undefined 122 | } 123 | 124 | /** 125 | * @template K, V 126 | * 127 | * @param {Cache} cache 128 | * @param {K} key 129 | */ 130 | export const refreshTimeout = (cache, key) => { 131 | const now = time.getUnixTime() 132 | const q = cache._q 133 | const n = cache._map.get(key) 134 | if (n) { 135 | list.removeNode(q, n) 136 | list.pushEnd(q, n) 137 | n.created = now 138 | } 139 | } 140 | 141 | /** 142 | * Works well in conjunktion with setIfUndefined which has an async init function. 143 | * Using getAsync & setIfUndefined ensures that the init function is only called once. 144 | * 145 | * @template K, V 146 | * 147 | * @param {Cache} cache 148 | * @param {K} key 149 | * @return {V | Promise | undefined} 150 | */ 151 | export const getAsync = (cache, key) => { 152 | const n = getNode(cache, key) 153 | return n ? n.val : undefined 154 | } 155 | 156 | /** 157 | * @template K, V 158 | * 159 | * @param {Cache} cache 160 | * @param {K} key 161 | */ 162 | export const remove = (cache, key) => { 163 | const n = cache._map.get(key) 164 | if (n) { 165 | list.removeNode(cache._q, n) 166 | cache._map.delete(key) 167 | return n.val && !(n.val instanceof Promise) ? n.val : undefined 168 | } 169 | } 170 | 171 | /** 172 | * @template K, V 173 | * 174 | * @param {Cache} cache 175 | * @param {K} key 176 | * @param {function():Promise} init 177 | * @param {boolean} removeNull Optional argument that automatically removes values that resolve to null/undefined from the cache. 178 | * @return {Promise | V} 179 | */ 180 | export const setIfUndefined = (cache, key, init, removeNull = false) => { 181 | removeStale(cache) 182 | const q = cache._q 183 | const n = cache._map.get(key) 184 | if (n) { 185 | return n.val 186 | } else { 187 | const p = init() 188 | const node = new Entry(key, p) 189 | list.pushEnd(q, node) 190 | cache._map.set(key, node) 191 | p.then(v => { 192 | if (p === node.val) { 193 | node.val = v 194 | } 195 | if (removeNull && v == null) { 196 | remove(cache, key) 197 | } 198 | }) 199 | return p 200 | } 201 | } 202 | 203 | /** 204 | * @param {number} timeout 205 | */ 206 | export const create = timeout => new Cache(timeout) 207 | -------------------------------------------------------------------------------- /cache.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as cache from './cache.js' 3 | import * as promise from './promise.js' 4 | 5 | /** 6 | * @param {t.TestCase} tc 7 | */ 8 | export const testCache = async tc => { 9 | /** 10 | * @type {cache.Cache} 11 | */ 12 | const c = cache.create(50) 13 | cache.set(c, 'a', '1') 14 | t.assert(cache.get(c, 'a') === '1') 15 | t.assert(await cache.getAsync(c, 'a') === '1') 16 | const p = cache.setIfUndefined(c, 'b', () => promise.resolveWith('2')) 17 | const q = cache.setIfUndefined(c, 'b', () => promise.resolveWith('3')) 18 | t.assert(p === q) 19 | t.assert(cache.get(c, 'b') == null) 20 | t.assert(cache.getAsync(c, 'b') === p) 21 | t.assert(await p === '2') 22 | t.assert(cache.get(c, 'b') === '2') 23 | t.assert(cache.getAsync(c, 'b') === '2') 24 | 25 | await promise.wait(5) // keys shouldn't be timed out yet 26 | t.assert(cache.get(c, 'a') === '1') 27 | t.assert(cache.get(c, 'b') === '2') 28 | 29 | /** 30 | * @type {any} 31 | */ 32 | const m = c._map 33 | const aTimestamp1 = m.get('a').created 34 | const bTimestamp1 = m.get('b').created 35 | 36 | // write new values and check later if the creation-timestamp was updated 37 | cache.set(c, 'a', '11') 38 | cache.set(c, 'b', '22') 39 | 40 | await promise.wait(5) // keys should be updated and not timed out. Hence the creation time should be updated 41 | t.assert(cache.get(c, 'a') === '11') 42 | t.assert(cache.get(c, 'b') === '22') 43 | cache.set(c, 'a', '11') 44 | cache.set(c, 'b', '22') 45 | // timestamps should be updated 46 | t.assert(aTimestamp1 !== m.get('a').created) 47 | t.assert(bTimestamp1 !== m.get('b').created) 48 | 49 | await promise.wait(60) // now the keys should be timed-out 50 | 51 | t.assert(cache.get(c, 'a') == null) 52 | t.assert(cache.getAsync(c, 'b') == null) 53 | 54 | t.assert(c._map.size === 0) 55 | t.assert(c._q.start === null && c._q.end === null) 56 | 57 | // test edge case of setIfUndefined 58 | const xp = cache.setIfUndefined(c, 'a', () => promise.resolve('x')) 59 | cache.set(c, 'a', 'y') 60 | await xp 61 | // we override the Entry.val property in cache when p resolves. However, we must prevent that when the value is overriden before p is resolved. 62 | t.assert(cache.get(c, 'a') === 'y') 63 | 64 | // test that we can remove properties 65 | cache.remove(c, 'a') 66 | cache.remove(c, 'does not exist') // remove a non-existent property to achieve full test-coverage 67 | t.assert(cache.get(c, 'a') === undefined) 68 | 69 | // test that the optional property in setifUndefined works 70 | const yp = cache.setIfUndefined(c, 'a', () => promise.resolveWith(null), true) 71 | t.assert(await yp === null) 72 | t.assert(cache.get(c, 'a') === undefined) 73 | 74 | // check manual updating of timeout 75 | cache.set(c, 'a', '3') 76 | const ts1 = m.get('a').created 77 | await promise.wait(30) 78 | cache.refreshTimeout(c, 'a') 79 | const ts2 = m.get('a').created 80 | t.assert(ts1 !== ts2) 81 | cache.refreshTimeout(c, 'x') // for full test coverage 82 | t.assert(m.get('x') == null) 83 | } 84 | -------------------------------------------------------------------------------- /conditions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Often used conditions. 3 | * 4 | * @module conditions 5 | */ 6 | 7 | /** 8 | * @template T 9 | * @param {T|null|undefined} v 10 | * @return {T|null} 11 | */ 12 | /* c8 ignore next */ 13 | export const undefinedToNull = v => v === undefined ? null : v 14 | -------------------------------------------------------------------------------- /crypto/aes-gcm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AES-GCM is a symmetric key for encryption 3 | */ 4 | 5 | import * as encoding from '../encoding.js' 6 | import * as decoding from '../decoding.js' 7 | import * as webcrypto from 'lib0/webcrypto' 8 | import * as string from '../string.js' 9 | export { exportKeyJwk, exportKeyRaw } from './common.js' 10 | 11 | /** 12 | * @typedef {Array<'encrypt'|'decrypt'>} Usages 13 | */ 14 | 15 | /** 16 | * @type {Usages} 17 | */ 18 | const defaultUsages = ['encrypt', 'decrypt'] 19 | 20 | /** 21 | * @param {CryptoKey} key 22 | * @param {Uint8Array} data 23 | */ 24 | export const encrypt = (key, data) => { 25 | const iv = webcrypto.getRandomValues(new Uint8Array(16)) // 92bit is enough. 128bit is recommended if space is not an issue. 26 | return webcrypto.subtle.encrypt( 27 | { 28 | name: 'AES-GCM', 29 | iv 30 | }, 31 | key, 32 | data 33 | ).then(cipher => { 34 | const encryptedDataEncoder = encoding.createEncoder() 35 | // iv may be sent in the clear to the other peers 36 | encoding.writeUint8Array(encryptedDataEncoder, iv) 37 | encoding.writeVarUint8Array(encryptedDataEncoder, new Uint8Array(cipher)) 38 | return encoding.toUint8Array(encryptedDataEncoder) 39 | }) 40 | } 41 | 42 | /** 43 | * @experimental The API is not final! 44 | * 45 | * Decrypt some data using AES-GCM method. 46 | * 47 | * @param {CryptoKey} key 48 | * @param {Uint8Array} data 49 | * @return {PromiseLike} decrypted buffer 50 | */ 51 | export const decrypt = (key, data) => { 52 | const dataDecoder = decoding.createDecoder(data) 53 | const iv = decoding.readUint8Array(dataDecoder, 16) 54 | const cipher = decoding.readVarUint8Array(dataDecoder) 55 | return webcrypto.subtle.decrypt( 56 | { 57 | name: 'AES-GCM', 58 | iv 59 | }, 60 | key, 61 | cipher 62 | ).then(data => new Uint8Array(data)) 63 | } 64 | 65 | const aesAlgDef = { 66 | name: 'AES-GCM', 67 | length: 256 68 | } 69 | 70 | /** 71 | * @param {any} jwk 72 | * @param {Object} opts 73 | * @param {Usages} [opts.usages] 74 | * @param {boolean} [opts.extractable] 75 | */ 76 | export const importKeyJwk = (jwk, { usages, extractable = false } = {}) => { 77 | if (usages == null) { 78 | /* c8 ignore next */ 79 | usages = jwk.key_ops || defaultUsages 80 | } 81 | return webcrypto.subtle.importKey('jwk', jwk, 'AES-GCM', extractable, /** @type {Usages} */ (usages)) 82 | } 83 | 84 | /** 85 | * Only suited for importing public keys. 86 | * 87 | * @param {Uint8Array} raw 88 | * @param {Object} opts 89 | * @param {Usages} [opts.usages] 90 | * @param {boolean} [opts.extractable] 91 | */ 92 | export const importKeyRaw = (raw, { usages = defaultUsages, extractable = false } = {}) => 93 | webcrypto.subtle.importKey('raw', raw, aesAlgDef, extractable, /** @type {Usages} */ (usages)) 94 | 95 | /** 96 | * @param {Uint8Array | string} data 97 | */ 98 | /* c8 ignore next */ 99 | const toBinary = data => typeof data === 'string' ? string.encodeUtf8(data) : data 100 | 101 | /** 102 | * @experimental The API is not final! 103 | * 104 | * Derive an symmetric key using the Password-Based-Key-Derivation-Function-2. 105 | * 106 | * @param {Uint8Array|string} secret 107 | * @param {Uint8Array|string} salt 108 | * @param {Object} opts 109 | * @param {boolean} [opts.extractable] 110 | * @param {Usages} [opts.usages] 111 | */ 112 | export const deriveKey = (secret, salt, { extractable = false, usages = defaultUsages } = {}) => 113 | webcrypto.subtle.importKey( 114 | 'raw', 115 | toBinary(secret), 116 | 'PBKDF2', 117 | false, 118 | ['deriveKey'] 119 | ).then(keyMaterial => 120 | webcrypto.subtle.deriveKey( 121 | { 122 | name: 'PBKDF2', 123 | salt: toBinary(salt), // NIST recommends at least 64 bits 124 | iterations: 600000, // OWASP recommends 600k iterations 125 | hash: 'SHA-256' 126 | }, 127 | keyMaterial, 128 | aesAlgDef, 129 | extractable, 130 | usages 131 | ) 132 | ) 133 | -------------------------------------------------------------------------------- /crypto/common.js: -------------------------------------------------------------------------------- 1 | import * as webcrypto from 'lib0/webcrypto' 2 | 3 | /** 4 | * @param {CryptoKey} key 5 | */ 6 | export const exportKeyJwk = async key => { 7 | const jwk = await webcrypto.subtle.exportKey('jwk', key) 8 | jwk.key_ops = key.usages 9 | return jwk 10 | } 11 | 12 | /** 13 | * Only suited for exporting public keys. 14 | * 15 | * @param {CryptoKey} key 16 | * @return {Promise} 17 | */ 18 | export const exportKeyRaw = key => 19 | webcrypto.subtle.exportKey('raw', key).then(key => new Uint8Array(key)) 20 | -------------------------------------------------------------------------------- /crypto/ecdsa.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ECDSA is an asymmetric key for signing 3 | */ 4 | 5 | import * as webcrypto from 'lib0/webcrypto' 6 | export { exportKeyJwk, exportKeyRaw } from './common.js' 7 | 8 | /** 9 | * @typedef {Array<'sign'|'verify'>} Usages 10 | */ 11 | 12 | /** 13 | * @type {Usages} 14 | */ 15 | const defaultUsages = ['sign', 'verify'] 16 | 17 | const defaultSignAlgorithm = { 18 | name: 'ECDSA', 19 | hash: 'SHA-384' 20 | } 21 | 22 | /** 23 | * @experimental The API is not final! 24 | * 25 | * Sign a message 26 | * 27 | * @param {CryptoKey} key 28 | * @param {Uint8Array} data 29 | * @return {PromiseLike} signature 30 | */ 31 | export const sign = (key, data) => 32 | webcrypto.subtle.sign( 33 | defaultSignAlgorithm, 34 | key, 35 | data 36 | ).then(signature => new Uint8Array(signature)) 37 | 38 | /** 39 | * @experimental The API is not final! 40 | * 41 | * Sign a message 42 | * 43 | * @param {CryptoKey} key 44 | * @param {Uint8Array} signature 45 | * @param {Uint8Array} data 46 | * @return {PromiseLike} signature 47 | */ 48 | export const verify = (key, signature, data) => 49 | webcrypto.subtle.verify( 50 | defaultSignAlgorithm, 51 | key, 52 | signature, 53 | data 54 | ) 55 | 56 | const defaultKeyAlgorithm = { 57 | name: 'ECDSA', 58 | namedCurve: 'P-384' 59 | } 60 | 61 | /* c8 ignore next */ 62 | /** 63 | * @param {Object} opts 64 | * @param {boolean} [opts.extractable] 65 | * @param {Usages} [opts.usages] 66 | */ 67 | export const generateKeyPair = ({ extractable = false, usages = defaultUsages } = {}) => 68 | webcrypto.subtle.generateKey( 69 | defaultKeyAlgorithm, 70 | extractable, 71 | usages 72 | ) 73 | 74 | /** 75 | * @param {any} jwk 76 | * @param {Object} opts 77 | * @param {boolean} [opts.extractable] 78 | * @param {Usages} [opts.usages] 79 | */ 80 | export const importKeyJwk = (jwk, { extractable = false, usages } = {}) => { 81 | if (usages == null) { 82 | /* c8 ignore next 2 */ 83 | usages = jwk.key_ops || defaultUsages 84 | } 85 | return webcrypto.subtle.importKey('jwk', jwk, defaultKeyAlgorithm, extractable, /** @type {Usages} */ (usages)) 86 | } 87 | 88 | /** 89 | * Only suited for importing public keys. 90 | * 91 | * @param {any} raw 92 | * @param {Object} opts 93 | * @param {boolean} [opts.extractable] 94 | * @param {Usages} [opts.usages] 95 | */ 96 | export const importKeyRaw = (raw, { extractable = false, usages = defaultUsages } = {}) => 97 | webcrypto.subtle.importKey('raw', raw, defaultKeyAlgorithm, extractable, usages) 98 | -------------------------------------------------------------------------------- /crypto/jwt.js: -------------------------------------------------------------------------------- 1 | import * as error from '../error.js' 2 | import * as buffer from '../buffer.js' 3 | import * as string from '../string.js' 4 | import * as json from '../json.js' 5 | import * as ecdsa from '../crypto/ecdsa.js' 6 | import * as time from '../time.js' 7 | 8 | /** 9 | * @param {Object} data 10 | */ 11 | const _stringify = data => buffer.toBase64UrlEncoded(string.encodeUtf8(json.stringify(data))) 12 | 13 | /** 14 | * @param {string} base64url 15 | */ 16 | const _parse = base64url => json.parse(string.decodeUtf8(buffer.fromBase64UrlEncoded(base64url))) 17 | 18 | /** 19 | * @param {CryptoKey} privateKey 20 | * @param {Object} payload 21 | */ 22 | export const encodeJwt = (privateKey, payload) => { 23 | const { name: algName, namedCurve: algCurve } = /** @type {any} */ (privateKey.algorithm) 24 | /* c8 ignore next 3 */ 25 | if (algName !== 'ECDSA' || algCurve !== 'P-384') { 26 | error.unexpectedCase() 27 | } 28 | const header = { 29 | alg: 'ES384', 30 | typ: 'JWT' 31 | } 32 | const jwt = _stringify(header) + '.' + _stringify(payload) 33 | return ecdsa.sign(privateKey, string.encodeUtf8(jwt)).then(signature => 34 | jwt + '.' + buffer.toBase64UrlEncoded(signature) 35 | ) 36 | } 37 | 38 | /** 39 | * @param {CryptoKey} publicKey 40 | * @param {string} jwt 41 | */ 42 | export const verifyJwt = async (publicKey, jwt) => { 43 | const [headerBase64, payloadBase64, signatureBase64] = jwt.split('.') 44 | const verified = await ecdsa.verify(publicKey, buffer.fromBase64UrlEncoded(signatureBase64), string.encodeUtf8(headerBase64 + '.' + payloadBase64)) 45 | /* c8 ignore next 3 */ 46 | if (!verified) { 47 | throw new Error('Invalid JWT') 48 | } 49 | const payload = _parse(payloadBase64) 50 | if (payload.exp != null && time.getUnixTime() > payload.exp) { 51 | throw new Error('Expired JWT') 52 | } 53 | return { 54 | header: _parse(headerBase64), 55 | payload 56 | } 57 | } 58 | 59 | /** 60 | * Decode a jwt without verifying it. Probably a bad idea to use this. Only use if you know the jwt was already verified! 61 | * 62 | * @param {string} jwt 63 | */ 64 | export const unsafeDecode = jwt => { 65 | const [headerBase64, payloadBase64] = jwt.split('.') 66 | return { 67 | header: _parse(headerBase64), 68 | payload: _parse(payloadBase64) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crypto/rsa-oaep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * RSA-OAEP is an asymmetric keypair used for encryption 3 | */ 4 | 5 | import * as webcrypto from 'lib0/webcrypto' 6 | export { exportKeyJwk } from './common.js' 7 | 8 | /** 9 | * @typedef {Array<'encrypt'|'decrypt'>} Usages 10 | */ 11 | 12 | /** 13 | * @type {Usages} 14 | */ 15 | const defaultUsages = ['encrypt', 'decrypt'] 16 | 17 | /** 18 | * Note that the max data size is limited by the size of the RSA key. 19 | * 20 | * @param {CryptoKey} key 21 | * @param {Uint8Array} data 22 | * @return {PromiseLike} 23 | */ 24 | export const encrypt = (key, data) => 25 | webcrypto.subtle.encrypt( 26 | { 27 | name: 'RSA-OAEP' 28 | }, 29 | key, 30 | data 31 | ).then(buf => new Uint8Array(buf)) 32 | 33 | /** 34 | * @experimental The API is not final! 35 | * 36 | * Decrypt some data using AES-GCM method. 37 | * 38 | * @param {CryptoKey} key 39 | * @param {Uint8Array} data 40 | * @return {PromiseLike} decrypted buffer 41 | */ 42 | export const decrypt = (key, data) => 43 | webcrypto.subtle.decrypt( 44 | { 45 | name: 'RSA-OAEP' 46 | }, 47 | key, 48 | data 49 | ).then(data => new Uint8Array(data)) 50 | 51 | /** 52 | * @param {Object} opts 53 | * @param {boolean} [opts.extractable] 54 | * @param {Usages} [opts.usages] 55 | * @return {Promise} 56 | */ 57 | export const generateKeyPair = ({ extractable = false, usages = defaultUsages } = {}) => 58 | webcrypto.subtle.generateKey( 59 | { 60 | name: 'RSA-OAEP', 61 | modulusLength: 4096, 62 | publicExponent: new Uint8Array([1, 0, 1]), 63 | hash: 'SHA-256' 64 | }, 65 | extractable, 66 | usages 67 | ) 68 | 69 | /** 70 | * @param {any} jwk 71 | * @param {Object} opts 72 | * @param {boolean} [opts.extractable] 73 | * @param {Usages} [opts.usages] 74 | */ 75 | export const importKeyJwk = (jwk, { extractable = false, usages } = {}) => { 76 | if (usages == null) { 77 | /* c8 ignore next */ 78 | usages = jwk.key_ops || defaultUsages 79 | } 80 | return webcrypto.subtle.importKey('jwk', jwk, { name: 'RSA-OAEP', hash: 'SHA-256' }, extractable, /** @type {Usages} */ (usages)) 81 | } 82 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "isomorphic.js": "./node_modules/isomorphic.js/node.mjs", 4 | "lib0/logging": "./logging.node.js", 5 | "lib0/performance": "./performance.node.js", 6 | "lib0/crypto/aes-gcm": "./crypto/aes-gcm.js", 7 | "lib0/crypto/rsa-oaep": "./crypto/rsa-oaep.js", 8 | "lib0/crypto/ecdsa": "./crypto/ecdsa.js", 9 | "lib0/crypto/jwt": "./crypto/jwt.js", 10 | "lib0/webcrypto": "./webcrypto.deno.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /diff.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Efficient diffs. 3 | * 4 | * @module diff 5 | */ 6 | 7 | import { equalityStrict } from './function.js' 8 | 9 | /** 10 | * A SimpleDiff describes a change on a String. 11 | * 12 | * ```js 13 | * console.log(a) // the old value 14 | * console.log(b) // the updated value 15 | * // Apply changes of diff (pseudocode) 16 | * a.remove(diff.index, diff.remove) // Remove `diff.remove` characters 17 | * a.insert(diff.index, diff.insert) // Insert `diff.insert` 18 | * a === b // values match 19 | * ``` 20 | * 21 | * @typedef {Object} SimpleDiff 22 | * @property {Number} index The index where changes were applied 23 | * @property {Number} remove The number of characters to delete starting 24 | * at `index`. 25 | * @property {T} insert The new text to insert at `index` after applying 26 | * `delete` 27 | * 28 | * @template T 29 | */ 30 | 31 | const highSurrogateRegex = /[\uD800-\uDBFF]/ 32 | const lowSurrogateRegex = /[\uDC00-\uDFFF]/ 33 | 34 | /** 35 | * Create a diff between two strings. This diff implementation is highly 36 | * efficient, but not very sophisticated. 37 | * 38 | * @function 39 | * 40 | * @param {string} a The old version of the string 41 | * @param {string} b The updated version of the string 42 | * @return {SimpleDiff} The diff description. 43 | */ 44 | export const simpleDiffString = (a, b) => { 45 | let left = 0 // number of same characters counting from left 46 | let right = 0 // number of same characters counting from right 47 | while (left < a.length && left < b.length && a[left] === b[left]) { 48 | left++ 49 | } 50 | // If the last same character is a high surrogate, we need to rollback to the previous character 51 | if (left > 0 && highSurrogateRegex.test(a[left - 1])) left-- 52 | while (right + left < a.length && right + left < b.length && a[a.length - right - 1] === b[b.length - right - 1]) { 53 | right++ 54 | } 55 | // If the last same character is a low surrogate, we need to rollback to the previous character 56 | if (right > 0 && lowSurrogateRegex.test(a[a.length - right])) right-- 57 | return { 58 | index: left, 59 | remove: a.length - left - right, 60 | insert: b.slice(left, b.length - right) 61 | } 62 | } 63 | 64 | /** 65 | * @todo Remove in favor of simpleDiffString 66 | * @deprecated 67 | */ 68 | export const simpleDiff = simpleDiffString 69 | 70 | /** 71 | * Create a diff between two arrays. This diff implementation is highly 72 | * efficient, but not very sophisticated. 73 | * 74 | * Note: This is basically the same function as above. Another function was created so that the runtime 75 | * can better optimize these function calls. 76 | * 77 | * @function 78 | * @template T 79 | * 80 | * @param {Array} a The old version of the array 81 | * @param {Array} b The updated version of the array 82 | * @param {function(T, T):boolean} [compare] 83 | * @return {SimpleDiff>} The diff description. 84 | */ 85 | export const simpleDiffArray = (a, b, compare = equalityStrict) => { 86 | let left = 0 // number of same characters counting from left 87 | let right = 0 // number of same characters counting from right 88 | while (left < a.length && left < b.length && compare(a[left], b[left])) { 89 | left++ 90 | } 91 | while (right + left < a.length && right + left < b.length && compare(a[a.length - right - 1], b[b.length - right - 1])) { 92 | right++ 93 | } 94 | return { 95 | index: left, 96 | remove: a.length - left - right, 97 | insert: b.slice(left, b.length - right) 98 | } 99 | } 100 | 101 | /** 102 | * Diff text and try to diff at the current cursor position. 103 | * 104 | * @param {string} a 105 | * @param {string} b 106 | * @param {number} cursor This should refer to the current left cursor-range position 107 | */ 108 | export const simpleDiffStringWithCursor = (a, b, cursor) => { 109 | let left = 0 // number of same characters counting from left 110 | let right = 0 // number of same characters counting from right 111 | // Iterate left to the right until we find a changed character 112 | // First iteration considers the current cursor position 113 | while ( 114 | left < a.length && 115 | left < b.length && 116 | a[left] === b[left] && 117 | left < cursor 118 | ) { 119 | left++ 120 | } 121 | // If the last same character is a high surrogate, we need to rollback to the previous character 122 | if (left > 0 && highSurrogateRegex.test(a[left - 1])) left-- 123 | // Iterate right to the left until we find a changed character 124 | while ( 125 | right + left < a.length && 126 | right + left < b.length && 127 | a[a.length - right - 1] === b[b.length - right - 1] 128 | ) { 129 | right++ 130 | } 131 | // If the last same character is a low surrogate, we need to rollback to the previous character 132 | if (right > 0 && lowSurrogateRegex.test(a[a.length - right])) right-- 133 | // Try to iterate left further to the right without caring about the current cursor position 134 | while ( 135 | right + left < a.length && 136 | right + left < b.length && 137 | a[left] === b[left] 138 | ) { 139 | left++ 140 | } 141 | if (left > 0 && highSurrogateRegex.test(a[left - 1])) left-- 142 | return { 143 | index: left, 144 | remove: a.length - left - right, 145 | insert: b.slice(left, b.length - right) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /diff.test.js: -------------------------------------------------------------------------------- 1 | import { simpleDiffString, simpleDiffArray, simpleDiffStringWithCursor } from './diff.js' 2 | import * as prng from './prng.js' 3 | import * as f from './function.js' 4 | import * as t from './testing.js' 5 | import * as str from './string.js' 6 | 7 | /** 8 | * @param {string} a 9 | * @param {string} b 10 | * @param {{index: number,remove:number,insert:string}} expected 11 | */ 12 | function runDiffTest (a, b, expected) { 13 | const result = simpleDiffString(a, b) 14 | t.compare(result, expected) 15 | t.compare(result, simpleDiffStringWithCursor(a, b, a.length)) // check that the withCursor approach returns the same result 16 | const recomposed = str.splice(a, result.index, result.remove, result.insert) 17 | t.compareStrings(recomposed, b) 18 | const arrResult = simpleDiffArray(Array.from(a), Array.from(b)) 19 | const arrRecomposed = Array.from(a) 20 | arrRecomposed.splice(arrResult.index, arrResult.remove, ...arrResult.insert) 21 | t.compareStrings(arrRecomposed.join(''), b) 22 | } 23 | 24 | /** 25 | * @param {t.TestCase} tc 26 | */ 27 | export const testDiffing = tc => { 28 | runDiffTest('abc', 'axc', { index: 1, remove: 1, insert: 'x' }) 29 | runDiffTest('bc', 'xc', { index: 0, remove: 1, insert: 'x' }) 30 | runDiffTest('ab', 'ax', { index: 1, remove: 1, insert: 'x' }) 31 | runDiffTest('b', 'x', { index: 0, remove: 1, insert: 'x' }) 32 | runDiffTest('', 'abc', { index: 0, remove: 0, insert: 'abc' }) 33 | runDiffTest('abc', 'xyz', { index: 0, remove: 3, insert: 'xyz' }) 34 | runDiffTest('axz', 'au', { index: 1, remove: 2, insert: 'u' }) 35 | runDiffTest('ax', 'axy', { index: 2, remove: 0, insert: 'y' }) 36 | // These strings share high-surrogate characters 37 | runDiffTest('\u{d83d}\u{dc77}'/* '👷' */, '\u{d83d}\u{dea7}\u{d83d}\u{dc77}'/* '🚧👷' */, { index: 0, remove: 0, insert: '🚧' }) 38 | runDiffTest('\u{d83d}\u{dea7}\u{d83d}\u{dc77}'/* '🚧👷' */, '\u{d83d}\u{dc77}'/* '👷' */, { index: 0, remove: 2, insert: '' }) 39 | // These strings share low-surrogate characters 40 | runDiffTest('\u{d83d}\u{dfe6}\u{d83d}\u{dfe6}'/* '🟦🟦' */, '\u{d83c}\u{dfe6}\u{d83d}\u{dfe6}'/* '🏦🟦' */, { index: 0, remove: 2, insert: '🏦' }) 41 | // check 4-character unicode symbols 42 | runDiffTest('🇦🇨', '🇦🇩', { index: 2, remove: 2, insert: '🇩' }) 43 | runDiffTest('a🇧🇩', '🇦🇩', { index: 0, remove: 3, insert: '🇦' }) 44 | } 45 | 46 | /** 47 | * @param {t.TestCase} tc 48 | */ 49 | export const testRepeatDiffing = tc => { 50 | const a = prng.word(tc.prng) 51 | const b = prng.word(tc.prng) 52 | const change = simpleDiffString(a, b) 53 | const recomposed = str.splice(a, change.index, change.remove, change.insert) 54 | t.compareStrings(recomposed, b) 55 | } 56 | 57 | /** 58 | * @param {t.TestCase} tc 59 | */ 60 | export const testSimpleDiffWithCursor = tc => { 61 | const initial = 'Hello WorldHello World' 62 | const expected = 'Hello World' 63 | { 64 | const change = simpleDiffStringWithCursor(initial, 'Hello World', 0) // should delete the first hello world 65 | t.compare(change, { insert: '', remove: 11, index: 0 }) 66 | const recomposed = str.splice(initial, change.index, change.remove, change.insert) 67 | t.compareStrings(expected, recomposed) 68 | } 69 | { 70 | const change = simpleDiffStringWithCursor(initial, 'Hello World', 11) // should delete the second hello world 71 | t.compare(change, { insert: '', remove: 11, index: 11 }) 72 | const recomposedSecond = str.splice(initial, change.index, change.remove, change.insert) 73 | t.compareStrings(recomposedSecond, expected) 74 | } 75 | { 76 | const change = simpleDiffStringWithCursor(initial, 'Hello World', 5) // should delete in the midst of Hello World 77 | t.compare(change, { insert: '', remove: 11, index: 5 }) 78 | const recomposed = str.splice(initial, change.index, change.remove, change.insert) 79 | t.compareStrings(expected, recomposed) 80 | } 81 | { 82 | const initial = 'Hello my World' 83 | const change = simpleDiffStringWithCursor(initial, 'Hello World', 0) // Should delete after the current cursor position 84 | t.compare(change, { insert: '', remove: 3, index: 5 }) 85 | const recomposed = str.splice(initial, change.index, change.remove, change.insert) 86 | t.compareStrings(expected, recomposed) 87 | } 88 | { 89 | const initial = '🚧🚧🚧' 90 | const change = simpleDiffStringWithCursor(initial, '🚧🚧', 2) // Should delete after the midst of 🚧 91 | t.compare(change, { insert: '', remove: 2, index: 2 }) 92 | const recomposed = str.splice(initial, change.index, change.remove, change.insert) 93 | t.compareStrings('🚧🚧', recomposed) 94 | } 95 | { 96 | const initial = '🚧👷🚧👷' 97 | const change = simpleDiffStringWithCursor(initial, '🚧🚧', 2) // Should delete after the first 🚧 and insert 🚧 98 | t.compare(change, { insert: '🚧', remove: 6, index: 2 }) 99 | const recomposed = str.splice(initial, change.index, change.remove, change.insert) 100 | t.compareStrings('🚧🚧', recomposed) 101 | } 102 | } 103 | 104 | /** 105 | * @param {t.TestCase} tc 106 | */ 107 | export const testArrayDiffing = tc => { 108 | const a = [[1, 2], { x: 'x' }] 109 | const b = [[1, 2], { x: 'x' }] 110 | t.compare(simpleDiffArray(a, b, f.equalityFlat), { index: 2, remove: 0, insert: [] }) 111 | t.compare(simpleDiffArray(a, b, f.equalityStrict), { index: 0, remove: 2, insert: b }) 112 | t.compare(simpleDiffArray([{ x: 'y' }, []], a, f.equalityFlat), { index: 0, remove: 2, insert: b }) 113 | } 114 | -------------------------------------------------------------------------------- /environment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Isomorphic module to work access the environment (query params, env variables). 3 | * 4 | * @module environment 5 | */ 6 | 7 | import * as map from './map.js' 8 | import * as string from './string.js' 9 | import * as conditions from './conditions.js' 10 | import * as storage from './storage.js' 11 | import * as f from './function.js' 12 | 13 | /* c8 ignore next 2 */ 14 | // @ts-ignore 15 | export const isNode = typeof process !== 'undefined' && process.release && /node|io\.js/.test(process.release.name) && Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]' 16 | 17 | /* c8 ignore next */ 18 | export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && !isNode 19 | /* c8 ignore next 3 */ 20 | export const isMac = typeof navigator !== 'undefined' 21 | ? /Mac/.test(navigator.platform) 22 | : false 23 | 24 | /** 25 | * @type {Map} 26 | */ 27 | let params 28 | const args = [] 29 | 30 | /* c8 ignore start */ 31 | const computeParams = () => { 32 | if (params === undefined) { 33 | if (isNode) { 34 | params = map.create() 35 | const pargs = process.argv 36 | let currParamName = null 37 | for (let i = 0; i < pargs.length; i++) { 38 | const parg = pargs[i] 39 | if (parg[0] === '-') { 40 | if (currParamName !== null) { 41 | params.set(currParamName, '') 42 | } 43 | currParamName = parg 44 | } else { 45 | if (currParamName !== null) { 46 | params.set(currParamName, parg) 47 | currParamName = null 48 | } else { 49 | args.push(parg) 50 | } 51 | } 52 | } 53 | if (currParamName !== null) { 54 | params.set(currParamName, '') 55 | } 56 | // in ReactNative for example this would not be true (unless connected to the Remote Debugger) 57 | } else if (typeof location === 'object') { 58 | params = map.create(); // eslint-disable-next-line no-undef 59 | (location.search || '?').slice(1).split('&').forEach((kv) => { 60 | if (kv.length !== 0) { 61 | const [key, value] = kv.split('=') 62 | params.set(`--${string.fromCamelCase(key, '-')}`, value) 63 | params.set(`-${string.fromCamelCase(key, '-')}`, value) 64 | } 65 | }) 66 | } else { 67 | params = map.create() 68 | } 69 | } 70 | return params 71 | } 72 | /* c8 ignore stop */ 73 | 74 | /** 75 | * @param {string} name 76 | * @return {boolean} 77 | */ 78 | /* c8 ignore next */ 79 | export const hasParam = (name) => computeParams().has(name) 80 | 81 | /** 82 | * @param {string} name 83 | * @param {string} defaultVal 84 | * @return {string} 85 | */ 86 | /* c8 ignore next 2 */ 87 | export const getParam = (name, defaultVal) => 88 | computeParams().get(name) || defaultVal 89 | 90 | /** 91 | * @param {string} name 92 | * @return {string|null} 93 | */ 94 | /* c8 ignore next 4 */ 95 | export const getVariable = (name) => 96 | isNode 97 | ? conditions.undefinedToNull(process.env[name.toUpperCase().replaceAll('-', '_')]) 98 | : conditions.undefinedToNull(storage.varStorage.getItem(name)) 99 | 100 | /** 101 | * @param {string} name 102 | * @return {string|null} 103 | */ 104 | /* c8 ignore next 2 */ 105 | export const getConf = (name) => 106 | computeParams().get('--' + name) || getVariable(name) 107 | 108 | /** 109 | * @param {string} name 110 | * @return {string} 111 | */ 112 | /* c8 ignore next 5 */ 113 | export const ensureConf = (name) => { 114 | const c = getConf(name) 115 | if (c == null) throw new Error(`Expected configuration "${name.toUpperCase().replaceAll('-', '_')}"`) 116 | return c 117 | } 118 | 119 | /** 120 | * @param {string} name 121 | * @return {boolean} 122 | */ 123 | /* c8 ignore next 2 */ 124 | export const hasConf = (name) => 125 | hasParam('--' + name) || getVariable(name) !== null 126 | 127 | /* c8 ignore next */ 128 | export const production = hasConf('production') 129 | 130 | /* c8 ignore next 2 */ 131 | const forceColor = isNode && 132 | f.isOneOf(process.env.FORCE_COLOR, ['true', '1', '2']) 133 | 134 | /* c8 ignore start */ 135 | /** 136 | * Color is enabled by default if the terminal supports it. 137 | * 138 | * Explicitly enable color using `--color` parameter 139 | * Disable color using `--no-color` parameter or using `NO_COLOR=1` environment variable. 140 | * `FORCE_COLOR=1` enables color and takes precedence over all. 141 | */ 142 | export const supportsColor = forceColor || ( 143 | !hasParam('--no-colors') && // @todo deprecate --no-colors 144 | !hasConf('no-color') && 145 | (!isNode || process.stdout.isTTY) && ( 146 | !isNode || 147 | hasParam('--color') || 148 | getVariable('COLORTERM') !== null || 149 | (getVariable('TERM') || '').includes('color') 150 | ) 151 | ) 152 | /* c8 ignore stop */ 153 | -------------------------------------------------------------------------------- /error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error helpers. 3 | * 4 | * @module error 5 | */ 6 | 7 | /** 8 | * @param {string} s 9 | * @return {Error} 10 | */ 11 | /* c8 ignore next */ 12 | export const create = s => new Error(s) 13 | 14 | /** 15 | * @throws {Error} 16 | * @return {never} 17 | */ 18 | /* c8 ignore next 3 */ 19 | export const methodUnimplemented = () => { 20 | throw create('Method unimplemented') 21 | } 22 | 23 | /** 24 | * @throws {Error} 25 | * @return {never} 26 | */ 27 | /* c8 ignore next 3 */ 28 | export const unexpectedCase = () => { 29 | throw create('Unexpected case') 30 | } 31 | -------------------------------------------------------------------------------- /eventloop.js: -------------------------------------------------------------------------------- 1 | /* global requestIdleCallback, requestAnimationFrame, cancelIdleCallback, cancelAnimationFrame */ 2 | 3 | import * as time from './time.js' 4 | 5 | /** 6 | * Utility module to work with EcmaScript's event loop. 7 | * 8 | * @module eventloop 9 | */ 10 | 11 | /** 12 | * @type {Array} 13 | */ 14 | let queue = [] 15 | 16 | const _runQueue = () => { 17 | for (let i = 0; i < queue.length; i++) { 18 | queue[i]() 19 | } 20 | queue = [] 21 | } 22 | 23 | /** 24 | * @param {function():void} f 25 | */ 26 | export const enqueue = f => { 27 | queue.push(f) 28 | if (queue.length === 1) { 29 | setTimeout(_runQueue, 0) 30 | } 31 | } 32 | 33 | /** 34 | * @typedef {Object} TimeoutObject 35 | * @property {function} TimeoutObject.destroy 36 | */ 37 | 38 | /** 39 | * @param {function(number):void} clearFunction 40 | */ 41 | const createTimeoutClass = clearFunction => class TT { 42 | /** 43 | * @param {number} timeoutId 44 | */ 45 | constructor (timeoutId) { 46 | this._ = timeoutId 47 | } 48 | 49 | destroy () { 50 | clearFunction(this._) 51 | } 52 | } 53 | 54 | const Timeout = createTimeoutClass(clearTimeout) 55 | 56 | /** 57 | * @param {number} timeout 58 | * @param {function} callback 59 | * @return {TimeoutObject} 60 | */ 61 | export const timeout = (timeout, callback) => new Timeout(setTimeout(callback, timeout)) 62 | 63 | const Interval = createTimeoutClass(clearInterval) 64 | 65 | /** 66 | * @param {number} timeout 67 | * @param {function} callback 68 | * @return {TimeoutObject} 69 | */ 70 | export const interval = (timeout, callback) => new Interval(setInterval(callback, timeout)) 71 | 72 | /* c8 ignore next */ 73 | export const Animation = createTimeoutClass(arg => typeof requestAnimationFrame !== 'undefined' && cancelAnimationFrame(arg)) 74 | 75 | /** 76 | * @param {function(number):void} cb 77 | * @return {TimeoutObject} 78 | */ 79 | /* c8 ignore next */ 80 | export const animationFrame = cb => typeof requestAnimationFrame === 'undefined' ? timeout(0, cb) : new Animation(requestAnimationFrame(cb)) 81 | 82 | /* c8 ignore next */ 83 | // @ts-ignore 84 | const Idle = createTimeoutClass(arg => typeof cancelIdleCallback !== 'undefined' && cancelIdleCallback(arg)) 85 | 86 | /** 87 | * Note: this is experimental and is probably only useful in browsers. 88 | * 89 | * @param {function} cb 90 | * @return {TimeoutObject} 91 | */ 92 | /* c8 ignore next 2 */ 93 | // @ts-ignore 94 | export const idleCallback = cb => typeof requestIdleCallback !== 'undefined' ? new Idle(requestIdleCallback(cb)) : timeout(1000, cb) 95 | 96 | /** 97 | * @param {number} timeout Timeout of the debounce action 98 | * @param {number} triggerAfter Optional. Trigger callback after a certain amount of time 99 | * without waiting for debounce. 100 | */ 101 | export const createDebouncer = (timeout, triggerAfter = -1) => { 102 | let timer = -1 103 | /** 104 | * @type {number?} 105 | */ 106 | let lastCall = null 107 | /** 108 | * @param {((...args: any)=>void)?} cb function to trigger after debounce. If null, it will reset the 109 | * debounce. 110 | */ 111 | return cb => { 112 | clearTimeout(timer) 113 | if (cb) { 114 | if (triggerAfter >= 0) { 115 | const now = time.getUnixTime() 116 | if (lastCall === null) lastCall = now 117 | if (now - lastCall > triggerAfter) { 118 | lastCall = null 119 | timer = /** @type {any} */ (setTimeout(cb, 0)) 120 | return 121 | } 122 | } 123 | timer = /** @type {any} */ (setTimeout(() => { lastCall = null; cb() }, timeout)) 124 | } else { 125 | lastCall = null 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /eventloop.test.js: -------------------------------------------------------------------------------- 1 | import * as eventloop from './eventloop.js' 2 | import * as t from './testing.js' 3 | import * as promise from './promise.js' 4 | 5 | /** 6 | * @param {t.TestCase} _tc 7 | */ 8 | export const testEventloopOrder = _tc => { 9 | let currI = 0 10 | for (let i = 0; i < 10; i++) { 11 | const bi = i 12 | eventloop.enqueue(() => { 13 | t.assert(currI++ === bi) 14 | }) 15 | } 16 | eventloop.enqueue(() => { 17 | t.assert(currI === 10) 18 | }) 19 | t.assert(currI === 0) 20 | return promise.all([ 21 | promise.createEmpty(resolve => eventloop.enqueue(resolve)), 22 | promise.until(0, () => currI === 10) 23 | ]) 24 | } 25 | 26 | /** 27 | * @param {t.TestCase} _tc 28 | */ 29 | export const testTimeout = async _tc => { 30 | let set = false 31 | const timeout = eventloop.timeout(0, () => { 32 | set = true 33 | }) 34 | timeout.destroy() 35 | await promise.create(resolve => { 36 | eventloop.timeout(10, resolve) 37 | }) 38 | t.assert(set === false) 39 | } 40 | 41 | /** 42 | * @param {t.TestCase} _tc 43 | */ 44 | export const testInterval = async _tc => { 45 | let set = false 46 | const timeout = eventloop.interval(1, () => { 47 | set = true 48 | }) 49 | timeout.destroy() 50 | let i = 0 51 | eventloop.interval(1, () => { 52 | i++ 53 | }) 54 | await promise.until(0, () => i > 2) 55 | t.assert(set === false) 56 | t.assert(i > 1) 57 | } 58 | 59 | /** 60 | * @param {t.TestCase} _tc 61 | */ 62 | export const testAnimationFrame = async _tc => { 63 | let x = false 64 | eventloop.animationFrame(() => { x = true }) 65 | await promise.until(0, () => x) 66 | t.assert(x) 67 | } 68 | 69 | /** 70 | * @param {t.TestCase} _tc 71 | */ 72 | export const testIdleCallback = async _tc => { 73 | await promise.create(resolve => { 74 | eventloop.idleCallback(resolve) 75 | }) 76 | } 77 | 78 | /** 79 | * @param {t.TestCase} _tc 80 | */ 81 | export const testDebouncer = async _tc => { 82 | const debounce = eventloop.createDebouncer(10) 83 | let calls = 0 84 | debounce((_x) => { 85 | calls++ 86 | }) 87 | debounce((_y, _z) => { 88 | calls++ 89 | }) 90 | t.assert(calls === 0) 91 | await promise.wait(20) 92 | t.assert(calls === 1) 93 | } 94 | 95 | /** 96 | * @param {t.TestCase} _tc 97 | */ 98 | export const testDebouncerTriggerAfter = async _tc => { 99 | const debounce = eventloop.createDebouncer(100, 100) 100 | let calls = 0 101 | debounce(() => { 102 | calls++ 103 | }) 104 | await promise.wait(40) 105 | debounce(() => { 106 | calls++ 107 | }) 108 | await promise.wait(30) 109 | debounce(() => { 110 | calls++ 111 | }) 112 | await promise.wait(50) 113 | debounce(() => { 114 | calls++ 115 | }) 116 | t.assert(calls === 0) 117 | await promise.wait(0) 118 | t.assert(calls === 1) 119 | await promise.wait(30) 120 | t.assert(calls === 1) 121 | } 122 | 123 | /** 124 | * @param {t.TestCase} _tc 125 | */ 126 | export const testDebouncerClear = async _tc => { 127 | const debounce = eventloop.createDebouncer(10) 128 | let calls = 0 129 | debounce(() => { 130 | calls++ 131 | }) 132 | await promise.wait(5) 133 | debounce(() => { 134 | calls++ 135 | }) 136 | await promise.wait(5) 137 | debounce(() => { 138 | calls++ 139 | }) 140 | await promise.wait(5) 141 | debounce(() => { 142 | calls++ 143 | }) 144 | await promise.wait(5) 145 | debounce(null) 146 | t.assert(calls === 0) 147 | await promise.wait(30) 148 | t.assert(calls === 0) 149 | } 150 | -------------------------------------------------------------------------------- /function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Common functions and function call helpers. 3 | * 4 | * @module function 5 | */ 6 | 7 | import * as array from './array.js' 8 | import * as object from './object.js' 9 | import * as traits from './traits.js' 10 | 11 | /** 12 | * Calls all functions in `fs` with args. Only throws after all functions were called. 13 | * 14 | * @param {Array} fs 15 | * @param {Array} args 16 | */ 17 | export const callAll = (fs, args, i = 0) => { 18 | try { 19 | for (; i < fs.length; i++) { 20 | fs[i](...args) 21 | } 22 | } finally { 23 | if (i < fs.length) { 24 | callAll(fs, args, i + 1) 25 | } 26 | } 27 | } 28 | 29 | export const nop = () => {} 30 | 31 | /** 32 | * @template T 33 | * @param {function():T} f 34 | * @return {T} 35 | */ 36 | export const apply = f => f() 37 | 38 | /** 39 | * @template A 40 | * 41 | * @param {A} a 42 | * @return {A} 43 | */ 44 | export const id = a => a 45 | 46 | /** 47 | * @template T 48 | * 49 | * @param {T} a 50 | * @param {T} b 51 | * @return {boolean} 52 | */ 53 | export const equalityStrict = (a, b) => a === b 54 | 55 | /** 56 | * @template T 57 | * 58 | * @param {Array|object} a 59 | * @param {Array|object} b 60 | * @return {boolean} 61 | */ 62 | export const equalityFlat = (a, b) => a === b || (a != null && b != null && a.constructor === b.constructor && ((array.isArray(a) && array.equalFlat(a, /** @type {Array} */ (b))) || (typeof a === 'object' && object.equalFlat(a, b)))) 63 | 64 | /* c8 ignore start */ 65 | 66 | /** 67 | * @param {any} a 68 | * @param {any} b 69 | * @return {boolean} 70 | */ 71 | export const equalityDeep = (a, b) => { 72 | if (a === b) { 73 | return true 74 | } 75 | if (a == null || b == null || a.constructor !== b.constructor) { 76 | return false 77 | } 78 | if (a[traits.EqualityTraitSymbol] != null) { 79 | return a[traits.EqualityTraitSymbol](b) 80 | } 81 | switch (a.constructor) { 82 | case ArrayBuffer: 83 | a = new Uint8Array(a) 84 | b = new Uint8Array(b) 85 | // eslint-disable-next-line no-fallthrough 86 | case Uint8Array: { 87 | if (a.byteLength !== b.byteLength) { 88 | return false 89 | } 90 | for (let i = 0; i < a.length; i++) { 91 | if (a[i] !== b[i]) { 92 | return false 93 | } 94 | } 95 | break 96 | } 97 | case Set: { 98 | if (a.size !== b.size) { 99 | return false 100 | } 101 | for (const value of a) { 102 | if (!b.has(value)) { 103 | return false 104 | } 105 | } 106 | break 107 | } 108 | case Map: { 109 | if (a.size !== b.size) { 110 | return false 111 | } 112 | for (const key of a.keys()) { 113 | if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) { 114 | return false 115 | } 116 | } 117 | break 118 | } 119 | case Object: 120 | if (object.length(a) !== object.length(b)) { 121 | return false 122 | } 123 | for (const key in a) { 124 | if (!object.hasProperty(a, key) || !equalityDeep(a[key], b[key])) { 125 | return false 126 | } 127 | } 128 | break 129 | case Array: 130 | if (a.length !== b.length) { 131 | return false 132 | } 133 | for (let i = 0; i < a.length; i++) { 134 | if (!equalityDeep(a[i], b[i])) { 135 | return false 136 | } 137 | } 138 | break 139 | default: 140 | return false 141 | } 142 | return true 143 | } 144 | 145 | /** 146 | * @template V 147 | * @template {V} OPTS 148 | * 149 | * @param {V} value 150 | * @param {Array} options 151 | */ 152 | // @ts-ignore 153 | export const isOneOf = (value, options) => options.includes(value) 154 | /* c8 ignore stop */ 155 | 156 | export const isArray = array.isArray 157 | 158 | /** 159 | * @param {any} s 160 | * @return {s is String} 161 | */ 162 | export const isString = (s) => s && s.constructor === String 163 | 164 | /** 165 | * @param {any} n 166 | * @return {n is Number} 167 | */ 168 | export const isNumber = n => n != null && n.constructor === Number 169 | 170 | /** 171 | * @template {abstract new (...args: any) => any} TYPE 172 | * @param {any} n 173 | * @param {TYPE} T 174 | * @return {n is InstanceType} 175 | */ 176 | export const is = (n, T) => n && n.constructor === T 177 | 178 | /** 179 | * @template {abstract new (...args: any) => any} TYPE 180 | * @param {TYPE} T 181 | */ 182 | export const isTemplate = (T) => 183 | /** 184 | * @param {any} n 185 | * @return {n is InstanceType} 186 | **/ 187 | n => n && n.constructor === T 188 | -------------------------------------------------------------------------------- /function.test.js: -------------------------------------------------------------------------------- 1 | import * as f from './function.js' 2 | import * as t from './testing.js' 3 | 4 | /** 5 | * @param {t.TestCase} _tc 6 | */ 7 | export const testBasics = _tc => { 8 | let calls = 0 9 | f.apply(() => calls++) 10 | t.assert(calls === 1) 11 | t.assert(f.isOneOf(1, [3, 2, 1])) 12 | t.assert(!f.isOneOf(0, [3, 2, 1])) 13 | // test is* 14 | const arr = [1, 'two', [1], { one: 1 }] 15 | arr.forEach(val => { 16 | if (f.isArray(val)) { 17 | /** 18 | * @type {Array} 19 | */ 20 | const yy = val 21 | t.assert(yy) 22 | } 23 | if (f.isString(val)) { 24 | /** 25 | * @type {string} 26 | */ 27 | const yy = val 28 | t.assert(yy) 29 | } 30 | if (f.isNumber(val)) { 31 | /** 32 | * @type {number} 33 | */ 34 | const yy = val 35 | t.assert(yy) 36 | } 37 | if (f.is(val, String)) { 38 | /** 39 | * @type {string} 40 | */ 41 | const yy = val 42 | t.assert(yy) 43 | } 44 | if (f.isTemplate(Number)(val)) { 45 | /** 46 | * @type {number} 47 | */ 48 | const yy = val 49 | t.assert(yy) 50 | } 51 | }) 52 | } 53 | 54 | /** 55 | * @param {t.TestCase} _tc 56 | */ 57 | export const testCallAll = _tc => { 58 | const err = new Error() 59 | let calls = 0 60 | try { 61 | f.callAll([ 62 | () => { 63 | calls++ 64 | }, 65 | () => { 66 | throw err 67 | }, 68 | f.nop, 69 | () => { 70 | calls++ 71 | } 72 | ], []) 73 | } catch (e) { 74 | t.assert(calls === 2) 75 | return 76 | } 77 | t.fail('Expected callAll to throw error') 78 | } 79 | 80 | /** 81 | * @param {t.TestCase} _tc 82 | */ 83 | export const testDeepEquality = _tc => { 84 | t.assert(f.equalityDeep(1, 1)) 85 | t.assert(!f.equalityDeep(1, 2)) 86 | t.assert(!f.equalityDeep(1, '1')) 87 | t.assert(!f.equalityDeep(1, null)) 88 | 89 | const obj = { b: 5 } 90 | const map1 = new Map() 91 | const map2 = new Map() 92 | const map3 = new Map() 93 | const map4 = new Map() 94 | map1.set('a', obj) 95 | map2.set('a', { b: 5 }) 96 | map3.set('b', obj) 97 | map4.set('a', obj) 98 | map4.set('b', obj) 99 | 100 | t.assert(f.equalityDeep({ a: 4 }, { a: 4 })) 101 | t.assert(f.equalityDeep({ a: 4, obj: { b: 5 } }, { a: 4, obj })) 102 | t.assert(!f.equalityDeep({ a: 4 }, { a: 4, obj })) 103 | t.assert(f.equalityDeep({ a: [], obj }, { a: [], obj })) 104 | t.assert(!f.equalityDeep({ a: [], obj }, { a: [], obj: undefined })) 105 | 106 | t.assert(f.equalityDeep({}, {})) 107 | t.assert(!f.equalityDeep({}, { a: 4 })) 108 | 109 | t.assert(f.equalityDeep([{ a: 4 }, 1], [{ a: 4 }, 1])) 110 | t.assert(!f.equalityDeep([{ a: 4 }, 1], [{ a: 4 }, 2])) 111 | t.assert(!f.equalityDeep([{ a: 4 }, 1], [{ a: 4 }, 1, 3])) 112 | t.assert(f.equalityDeep([], [])) 113 | t.assert(!f.equalityDeep([1], [])) 114 | 115 | t.assert(f.equalityDeep(map1, map2)) 116 | t.assert(!f.equalityDeep(map1, map3)) 117 | t.assert(!f.equalityDeep(map1, map4)) 118 | 119 | const set1 = new Set([1]) 120 | const set2 = new Set([true]) 121 | const set3 = new Set([1, true]) 122 | const set4 = new Set([true]) 123 | 124 | t.assert(f.equalityDeep(set2, set4)) 125 | t.assert(!f.equalityDeep(set1, set2)) 126 | t.assert(!f.equalityDeep(set1, set3)) 127 | t.assert(!f.equalityDeep(set1, set4)) 128 | t.assert(!f.equalityDeep(set2, set3)) 129 | t.assert(f.equalityDeep(set2, set4)) 130 | 131 | const buf1 = Uint8Array.from([1, 2]) 132 | const buf2 = Uint8Array.from([1, 3]) 133 | const buf3 = Uint8Array.from([1, 2, 3]) 134 | const buf4 = Uint8Array.from([1, 2]) 135 | 136 | t.assert(!f.equalityDeep(buf1, buf2)) 137 | t.assert(!f.equalityDeep(buf2, buf3)) 138 | t.assert(!f.equalityDeep(buf3, buf4)) 139 | t.assert(f.equalityDeep(buf4, buf1)) 140 | 141 | t.assert(!f.equalityDeep(buf1.buffer, buf2.buffer)) 142 | t.assert(!f.equalityDeep(buf2.buffer, buf3.buffer)) 143 | t.assert(!f.equalityDeep(buf3.buffer, buf4.buffer)) 144 | t.assert(f.equalityDeep(buf4.buffer, buf1.buffer)) 145 | 146 | t.assert(!f.equalityDeep(buf1, buf4.buffer)) 147 | } 148 | -------------------------------------------------------------------------------- /hash/rabin-uncached.js: -------------------------------------------------------------------------------- 1 | /** 2 | * It is not recommended to use this package. This is the uncached implementation of the rabin 3 | * fingerprint algorithm. However, it can be used to verify the `rabin.js` implementation. 4 | * 5 | * @module rabin-uncached 6 | */ 7 | 8 | import * as math from '../math.js' 9 | import * as buffer from '../buffer.js' 10 | 11 | export class RabinUncachedEncoder { 12 | /** 13 | * @param {Uint8Array} m assert(m[0] === 1) 14 | */ 15 | constructor (m) { 16 | this.m = m 17 | this.blen = m.byteLength 18 | this.bs = new Uint8Array(this.blen) 19 | /** 20 | * This describes the position of the most significant byte (starts with 0 and increases with 21 | * shift) 22 | */ 23 | this.bpos = 0 24 | } 25 | 26 | /** 27 | * Add/Xor/Substract bytes. 28 | * 29 | * Discards bytes that are out of range. 30 | * @todo put this in function or inline 31 | * 32 | * @param {Uint8Array} cs 33 | */ 34 | add (cs) { 35 | const copyLen = math.min(this.blen, cs.byteLength) 36 | // copy from right to left until max is reached 37 | for (let i = 0; i < copyLen; i++) { 38 | this.bs[(this.bpos + this.blen - i - 1) % this.blen] ^= cs[cs.byteLength - i - 1] 39 | } 40 | } 41 | 42 | /** 43 | * @param {number} byte 44 | */ 45 | write (byte) { 46 | // [0,m1,m2,b] 47 | // x <- bpos 48 | // Shift one byte to the left, add b 49 | this.bs[this.bpos] = byte 50 | this.bpos = (this.bpos + 1) % this.blen 51 | // mod 52 | for (let i = 7; i >= 0; i--) { 53 | if (((this.bs[this.bpos] >>> i) & 1) === 1) { 54 | this.add(buffer.shiftNBitsLeft(this.m, i)) 55 | } 56 | } 57 | // if (this.bs[this.bpos] !== 0) { error.unexpectedCase() } 58 | // assert(this.bs[this.bpos] === 0) 59 | } 60 | 61 | getFingerprint () { 62 | const result = new Uint8Array(this.blen - 1) 63 | for (let i = 0; i < result.byteLength; i++) { 64 | result[i] = this.bs[(this.bpos + i + 1) % this.blen] 65 | } 66 | return result 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /hash/rabin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module rabin 3 | * 4 | * Very efficient & versatile fingerprint/hashing algorithm. However, it is not cryptographically 5 | * secure. Well suited for fingerprinting. 6 | */ 7 | 8 | import * as buffer from '../buffer.js' 9 | import * as map from '../map.js' 10 | 11 | export const StandardIrreducible8 = new Uint8Array([1, 221]) 12 | export const StandardIrreducible16 = new Uint8Array([1, 244, 157]) 13 | export const StandardIrreducible32 = new Uint8Array([1, 149, 183, 205, 191]) 14 | export const StandardIrreducible64 = new Uint8Array([1, 133, 250, 114, 193, 250, 28, 193, 231]) 15 | export const StandardIrreducible128 = new Uint8Array([1, 94, 109, 166, 228, 6, 222, 102, 239, 27, 128, 184, 13, 50, 112, 169, 199]) 16 | 17 | /** 18 | * Maps from a modulo to the precomputed values. 19 | * 20 | * @type {Map} 21 | */ 22 | const _precomputedFingerprintCache = new Map() 23 | 24 | /** 25 | * @param {Uint8Array} m 26 | */ 27 | const ensureCache = m => map.setIfUndefined(_precomputedFingerprintCache, buffer.toBase64(m), () => { 28 | const byteLen = m.byteLength 29 | const cache = new Uint8Array(256 * byteLen) 30 | // Use dynamic computing to compute the cached results. 31 | // Starting values: cache(0) = 0; cache(1) = m 32 | cache.set(m, byteLen) 33 | for (let bit = 1; bit < 8; bit++) { 34 | const mBitShifted = buffer.shiftNBitsLeft(m, bit) 35 | const bitShifted = 1 << bit 36 | for (let j = 0; j < bitShifted; j++) { 37 | // apply the shifted result (reducing the degree of the polynomial) 38 | const msb = bitShifted | j 39 | const rest = msb ^ mBitShifted[0] 40 | for (let i = 0; i < byteLen; i++) { 41 | // rest is already precomputed in the cache 42 | cache[msb * byteLen + i] = cache[rest * byteLen + i] ^ mBitShifted[i] 43 | } 44 | // if (cache[(bitShifted | j) * byteLen] !== (bitShifted | j)) { error.unexpectedCase() } 45 | } 46 | } 47 | return cache 48 | }) 49 | 50 | export class RabinEncoder { 51 | /** 52 | * @param {Uint8Array} m assert(m[0] === 1) 53 | */ 54 | constructor (m) { 55 | this.m = m 56 | this.blen = m.byteLength 57 | this.bs = new Uint8Array(this.blen) 58 | this.cache = ensureCache(m) 59 | /** 60 | * This describes the position of the most significant byte (starts with 0 and increases with 61 | * shift) 62 | */ 63 | this.bpos = 0 64 | } 65 | 66 | /** 67 | * @param {number} byte 68 | */ 69 | write (byte) { 70 | // assert(this.bs[0] === 0) 71 | // Shift one byte to the left, add b 72 | this.bs[this.bpos] = byte 73 | this.bpos = (this.bpos + 1) % this.blen 74 | const msb = this.bs[this.bpos] 75 | for (let i = 0; i < this.blen; i++) { 76 | this.bs[(this.bpos + i) % this.blen] ^= this.cache[msb * this.blen + i] 77 | } 78 | // assert(this.bs[this.bpos] === 0) 79 | } 80 | 81 | getFingerprint () { 82 | const result = new Uint8Array(this.blen - 1) 83 | for (let i = 0; i < result.byteLength; i++) { 84 | result[i] = this.bs[(this.bpos + i + 1) % this.blen] 85 | } 86 | return result 87 | } 88 | } 89 | 90 | /** 91 | * @param {Uint8Array} irreducible 92 | * @param {Uint8Array} data 93 | */ 94 | export const fingerprint = (irreducible, data) => { 95 | const encoder = new RabinEncoder(irreducible) 96 | for (let i = 0; i < data.length; i++) { 97 | encoder.write(data[i]) 98 | } 99 | return encoder.getFingerprint() 100 | } 101 | -------------------------------------------------------------------------------- /hash/sha256.node.js: -------------------------------------------------------------------------------- 1 | import { createHash } from 'node:crypto' 2 | 3 | /** 4 | * @param {Uint8Array} data 5 | */ 6 | export const digest = data => { 7 | const hasher = createHash('sha256') 8 | hasher.update(data) 9 | return hasher.digest() 10 | } 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Experimental method to import lib0. 3 | * 4 | * Not recommended if the module bundler doesn't support dead code elimination. 5 | * 6 | * @module lib0 7 | */ 8 | 9 | import * as array from './array.js' 10 | import * as binary from './binary.js' 11 | import * as broadcastchannel from './broadcastchannel.js' 12 | import * as buffer from './buffer.js' 13 | import * as conditions from './conditions.js' 14 | import * as decoding from './decoding.js' 15 | import * as diff from './diff.js' 16 | import * as dom from './dom.js' 17 | import * as encoding from './encoding.js' 18 | import * as environment from './environment.js' 19 | import * as error from './error.js' 20 | import * as eventloop from './eventloop.js' 21 | // @todo rename file to func 22 | import * as func from './function.js' 23 | import * as indexeddb from './indexeddb.js' 24 | import * as iterator from './iterator.js' 25 | import * as json from './json.js' 26 | import * as logging from 'lib0/logging' 27 | import * as map from './map.js' 28 | import * as math from './math.js' 29 | import * as mutex from './mutex.js' 30 | import * as number from './number.js' 31 | import * as object from './object.js' 32 | import * as pair from './pair.js' 33 | import * as prng from './prng.js' 34 | import * as promise from './promise.js' 35 | // import * as random from './random.js' 36 | import * as set from './set.js' 37 | import * as sort from './sort.js' 38 | import * as statistics from './statistics.js' 39 | import * as string from './string.js' 40 | import * as symbol from './symbol.js' 41 | // import * as testing from './testing.js' 42 | import * as time from './time.js' 43 | import * as tree from './tree.js' 44 | import * as websocket from './websocket.js' 45 | 46 | export { 47 | array, 48 | binary, 49 | broadcastchannel, 50 | buffer, 51 | conditions, 52 | decoding, 53 | diff, 54 | dom, 55 | encoding, 56 | environment, 57 | error, 58 | eventloop, 59 | func, 60 | indexeddb, 61 | iterator, 62 | json, 63 | logging, 64 | map, 65 | math, 66 | mutex, 67 | number, 68 | object, 69 | pair, 70 | prng, 71 | promise, 72 | // random, 73 | set, 74 | sort, 75 | statistics, 76 | string, 77 | symbol, 78 | // testing, 79 | time, 80 | tree, 81 | websocket 82 | } 83 | -------------------------------------------------------------------------------- /indexeddb.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as idb from './indexeddb.js' 3 | import { isBrowser } from './environment.js' 4 | 5 | /* c8 ignore next */ 6 | /** 7 | * @param {IDBDatabase} db 8 | */ 9 | const initTestDB = db => idb.createStores(db, [['test', { autoIncrement: true }]]) 10 | const testDBName = 'idb-test' 11 | 12 | /* c8 ignore next */ 13 | /** 14 | * @param {IDBDatabase} db 15 | */ 16 | const createTransaction = db => db.transaction(['test'], 'readwrite') 17 | 18 | /* c8 ignore next */ 19 | /** 20 | * @param {IDBTransaction} t 21 | * @return {IDBObjectStore} 22 | */ 23 | const getStore = t => idb.getStore(t, 'test') 24 | 25 | /* c8 ignore next */ 26 | export const testRetrieveElements = async () => { 27 | t.skip(!isBrowser) 28 | t.describe('create, then iterate some keys') 29 | await idb.deleteDB(testDBName) 30 | const db = await idb.openDB(testDBName, initTestDB) 31 | const transaction = createTransaction(db) 32 | const store = getStore(transaction) 33 | await idb.put(store, 0, ['t', 1]) 34 | await idb.put(store, 1, ['t', 2]) 35 | const expectedKeys = [['t', 1], ['t', 2]] 36 | const expectedVals = [0, 1] 37 | const expectedKeysVals = [{ v: 0, k: ['t', 1] }, { v: 1, k: ['t', 2] }] 38 | t.describe('idb.getAll') 39 | const valsGetAll = await idb.getAll(store) 40 | t.compare(valsGetAll, expectedVals) 41 | t.describe('idb.getAllKeys') 42 | const valsGetAllKeys = await idb.getAllKeys(store) 43 | t.compare(valsGetAllKeys, expectedKeys) 44 | t.describe('idb.getAllKeysVals') 45 | const valsGetAllKeysVals = await idb.getAllKeysValues(store) 46 | t.compare(valsGetAllKeysVals, expectedKeysVals) 47 | 48 | /** 49 | * @param {string} desc 50 | * @param {IDBKeyRange?} keyrange 51 | */ 52 | const iterateTests = async (desc, keyrange) => { 53 | t.describe(`idb.iterate (${desc})`) 54 | /** 55 | * @type {Array<{v:any,k:any}>} 56 | */ 57 | const valsIterate = [] 58 | await idb.iterate(store, keyrange, (v, k) => { 59 | valsIterate.push({ v, k }) 60 | }) 61 | t.compare(valsIterate, expectedKeysVals) 62 | t.describe(`idb.iterateKeys (${desc})`) 63 | /** 64 | * @type {Array} 65 | */ 66 | const keysIterate = [] 67 | await idb.iterateKeys(store, keyrange, key => { 68 | keysIterate.push(key) 69 | }) 70 | t.compare(keysIterate, expectedKeys) 71 | } 72 | await iterateTests('range=null', null) 73 | const range = idb.createIDBKeyRangeBound(['t', 1], ['t', 2], false, false) 74 | // adding more items that should not be touched by iteration with above range 75 | await idb.put(store, 2, ['t', 3]) 76 | await idb.put(store, 2, ['t', 0]) 77 | await iterateTests('range!=null', range) 78 | 79 | t.describe('idb.get') 80 | const getV = await idb.get(store, ['t', 1]) 81 | t.assert(getV === 0) 82 | t.describe('idb.del') 83 | await idb.del(store, ['t', 0]) 84 | const getVDel = await idb.get(store, ['t', 0]) 85 | t.assert(getVDel === undefined) 86 | t.describe('idb.add') 87 | await idb.add(store, 99, 42) 88 | const idbVAdd = await idb.get(store, 42) 89 | t.assert(idbVAdd === 99) 90 | t.describe('idb.addAutoKey') 91 | const key = await idb.addAutoKey(store, 1234) 92 | const retrieved = await idb.get(store, key) 93 | t.assert(retrieved === 1234) 94 | } 95 | 96 | /* c8 ignore next */ 97 | export const testBlocked = async () => { 98 | t.skip(!isBrowser) 99 | t.describe('ignore blocked event') 100 | await idb.deleteDB(testDBName) 101 | const db = await idb.openDB(testDBName, initTestDB) 102 | const transaction = createTransaction(db) 103 | const store = getStore(transaction) 104 | await idb.put(store, 0, ['t', 1]) 105 | await idb.put(store, 1, ['t', 2]) 106 | db.close() 107 | await idb.deleteDB(testDBName) 108 | } 109 | -------------------------------------------------------------------------------- /indexeddbV2.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as idb from './indexeddbV2.js' 3 | import * as pledge from './pledge.js' 4 | import { isBrowser } from './environment.js' 5 | 6 | /* c8 ignore next */ 7 | /** 8 | * @param {IDBDatabase} db 9 | */ 10 | const initTestDB = db => idb.createStores(db, [['test', { autoIncrement: true }]]) 11 | const testDBName = 'idb-test' 12 | 13 | /* c8 ignore next */ 14 | /** 15 | * @param {pledge.Pledge} db 16 | */ 17 | const createTransaction = db => pledge.createWithDependencies((p, db) => p.resolve(db.transaction(['test'], 'readwrite')), db) 18 | 19 | /* c8 ignore next */ 20 | /** 21 | * @param {pledge.Pledge} t 22 | * @return {pledge.PledgeInstance} 23 | */ 24 | const getStore = t => pledge.createWithDependencies((p, t) => p.resolve(idb.getStore(t, 'test')), t) 25 | 26 | /* c8 ignore next */ 27 | export const testRetrieveElements = async () => { 28 | t.skip(!isBrowser) 29 | t.describe('create, then iterate some keys') 30 | await idb.deleteDB(testDBName).promise() 31 | const db = idb.openDB(testDBName, initTestDB) 32 | const transaction = createTransaction(db) 33 | const store = getStore(transaction) 34 | await idb.put(store, 0, ['t', 1]).promise() 35 | await idb.put(store, 1, ['t', 2]).promise() 36 | const expectedKeys = [['t', 1], ['t', 2]] 37 | const expectedVals = [0, 1] 38 | const expectedKeysVals = [{ v: 0, k: ['t', 1] }, { v: 1, k: ['t', 2] }] 39 | t.describe('idb.getAll') 40 | const valsGetAll = await idb.getAll(store).promise() 41 | t.compare(valsGetAll, expectedVals) 42 | t.describe('idb.getAllKeys') 43 | const valsGetAllKeys = await idb.getAllKeys(store).promise() 44 | t.compare(valsGetAllKeys, expectedKeys) 45 | t.describe('idb.getAllKeysVals') 46 | const valsGetAllKeysVals = await idb.getAllKeysValues(store).promise() 47 | t.compare(valsGetAllKeysVals, expectedKeysVals) 48 | 49 | /** 50 | * @param {string} desc 51 | * @param {IDBKeyRange?} keyrange 52 | */ 53 | const iterateTests = async (desc, keyrange) => { 54 | t.describe(`idb.iterate (${desc})`) 55 | /** 56 | * @type {Array<{v:any,k:any}>} 57 | */ 58 | const valsIterate = [] 59 | await idb.iterate(store, keyrange, (v, k) => { 60 | valsIterate.push({ v, k }) 61 | }).promise() 62 | t.compare(valsIterate, expectedKeysVals) 63 | t.describe(`idb.iterateKeys (${desc})`) 64 | /** 65 | * @type {Array} 66 | */ 67 | const keysIterate = [] 68 | await idb.iterateKeys(store, keyrange, key => { 69 | keysIterate.push(key) 70 | }).promise() 71 | t.compare(keysIterate, expectedKeys) 72 | } 73 | await iterateTests('range=null', null) 74 | const range = idb.createIDBKeyRangeBound(['t', 1], ['t', 2], false, false) 75 | // adding more items that should not be touched by iteration with above range 76 | await idb.put(store, 2, ['t', 3]).promise() 77 | await idb.put(store, 2, ['t', 0]).promise() 78 | await iterateTests('range!=null', range) 79 | 80 | t.describe('idb.get') 81 | const getV = await idb.get(store, ['t', 1]).promise() 82 | t.assert(getV === 0) 83 | t.describe('idb.del') 84 | await idb.del(store, ['t', 0]).promise() 85 | const getVDel = await idb.get(store, ['t', 0]).promise() 86 | t.assert(getVDel === undefined) 87 | t.describe('idb.add') 88 | await idb.add(store, 99, 42).promise() 89 | const idbVAdd = await idb.get(store, 42).promise() 90 | t.assert(idbVAdd === 99) 91 | t.describe('idb.addAutoKey') 92 | const key = await idb.addAutoKey(store, 1234).promise() 93 | const retrieved = await idb.get(store, key).promise() 94 | t.assert(retrieved === 1234) 95 | } 96 | 97 | /* c8 ignore next */ 98 | export const testBlocked = async () => { 99 | t.skip(!isBrowser) 100 | t.describe('ignore blocked event') 101 | await idb.deleteDB(testDBName).map(() => { 102 | const db = idb.openDB(testDBName, initTestDB) 103 | const transaction = createTransaction(db) 104 | const store = getStore(transaction) 105 | return pledge.all({ 106 | _req1: idb.put(store, 0, ['t', 1]), 107 | _req2: idb.put(store, 1, ['t', 2]), 108 | db 109 | }) 110 | }).map(({ db }) => { 111 | db.close() 112 | return idb.deleteDB(testDBName) 113 | }).promise() 114 | } 115 | -------------------------------------------------------------------------------- /isomorphic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Isomorphic library exports from isomorphic.js. 3 | * 4 | * @todo remove this module 5 | * @deprecated 6 | * 7 | * @module isomorphic 8 | */ 9 | 10 | // @todo remove this module 11 | 12 | // @ts-ignore 13 | export { performance, cryptoRandomBuffer } from 'isomorphic.js' 14 | -------------------------------------------------------------------------------- /iterator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility module to create and manipulate Iterators. 3 | * 4 | * @module iterator 5 | */ 6 | 7 | /** 8 | * @template T,R 9 | * @param {Iterator} iterator 10 | * @param {function(T):R} f 11 | * @return {IterableIterator} 12 | */ 13 | export const mapIterator = (iterator, f) => ({ 14 | [Symbol.iterator] () { 15 | return this 16 | }, 17 | // @ts-ignore 18 | next () { 19 | const r = iterator.next() 20 | return { value: r.done ? undefined : f(r.value), done: r.done } 21 | } 22 | }) 23 | 24 | /** 25 | * @template T 26 | * @param {function():IteratorResult} next 27 | * @return {IterableIterator} 28 | */ 29 | export const createIterator = next => ({ 30 | /** 31 | * @return {IterableIterator} 32 | */ 33 | [Symbol.iterator] () { 34 | return this 35 | }, 36 | // @ts-ignore 37 | next 38 | }) 39 | 40 | /** 41 | * @template T 42 | * @param {Iterator} iterator 43 | * @param {function(T):boolean} filter 44 | */ 45 | export const iteratorFilter = (iterator, filter) => createIterator(() => { 46 | let res 47 | do { 48 | res = iterator.next() 49 | } while (!res.done && !filter(res.value)) 50 | return res 51 | }) 52 | 53 | /** 54 | * @template T,M 55 | * @param {Iterator} iterator 56 | * @param {function(T):M} fmap 57 | */ 58 | export const iteratorMap = (iterator, fmap) => createIterator(() => { 59 | const { done, value } = iterator.next() 60 | return { done, value: done ? undefined : fmap(value) } 61 | }) 62 | -------------------------------------------------------------------------------- /json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON utility functions. 3 | * 4 | * @module json 5 | */ 6 | 7 | /** 8 | * Transform JavaScript object to JSON. 9 | * 10 | * @param {any} object 11 | * @return {string} 12 | */ 13 | export const stringify = JSON.stringify 14 | 15 | /** 16 | * Parse JSON object. 17 | * 18 | * @param {string} json 19 | * @return {any} 20 | */ 21 | export const parse = JSON.parse 22 | -------------------------------------------------------------------------------- /list.js: -------------------------------------------------------------------------------- 1 | import { id } from './function.js' 2 | import * as error from './error.js' 3 | 4 | export class ListNode { 5 | constructor () { 6 | /** 7 | * @type {this|null} 8 | */ 9 | this.next = null 10 | /** 11 | * @type {this|null} 12 | */ 13 | this.prev = null 14 | } 15 | } 16 | 17 | /** 18 | * @template {ListNode} N 19 | */ 20 | export class List { 21 | constructor () { 22 | /** 23 | * @type {N | null} 24 | */ 25 | this.start = null 26 | /** 27 | * @type {N | null} 28 | */ 29 | this.end = null 30 | this.len = 0 31 | } 32 | } 33 | 34 | /** 35 | * @note The queue implementation is experimental and unfinished. 36 | * Don't use this in production yet. 37 | * 38 | * @template {ListNode} N 39 | * 40 | * @return {List} 41 | */ 42 | export const create = () => new List() 43 | 44 | /** 45 | * @template {ListNode} N 46 | * 47 | * @param {List} queue 48 | */ 49 | export const isEmpty = queue => queue.start === null 50 | 51 | /** 52 | * Remove a single node from the queue. Only works with Queues that operate on Doubly-linked lists of nodes. 53 | * 54 | * @template {ListNode} N 55 | * 56 | * @param {List} queue 57 | * @param {N} node 58 | */ 59 | export const remove = (queue, node) => { 60 | const prev = node.prev 61 | const next = node.next 62 | if (prev) { 63 | prev.next = next 64 | } else { 65 | queue.start = next 66 | } 67 | if (next) { 68 | next.prev = prev 69 | } else { 70 | queue.end = prev 71 | } 72 | queue.len-- 73 | return node 74 | } 75 | 76 | /** 77 | * @deprecated @todo remove in next major release 78 | */ 79 | export const removeNode = remove 80 | 81 | /** 82 | * @template {ListNode} N 83 | * 84 | * @param {List} queue 85 | * @param {N| null} left 86 | * @param {N| null} right 87 | * @param {N} node 88 | */ 89 | export const insertBetween = (queue, left, right, node) => { 90 | /* c8 ignore start */ 91 | if (left != null && left.next !== right) { 92 | throw error.unexpectedCase() 93 | } 94 | /* c8 ignore stop */ 95 | if (left) { 96 | left.next = node 97 | } else { 98 | queue.start = node 99 | } 100 | if (right) { 101 | right.prev = node 102 | } else { 103 | queue.end = node 104 | } 105 | node.prev = left 106 | node.next = right 107 | queue.len++ 108 | } 109 | 110 | /** 111 | * Remove a single node from the queue. Only works with Queues that operate on Doubly-linked lists of nodes. 112 | * 113 | * @template {ListNode} N 114 | * 115 | * @param {List} queue 116 | * @param {N} node 117 | * @param {N} newNode 118 | */ 119 | export const replace = (queue, node, newNode) => { 120 | insertBetween(queue, node, node.next, newNode) 121 | remove(queue, node) 122 | } 123 | 124 | /** 125 | * @template {ListNode} N 126 | * 127 | * @param {List} queue 128 | * @param {N} n 129 | */ 130 | export const pushEnd = (queue, n) => 131 | insertBetween(queue, queue.end, null, n) 132 | 133 | /** 134 | * @template {ListNode} N 135 | * 136 | * @param {List} queue 137 | * @param {N} n 138 | */ 139 | export const pushFront = (queue, n) => 140 | insertBetween(queue, null, queue.start, n) 141 | 142 | /** 143 | * @template {ListNode} N 144 | * 145 | * @param {List} list 146 | * @return {N| null} 147 | */ 148 | export const popFront = list => 149 | list.start ? removeNode(list, list.start) : null 150 | 151 | /** 152 | * @template {ListNode} N 153 | * 154 | * @param {List} list 155 | * @return {N| null} 156 | */ 157 | export const popEnd = list => 158 | list.end ? removeNode(list, list.end) : null 159 | 160 | /** 161 | * @template {ListNode} N 162 | * @template M 163 | * 164 | * @param {List} list 165 | * @param {function(N):M} f 166 | * @return {Array} 167 | */ 168 | export const map = (list, f) => { 169 | /** 170 | * @type {Array} 171 | */ 172 | const arr = [] 173 | let n = list.start 174 | while (n) { 175 | arr.push(f(n)) 176 | n = n.next 177 | } 178 | return arr 179 | } 180 | 181 | /** 182 | * @template {ListNode} N 183 | * 184 | * @param {List} list 185 | */ 186 | export const toArray = list => map(list, id) 187 | 188 | /** 189 | * @template {ListNode} N 190 | * @template M 191 | * 192 | * @param {List} list 193 | * @param {function(N):M} f 194 | */ 195 | export const forEach = (list, f) => { 196 | let n = list.start 197 | while (n) { 198 | f(n) 199 | n = n.next 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /list.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as list from './list.js' 3 | 4 | class QueueItem extends list.ListNode { 5 | /** 6 | * @param {number} v 7 | */ 8 | constructor (v) { 9 | super() 10 | this.v = v 11 | } 12 | } 13 | 14 | /** 15 | * @param {t.TestCase} _tc 16 | */ 17 | export const testEnqueueDequeue = _tc => { 18 | const N = 30 19 | /** 20 | * @type {list.List} 21 | */ 22 | const q = list.create() 23 | t.assert(list.isEmpty(q)) 24 | t.assert(list.popFront(q) === null) 25 | for (let i = 0; i < N; i++) { 26 | list.pushEnd(q, new QueueItem(i)) 27 | t.assert(!list.isEmpty(q)) 28 | } 29 | for (let i = 0; i < N; i++) { 30 | const item = /** @type {QueueItem} */ (list.popFront(q)) 31 | t.assert(item !== null && item.v === i) 32 | } 33 | t.assert(list.isEmpty(q)) 34 | t.assert(list.popFront(q) === null) 35 | for (let i = 0; i < N; i++) { 36 | list.pushEnd(q, new QueueItem(i)) 37 | t.assert(!list.isEmpty(q)) 38 | } 39 | for (let i = 0; i < N; i++) { 40 | const item = /** @type {QueueItem} */ (list.popFront(q)) 41 | t.assert(item !== null && item.v === i) 42 | } 43 | t.assert(list.isEmpty(q)) 44 | t.assert(list.popFront(q) === null) 45 | } 46 | 47 | /** 48 | * @param {t.TestCase} _tc 49 | */ 50 | export const testSelectivePop = _tc => { 51 | /** 52 | * @type {list.List} 53 | */ 54 | const l = list.create() 55 | list.pushFront(l, new QueueItem(1)) 56 | const q3 = new QueueItem(3) 57 | list.pushEnd(l, q3) 58 | const middleNode = new QueueItem(2) 59 | list.insertBetween(l, l.start, l.end, middleNode) 60 | list.replace(l, q3, new QueueItem(4)) 61 | t.compare(list.map(l, n => n.v), [1, 2, 4]) 62 | t.compare(list.toArray(l).map(n => n.v), [1, 2, 4]) 63 | { 64 | let cnt = 0 65 | list.forEach(l, () => cnt++) 66 | t.assert(cnt === l.len) 67 | } 68 | t.assert(l.len === 3) 69 | t.assert(list.remove(l, middleNode) === middleNode) 70 | t.assert(l.len === 2) 71 | t.compare(/** @type {QueueItem} */ (list.popEnd(l)).v, 4) 72 | t.assert(l.len === 1) 73 | t.compare(/** @type {QueueItem} */ (list.popEnd(l)).v, 1) 74 | t.assert(l.len === 0) 75 | t.compare(list.popEnd(l), null) 76 | t.assert(l.start === null) 77 | t.assert(l.end === null) 78 | t.assert(l.len === 0) 79 | } 80 | -------------------------------------------------------------------------------- /logging.common.js: -------------------------------------------------------------------------------- 1 | import * as symbol from './symbol.js' 2 | import * as time from './time.js' 3 | import * as env from './environment.js' 4 | import * as func from './function.js' 5 | import * as json from './json.js' 6 | 7 | export const BOLD = symbol.create() 8 | export const UNBOLD = symbol.create() 9 | export const BLUE = symbol.create() 10 | export const GREY = symbol.create() 11 | export const GREEN = symbol.create() 12 | export const RED = symbol.create() 13 | export const PURPLE = symbol.create() 14 | export const ORANGE = symbol.create() 15 | export const UNCOLOR = symbol.create() 16 | 17 | /* c8 ignore start */ 18 | /** 19 | * @param {Array} args 20 | * @return {Array} 21 | */ 22 | export const computeNoColorLoggingArgs = args => { 23 | if (args.length === 1 && args[0]?.constructor === Function) { 24 | args = /** @type {Array} */ (/** @type {[function]} */ (args)[0]()) 25 | } 26 | const strBuilder = [] 27 | const logArgs = [] 28 | // try with formatting until we find something unsupported 29 | let i = 0 30 | for (; i < args.length; i++) { 31 | const arg = args[i] 32 | if (arg === undefined) { 33 | break 34 | } else if (arg.constructor === String || arg.constructor === Number) { 35 | strBuilder.push(arg) 36 | } else if (arg.constructor === Object) { 37 | break 38 | } 39 | } 40 | if (i > 0) { 41 | // create logArgs with what we have so far 42 | logArgs.push(strBuilder.join('')) 43 | } 44 | // append the rest 45 | for (; i < args.length; i++) { 46 | const arg = args[i] 47 | if (!(arg instanceof Symbol)) { 48 | logArgs.push(arg) 49 | } 50 | } 51 | return logArgs 52 | } 53 | /* c8 ignore stop */ 54 | 55 | const loggingColors = [GREEN, PURPLE, ORANGE, BLUE] 56 | let nextColor = 0 57 | let lastLoggingTime = time.getUnixTime() 58 | 59 | /* c8 ignore start */ 60 | /** 61 | * @param {function(...any):void} _print 62 | * @param {string} moduleName 63 | * @return {function(...any):void} 64 | */ 65 | export const createModuleLogger = (_print, moduleName) => { 66 | const color = loggingColors[nextColor] 67 | const debugRegexVar = env.getVariable('log') 68 | const doLogging = debugRegexVar !== null && 69 | (debugRegexVar === '*' || debugRegexVar === 'true' || 70 | new RegExp(debugRegexVar, 'gi').test(moduleName)) 71 | nextColor = (nextColor + 1) % loggingColors.length 72 | moduleName += ': ' 73 | return !doLogging 74 | ? func.nop 75 | : (...args) => { 76 | if (args.length === 1 && args[0]?.constructor === Function) { 77 | args = args[0]() 78 | } 79 | const timeNow = time.getUnixTime() 80 | const timeDiff = timeNow - lastLoggingTime 81 | lastLoggingTime = timeNow 82 | _print( 83 | color, 84 | moduleName, 85 | UNCOLOR, 86 | ...args.map((arg) => { 87 | if (arg != null && arg.constructor === Uint8Array) { 88 | arg = Array.from(arg) 89 | } 90 | const t = typeof arg 91 | switch (t) { 92 | case 'string': 93 | case 'symbol': 94 | return arg 95 | default: { 96 | return json.stringify(arg) 97 | } 98 | } 99 | }), 100 | color, 101 | ' +' + timeDiff + 'ms' 102 | ) 103 | } 104 | } 105 | /* c8 ignore stop */ 106 | -------------------------------------------------------------------------------- /logging.node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Isomorphic logging module with support for colors! 3 | * 4 | * @module logging 5 | */ 6 | 7 | import * as env from './environment.js' 8 | import * as common from './logging.common.js' 9 | 10 | export { BOLD, UNBOLD, BLUE, GREY, GREEN, RED, PURPLE, ORANGE, UNCOLOR } from './logging.common.js' 11 | 12 | const _nodeStyleMap = { 13 | [common.BOLD]: '\u001b[1m', 14 | [common.UNBOLD]: '\u001b[2m', 15 | [common.BLUE]: '\x1b[34m', 16 | [common.GREEN]: '\x1b[32m', 17 | [common.GREY]: '\u001b[37m', 18 | [common.RED]: '\x1b[31m', 19 | [common.PURPLE]: '\x1b[35m', 20 | [common.ORANGE]: '\x1b[38;5;208m', 21 | [common.UNCOLOR]: '\x1b[0m' 22 | } 23 | 24 | /* c8 ignore start */ 25 | /** 26 | * @param {Array>} args 27 | * @return {Array} 28 | */ 29 | const computeNodeLoggingArgs = (args) => { 30 | if (args.length === 1 && args[0]?.constructor === Function) { 31 | args = /** @type {Array} */ (/** @type {[function]} */ (args)[0]()) 32 | } 33 | const strBuilder = [] 34 | const logArgs = [] 35 | // try with formatting until we find something unsupported 36 | let i = 0 37 | for (; i < args.length; i++) { 38 | const arg = args[i] 39 | // @ts-ignore 40 | const style = _nodeStyleMap[arg] 41 | if (style !== undefined) { 42 | strBuilder.push(style) 43 | } else { 44 | if (arg === undefined) { 45 | break 46 | } else if (arg.constructor === String || arg.constructor === Number) { 47 | strBuilder.push(arg) 48 | } else { 49 | break 50 | } 51 | } 52 | } 53 | if (i > 0) { 54 | // create logArgs with what we have so far 55 | strBuilder.push('\x1b[0m') 56 | logArgs.push(strBuilder.join('')) 57 | } 58 | // append the rest 59 | for (; i < args.length; i++) { 60 | const arg = args[i] 61 | if (!(arg instanceof Symbol)) { 62 | logArgs.push(arg) 63 | } 64 | } 65 | return logArgs 66 | } 67 | /* c8 ignore stop */ 68 | 69 | /* c8 ignore start */ 70 | const computeLoggingArgs = env.supportsColor 71 | ? computeNodeLoggingArgs 72 | : common.computeNoColorLoggingArgs 73 | /* c8 ignore stop */ 74 | 75 | /** 76 | * @param {Array} args 77 | */ 78 | export const print = (...args) => { 79 | console.log(...computeLoggingArgs(args)) 80 | } 81 | 82 | /* c8 ignore start */ 83 | /** 84 | * @param {Array} args 85 | */ 86 | export const warn = (...args) => { 87 | console.warn(...computeLoggingArgs(args)) 88 | } 89 | /* c8 ignore stop */ 90 | 91 | /** 92 | * @param {Error} err 93 | */ 94 | /* c8 ignore start */ 95 | export const printError = (err) => { 96 | console.error(err) 97 | } 98 | /* c8 ignore stop */ 99 | 100 | /** 101 | * @param {string} _url image location 102 | * @param {number} _height height of the image in pixel 103 | */ 104 | /* c8 ignore start */ 105 | export const printImg = (_url, _height) => { 106 | // console.log('%c ', `font-size: ${height}x; background: url(${url}) no-repeat;`) 107 | } 108 | /* c8 ignore stop */ 109 | 110 | /** 111 | * @param {string} base64 112 | * @param {number} height 113 | */ 114 | /* c8 ignore next 2 */ 115 | export const printImgBase64 = (base64, height) => 116 | printImg(`data:image/gif;base64,${base64}`, height) 117 | 118 | /** 119 | * @param {Array} args 120 | */ 121 | /* c8 ignore next 3 */ 122 | export const group = (...args) => { 123 | console.group(...computeLoggingArgs(args)) 124 | } 125 | 126 | /** 127 | * @param {Array} args 128 | */ 129 | /* c8 ignore next 3 */ 130 | export const groupCollapsed = (...args) => { 131 | console.groupCollapsed(...computeLoggingArgs(args)) 132 | } 133 | 134 | /* c8 ignore next 3 */ 135 | export const groupEnd = () => { 136 | console.groupEnd() 137 | } 138 | 139 | /** 140 | * @param {function():Node} _createNode 141 | */ 142 | /* c8 ignore next 2 */ 143 | export const printDom = (_createNode) => {} 144 | 145 | /** 146 | * @param {HTMLCanvasElement} canvas 147 | * @param {number} height 148 | */ 149 | /* c8 ignore next 2 */ 150 | export const printCanvas = (canvas, height) => 151 | printImg(canvas.toDataURL(), height) 152 | 153 | /** 154 | * @param {Element} _dom 155 | */ 156 | /* c8 ignore next */ 157 | export const createVConsole = (_dom) => {} 158 | 159 | /** 160 | * @param {string} moduleName 161 | * @return {function(...any):void} 162 | */ 163 | /* c8 ignore next */ 164 | export const createModuleLogger = (moduleName) => common.createModuleLogger(print, moduleName) 165 | -------------------------------------------------------------------------------- /logging.test.js: -------------------------------------------------------------------------------- 1 | import * as log from 'lib0/logging' 2 | 3 | export const testLogging = () => { 4 | log.print(log.BLUE, 'blue ') 5 | log.print(log.BLUE, 'blue ', log.BOLD, 'blue,bold') 6 | log.print(log.GREEN, log.RED, 'red ', 'red') 7 | log.print(log.ORANGE, 'orange') 8 | log.print(log.BOLD, 'bold ', log.UNBOLD, 'nobold') 9 | log.print(log.GREEN, 'green ', log.UNCOLOR, 'nocolor') 10 | log.print('expecting objects from now on!') 11 | log.print({ 'my-object': 'isLogged' }) 12 | log.print(log.GREEN, 'green ', { 'my-object': 'isLogged' }) 13 | log.print(log.GREEN, 'green ', { 'my-object': 'isLogged' }, 'unformatted') 14 | log.print(log.BLUE, log.BOLD, 'number', 1) 15 | log.print(log.BLUE, log.BOLD, 'number', 1, {}, 's', 2) 16 | log.print({}, 'dtrn') 17 | log.print(() => [log.GREEN, 'can lazyprint stuff ', log.RED, 'with formatting']) 18 | log.print(undefined, 'supports undefined') 19 | } 20 | 21 | export const testModuleLogger = () => { 22 | // if you want to see the messages, enable logging: LOG=* npm run test --filter logging 23 | const mlog = log.createModuleLogger('testing') 24 | mlog('can print ', log.GREEN, 'with colors') 25 | mlog(() => ['can lazyprint ', log.GREEN, 'with colors']) 26 | mlog(undefined, 'supports undefined') 27 | mlog(() => [undefined, 'supports lazyprint undefined']) 28 | } 29 | -------------------------------------------------------------------------------- /map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility module to work with key-value stores. 3 | * 4 | * @module map 5 | */ 6 | 7 | /** 8 | * Creates a new Map instance. 9 | * 10 | * @function 11 | * @return {Map} 12 | * 13 | * @function 14 | */ 15 | export const create = () => new Map() 16 | 17 | /** 18 | * Copy a Map object into a fresh Map object. 19 | * 20 | * @function 21 | * @template K,V 22 | * @param {Map} m 23 | * @return {Map} 24 | */ 25 | export const copy = m => { 26 | const r = create() 27 | m.forEach((v, k) => { r.set(k, v) }) 28 | return r 29 | } 30 | 31 | /** 32 | * Get map property. Create T if property is undefined and set T on map. 33 | * 34 | * ```js 35 | * const listeners = map.setIfUndefined(events, 'eventName', set.create) 36 | * listeners.add(listener) 37 | * ``` 38 | * 39 | * @function 40 | * @template {Map} MAP 41 | * @template {MAP extends Map ? function():V : unknown} CF 42 | * @param {MAP} map 43 | * @param {MAP extends Map ? K : unknown} key 44 | * @param {CF} createT 45 | * @return {ReturnType} 46 | */ 47 | export const setIfUndefined = (map, key, createT) => { 48 | let set = map.get(key) 49 | if (set === undefined) { 50 | map.set(key, set = createT()) 51 | } 52 | return set 53 | } 54 | 55 | /** 56 | * Creates an Array and populates it with the content of all key-value pairs using the `f(value, key)` function. 57 | * 58 | * @function 59 | * @template K 60 | * @template V 61 | * @template R 62 | * @param {Map} m 63 | * @param {function(V,K):R} f 64 | * @return {Array} 65 | */ 66 | export const map = (m, f) => { 67 | const res = [] 68 | for (const [key, value] of m) { 69 | res.push(f(value, key)) 70 | } 71 | return res 72 | } 73 | 74 | /** 75 | * Tests whether any key-value pairs pass the test implemented by `f(value, key)`. 76 | * 77 | * @todo should rename to some - similarly to Array.some 78 | * 79 | * @function 80 | * @template K 81 | * @template V 82 | * @param {Map} m 83 | * @param {function(V,K):boolean} f 84 | * @return {boolean} 85 | */ 86 | export const any = (m, f) => { 87 | for (const [key, value] of m) { 88 | if (f(value, key)) { 89 | return true 90 | } 91 | } 92 | return false 93 | } 94 | 95 | /** 96 | * Tests whether all key-value pairs pass the test implemented by `f(value, key)`. 97 | * 98 | * @function 99 | * @template K 100 | * @template V 101 | * @param {Map} m 102 | * @param {function(V,K):boolean} f 103 | * @return {boolean} 104 | */ 105 | export const all = (m, f) => { 106 | for (const [key, value] of m) { 107 | if (!f(value, key)) { 108 | return false 109 | } 110 | } 111 | return true 112 | } 113 | -------------------------------------------------------------------------------- /map.test.js: -------------------------------------------------------------------------------- 1 | import * as map from './map.js' 2 | import * as math from './math.js' 3 | import * as t from './testing.js' 4 | 5 | /** 6 | * @param {t.TestCase} _tc 7 | */ 8 | export const testMap = _tc => { 9 | /** 10 | * @type {Map} 11 | */ 12 | const m = map.create() 13 | m.set(1, 2) 14 | m.set(2, 3) 15 | t.assert(map.map(m, (value, key) => value * 2 + key).reduce(math.add) === 13) 16 | let numberOfWrites = 0 17 | const createT = () => ++numberOfWrites 18 | map.setIfUndefined(m, 3, createT) 19 | map.setIfUndefined(m, 3, createT) 20 | map.setIfUndefined(m, 3, createT) 21 | t.compare(map.copy(m), m) 22 | t.assert(numberOfWrites === 1) 23 | t.assert(map.any(m, (v, k) => k === 1 && v === 2)) 24 | t.assert(map.any(m, (v, k) => k === 2 && v === 3)) 25 | t.assert(!map.any(m, () => false)) 26 | t.assert(!map.all(m, (v, k) => k === 1 && v === 2)) 27 | t.assert(map.all(m, (v) => v === 2 || v === 3 || v === numberOfWrites)) 28 | } 29 | 30 | /** 31 | * @param {t.TestCase} _tc 32 | */ 33 | export const testTypeDefinitions = _tc => { 34 | // setIfUndefined supports inheritance properly: See https://github.com/dmonad/lib0/issues/82 35 | class A { 36 | constructor () { 37 | this.a = 4 38 | } 39 | } 40 | class B extends A { 41 | constructor () { 42 | super() 43 | this.b = 4 44 | } 45 | } 46 | /** 47 | * @type {Map} 48 | */ 49 | const m = map.create() 50 | /** 51 | * @type {B} 52 | */ 53 | const b = map.setIfUndefined(m, 0, () => new B()) 54 | console.log(b) 55 | } 56 | -------------------------------------------------------------------------------- /math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Common Math expressions. 3 | * 4 | * @module math 5 | */ 6 | 7 | export const floor = Math.floor 8 | export const ceil = Math.ceil 9 | export const abs = Math.abs 10 | export const imul = Math.imul 11 | export const round = Math.round 12 | export const log10 = Math.log10 13 | export const log2 = Math.log2 14 | export const log = Math.log 15 | export const sqrt = Math.sqrt 16 | 17 | /** 18 | * @function 19 | * @param {number} a 20 | * @param {number} b 21 | * @return {number} The sum of a and b 22 | */ 23 | export const add = (a, b) => a + b 24 | 25 | /** 26 | * @function 27 | * @param {number} a 28 | * @param {number} b 29 | * @return {number} The smaller element of a and b 30 | */ 31 | export const min = (a, b) => a < b ? a : b 32 | 33 | /** 34 | * @function 35 | * @param {number} a 36 | * @param {number} b 37 | * @return {number} The bigger element of a and b 38 | */ 39 | export const max = (a, b) => a > b ? a : b 40 | 41 | export const isNaN = Number.isNaN 42 | 43 | export const pow = Math.pow 44 | /** 45 | * Base 10 exponential function. Returns the value of 10 raised to the power of pow. 46 | * 47 | * @param {number} exp 48 | * @return {number} 49 | */ 50 | export const exp10 = exp => Math.pow(10, exp) 51 | 52 | export const sign = Math.sign 53 | 54 | /** 55 | * @param {number} n 56 | * @return {boolean} Wether n is negative. This function also differentiates between -0 and +0 57 | */ 58 | export const isNegativeZero = n => n !== 0 ? n < 0 : 1 / n < 0 59 | -------------------------------------------------------------------------------- /math.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as math from './math.js' 3 | import * as array from './array.js' 4 | 5 | /** 6 | * @param {t.TestCase} tc 7 | */ 8 | export const testMath = tc => { 9 | t.describe('math.abs') 10 | t.assert(math.abs(-1) === 1) 11 | t.assert(math.abs(Number.MIN_SAFE_INTEGER) === Number.MAX_SAFE_INTEGER) 12 | t.assert(math.abs(Number.MAX_SAFE_INTEGER) === Number.MAX_SAFE_INTEGER) 13 | t.describe('math.add') 14 | t.assert(array.fold([1, 2, 3, 4, 5], 0, math.add) === 15) 15 | t.describe('math.ceil') 16 | t.assert(math.ceil(1.5) === 2) 17 | t.assert(math.ceil(-1.5) === -1) 18 | t.describe('math.floor') 19 | t.assert(math.floor(1.5) === 1) 20 | t.assert(math.floor(-1.5) === -2) 21 | t.describe('math.isNaN') 22 | t.assert(math.isNaN(NaN)) 23 | // @ts-ignore 24 | t.assert(!math.isNaN(null)) 25 | t.describe('math.max') 26 | t.assert([1, 3, 65, 1, 314, 25, 3475, 2, 1].reduce(math.max) === 3475) 27 | t.describe('math.min') 28 | t.assert([1, 3, 65, 1, 314, 25, 3475, 2, 1].reduce(math.min) === 1) 29 | t.describe('math.round') 30 | t.assert(math.round(0.5) === 1) 31 | t.assert(math.round(-0.5) === 0) 32 | } 33 | -------------------------------------------------------------------------------- /metric.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility module to convert metric values. 3 | * 4 | * @module metric 5 | */ 6 | 7 | import * as math from './math.js' 8 | 9 | export const yotta = 1e24 10 | export const zetta = 1e21 11 | export const exa = 1e18 12 | export const peta = 1e15 13 | export const tera = 1e12 14 | export const giga = 1e9 15 | export const mega = 1e6 16 | export const kilo = 1e3 17 | export const hecto = 1e2 18 | export const deca = 10 19 | export const deci = 0.1 20 | export const centi = 0.01 21 | export const milli = 1e-3 22 | export const micro = 1e-6 23 | export const nano = 1e-9 24 | export const pico = 1e-12 25 | export const femto = 1e-15 26 | export const atto = 1e-18 27 | export const zepto = 1e-21 28 | export const yocto = 1e-24 29 | 30 | const prefixUp = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] 31 | const prefixDown = ['', 'm', 'μ', 'n', 'p', 'f', 'a', 'z', 'y'] 32 | 33 | /** 34 | * Calculate the metric prefix for a number. Assumes E.g. `prefix(1000) = { n: 1, prefix: 'k' }` 35 | * 36 | * @param {number} n 37 | * @param {number} [baseMultiplier] Multiplier of the base (10^(3*baseMultiplier)). E.g. `convert(time, -3)` if time is already in milli seconds 38 | * @return {{n:number,prefix:string}} 39 | */ 40 | export const prefix = (n, baseMultiplier = 0) => { 41 | const nPow = n === 0 ? 0 : math.log10(n) 42 | let mult = 0 43 | while (nPow < mult * 3 && baseMultiplier > -8) { 44 | baseMultiplier-- 45 | mult-- 46 | } 47 | while (nPow >= 3 + mult * 3 && baseMultiplier < 8) { 48 | baseMultiplier++ 49 | mult++ 50 | } 51 | const prefix = baseMultiplier < 0 ? prefixDown[-baseMultiplier] : prefixUp[baseMultiplier] 52 | return { 53 | n: math.round((mult > 0 ? n / math.exp10(mult * 3) : n * math.exp10(mult * -3)) * 1e12) / 1e12, 54 | prefix 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /metric.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as metric from './metric.js' 3 | 4 | /** 5 | * @param {t.TestCase} tc 6 | */ 7 | export const testMetricPrefix = tc => { 8 | t.compare(metric.prefix(0), { n: 0, prefix: '' }) 9 | t.compare(metric.prefix(1, -1), { n: 1, prefix: 'm' }) 10 | t.compare(metric.prefix(1.5), { n: 1.5, prefix: '' }) 11 | t.compare(metric.prefix(100.5), { n: 100.5, prefix: '' }) 12 | t.compare(metric.prefix(1000.5), { n: 1.0005, prefix: 'k' }) 13 | t.compare(metric.prefix(0.3), { n: 300, prefix: 'm' }) 14 | t.compare(metric.prefix(0.001), { n: 1, prefix: 'm' }) 15 | // up 16 | t.compare(metric.prefix(10000), { n: 10, prefix: 'k' }) 17 | t.compare(metric.prefix(1e7), { n: 10, prefix: 'M' }) 18 | t.compare(metric.prefix(1e11), { n: 100, prefix: 'G' }) 19 | t.compare(metric.prefix(1e12 + 3), { n: (1e12 + 3) / 1e12, prefix: 'T' }) 20 | t.compare(metric.prefix(1e15), { n: 1, prefix: 'P' }) 21 | t.compare(metric.prefix(1e20), { n: 100, prefix: 'E' }) 22 | t.compare(metric.prefix(1e22), { n: 10, prefix: 'Z' }) 23 | t.compare(metric.prefix(1e24), { n: 1, prefix: 'Y' }) 24 | t.compare(metric.prefix(1e28), { n: 10000, prefix: 'Y' }) 25 | // down 26 | t.compare(metric.prefix(0.01), { n: 10, prefix: 'm' }) 27 | t.compare(metric.prefix(1e-4), { n: 100, prefix: 'μ' }) 28 | t.compare(metric.prefix(1e-9), { n: 1, prefix: 'n' }) 29 | t.compare(metric.prefix(1e-12), { n: 1, prefix: 'p' }) 30 | t.compare(metric.prefix(1e-14), { n: 10, prefix: 'f' }) 31 | t.compare(metric.prefix(1e-18), { n: 1, prefix: 'a' }) 32 | t.compare(metric.prefix(1e-21), { n: 1, prefix: 'z' }) 33 | t.compare(metric.prefix(1e-22), { n: 100, prefix: 'y' }) 34 | t.compare(metric.prefix(1e-30), { n: 0.000001, prefix: 'y' }) 35 | } 36 | -------------------------------------------------------------------------------- /mutex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mutual exclude for JavaScript. 3 | * 4 | * @module mutex 5 | */ 6 | 7 | /** 8 | * @callback mutex 9 | * @param {function():void} cb Only executed when this mutex is not in the current stack 10 | * @param {function():void} [elseCb] Executed when this mutex is in the current stack 11 | */ 12 | 13 | /** 14 | * Creates a mutual exclude function with the following property: 15 | * 16 | * ```js 17 | * const mutex = createMutex() 18 | * mutex(() => { 19 | * // This function is immediately executed 20 | * mutex(() => { 21 | * // This function is not executed, as the mutex is already active. 22 | * }) 23 | * }) 24 | * ``` 25 | * 26 | * @return {mutex} A mutual exclude function 27 | * @public 28 | */ 29 | export const createMutex = () => { 30 | let token = true 31 | return (f, g) => { 32 | if (token) { 33 | token = false 34 | try { 35 | f() 36 | } finally { 37 | token = true 38 | } 39 | } else if (g !== undefined) { 40 | g() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /number.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility helpers for working with numbers. 3 | * 4 | * @module number 5 | */ 6 | 7 | import * as math from './math.js' 8 | import * as binary from './binary.js' 9 | 10 | export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER 11 | export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER 12 | 13 | export const LOWEST_INT32 = 1 << 31 14 | export const HIGHEST_INT32 = binary.BITS31 15 | export const HIGHEST_UINT32 = binary.BITS32 16 | 17 | /* c8 ignore next */ 18 | export const isInteger = Number.isInteger || (num => typeof num === 'number' && isFinite(num) && math.floor(num) === num) 19 | export const isNaN = Number.isNaN 20 | export const parseInt = Number.parseInt 21 | 22 | /** 23 | * Count the number of "1" bits in an unsigned 32bit number. 24 | * 25 | * Super fun bitcount algorithm by Brian Kernighan. 26 | * 27 | * @param {number} n 28 | */ 29 | export const countBits = n => { 30 | n &= binary.BITS32 31 | let count = 0 32 | while (n) { 33 | n &= (n - 1) 34 | count++ 35 | } 36 | return count 37 | } 38 | -------------------------------------------------------------------------------- /number.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as number from './number.js' 3 | import * as random from './random.js' 4 | import * as math from './math.js' 5 | 6 | /** 7 | * @param {t.TestCase} _tc 8 | */ 9 | export const testNumber = _tc => { 10 | t.describe('isNaN') 11 | t.assert(number.isNaN(NaN)) 12 | t.assert(!number.isNaN(1 / 0)) 13 | // @ts-ignore 14 | t.assert(number.isNaN('a' / 0)) 15 | t.assert(!number.isNaN(0)) 16 | t.describe('isInteger') 17 | t.assert(!number.isInteger(1 / 0)) 18 | t.assert(!number.isInteger(NaN)) 19 | t.assert(number.isInteger(0)) 20 | t.assert(number.isInteger(-1)) 21 | t.assert(number.countBits(1) === 1) 22 | t.assert(number.countBits(3) === 2) 23 | t.assert(number.countBits(128 + 3) === 3) 24 | } 25 | 26 | /** 27 | * This benchmark confirms performance of division vs shifting numbers. 28 | * 29 | * @param {t.TestCase} tc 30 | */ 31 | export const testShiftVsDivision = tc => { 32 | /** 33 | * @type {Array} 34 | */ 35 | const numbers = [] 36 | 37 | for (let i = 0; i < 10000; i++) { 38 | numbers.push(random.uint32()) 39 | } 40 | 41 | t.measureTime('comparison', () => { 42 | for (let i = 0; i < numbers.length; i++) { 43 | let n = numbers[i] 44 | while (n > 0) { 45 | const ns = n >>> 7 46 | const nd = math.floor(n / 128) 47 | t.assert(ns === nd) 48 | n = nd 49 | } 50 | } 51 | }) 52 | 53 | t.measureTime('shift', () => { 54 | let x = 0 55 | for (let i = 0; i < numbers.length; i++) { 56 | x = numbers[i] >>> 7 57 | } 58 | t.info('' + x) 59 | }) 60 | 61 | t.measureTime('division', () => { 62 | for (let i = 0; i < numbers.length; i++) { 63 | math.floor(numbers[i] / 128) 64 | } 65 | }) 66 | 67 | { 68 | /** 69 | * @type {Array} 70 | */ 71 | const divided = [] 72 | /** 73 | * @type {Array} 74 | */ 75 | const shifted = [] 76 | t.measureTime('division', () => { 77 | for (let i = 0; i < numbers.length; i++) { 78 | divided.push(math.floor(numbers[i] / 128)) 79 | } 80 | }) 81 | 82 | t.measureTime('shift', () => { 83 | for (let i = 0; i < numbers.length; i++) { 84 | shifted.push(numbers[i] >>> 7) 85 | } 86 | }) 87 | 88 | t.compareArrays(shifted, divided) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for working with EcmaScript objects. 3 | * 4 | * @module object 5 | */ 6 | 7 | /** 8 | * @return {Object} obj 9 | */ 10 | export const create = () => Object.create(null) 11 | 12 | /** 13 | * Object.assign 14 | */ 15 | export const assign = Object.assign 16 | 17 | /** 18 | * @param {Object} obj 19 | */ 20 | export const keys = Object.keys 21 | 22 | /** 23 | * @template V 24 | * @param {{[key:string]: V}} obj 25 | * @return {Array} 26 | */ 27 | export const values = Object.values 28 | 29 | /** 30 | * @template V 31 | * @param {{[k:string]:V}} obj 32 | * @param {function(V,string):any} f 33 | */ 34 | export const forEach = (obj, f) => { 35 | for (const key in obj) { 36 | f(obj[key], key) 37 | } 38 | } 39 | 40 | /** 41 | * @todo implement mapToArray & map 42 | * 43 | * @template R 44 | * @param {Object} obj 45 | * @param {function(any,string):R} f 46 | * @return {Array} 47 | */ 48 | export const map = (obj, f) => { 49 | const results = [] 50 | for (const key in obj) { 51 | results.push(f(obj[key], key)) 52 | } 53 | return results 54 | } 55 | 56 | /** 57 | * @deprecated use object.size instead 58 | * @param {Object} obj 59 | * @return {number} 60 | */ 61 | export const length = obj => keys(obj).length 62 | 63 | /** 64 | * @param {Object} obj 65 | * @return {number} 66 | */ 67 | export const size = obj => keys(obj).length 68 | 69 | /** 70 | * @param {Object} obj 71 | * @param {function(any,string):boolean} f 72 | * @return {boolean} 73 | */ 74 | export const some = (obj, f) => { 75 | for (const key in obj) { 76 | if (f(obj[key], key)) { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | /** 84 | * @param {Object|null|undefined} obj 85 | */ 86 | export const isEmpty = obj => { 87 | // eslint-disable-next-line no-unreachable-loop 88 | for (const _k in obj) { 89 | return false 90 | } 91 | return true 92 | } 93 | 94 | /** 95 | * @param {Object} obj 96 | * @param {function(any,string):boolean} f 97 | * @return {boolean} 98 | */ 99 | export const every = (obj, f) => { 100 | for (const key in obj) { 101 | if (!f(obj[key], key)) { 102 | return false 103 | } 104 | } 105 | return true 106 | } 107 | 108 | /** 109 | * Calls `Object.prototype.hasOwnProperty`. 110 | * 111 | * @param {any} obj 112 | * @param {string|symbol} key 113 | * @return {boolean} 114 | */ 115 | export const hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) 116 | 117 | /** 118 | * @param {Object} a 119 | * @param {Object} b 120 | * @return {boolean} 121 | */ 122 | export const equalFlat = (a, b) => a === b || (size(a) === size(b) && every(a, (val, key) => (val !== undefined || hasProperty(b, key)) && b[key] === val)) 123 | 124 | /** 125 | * Make an object immutable. This hurts performance and is usually not needed if you perform good 126 | * coding practices. 127 | */ 128 | export const freeze = Object.freeze 129 | 130 | /** 131 | * Make an object and all its children immutable. 132 | * This *really* hurts performance and is usually not needed if you perform good coding practices. 133 | * 134 | * @template {any} T 135 | * @param {T} o 136 | * @return {Readonly} 137 | */ 138 | export const deepFreeze = (o) => { 139 | for (const key in o) { 140 | const c = o[key] 141 | if (typeof c === 'object' || typeof c === 'function') { 142 | deepFreeze(o[key]) 143 | } 144 | } 145 | return freeze(o) 146 | } 147 | -------------------------------------------------------------------------------- /object.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as object from './object.js' 3 | import * as math from './math.js' 4 | 5 | /** 6 | * @param {t.TestCase} _tc 7 | */ 8 | export const testObject = _tc => { 9 | t.assert(object.create().constructor === undefined, 'object.create creates an empty object without constructor') 10 | t.describe('object.equalFlat') 11 | t.assert(object.equalFlat({}, {}), 'comparing equal objects') 12 | t.assert(object.equalFlat({ x: 1 }, { x: 1 }), 'comparing equal objects') 13 | t.assert(object.equalFlat({ x: 'dtrn' }, { x: 'dtrn' }), 'comparing equal objects') 14 | t.assert(!object.equalFlat({ x: {} }, { x: {} }), 'flatEqual does not dive deep') 15 | t.assert(object.equalFlat({ x: undefined }, { x: undefined }), 'flatEqual handles undefined') 16 | t.assert(!object.equalFlat({ x: undefined }, { y: {} }), 'flatEqual handles undefined') 17 | t.describe('object.every') 18 | t.assert(object.every({ a: 1, b: 3 }, (v, k) => (v % 2) === 1 && k !== 'c')) 19 | t.assert(!object.every({ a: 1, b: 3, c: 5 }, (v, k) => (v % 2) === 1 && k !== 'c')) 20 | t.describe('object.some') 21 | t.assert(object.some({ a: 1, b: 3 }, (v, k) => v === 3 && k === 'b')) 22 | t.assert(!object.some({ a: 1, b: 5 }, (v, _k) => v === 3)) 23 | t.assert(object.some({ a: 1, b: 5 }, () => true)) 24 | t.assert(!object.some({ a: 1, b: 5 }, (_v, _k) => false)) 25 | t.describe('object.forEach') 26 | let forEachSum = 0 27 | const r = { x: 1, y: 3 } 28 | object.forEach(r, (v, _k) => { forEachSum += v }) 29 | t.assert(forEachSum === 4) 30 | t.describe('object.map') 31 | t.assert(object.map({ x: 1, z: 5 }, (v, _k) => v).reduce(math.add) === 6) 32 | t.describe('object.length') 33 | t.assert(object.length({}) === 0) 34 | t.assert(object.length({ x: 1 }) === 1) 35 | t.assert(object.isEmpty({})) 36 | t.assert(!object.isEmpty({ a: 3 })) 37 | t.assert(object.isEmpty(null)) 38 | t.assert(object.isEmpty(undefined)) 39 | 40 | /** 41 | * @type {Array} 42 | */ 43 | const keys = object.keys({ a: 1, b: 2 }) 44 | t.compare(keys, ['a', 'b']) 45 | /** 46 | * @type {Array} 47 | */ 48 | const vals = object.values({ a: 1 }) 49 | t.compare(vals, [1, 2]) 50 | } 51 | 52 | /** 53 | * @param {t.TestCase} _tc 54 | */ 55 | export const testFreeze = _tc => { 56 | const o1 = { a: { b: [1, 2, 3] } } 57 | const o2 = [1, 2, { a: 'hi' }] 58 | object.deepFreeze(o1) 59 | object.deepFreeze(o2) 60 | t.fails(() => { 61 | o1.a.b.push(4) 62 | }) 63 | t.fails(() => { 64 | o1.a.b = [1] 65 | }) 66 | t.fails(() => { 67 | o2.push(4) 68 | }) 69 | t.fails(() => { 70 | o2[2] = 42 71 | }) 72 | t.fails(() => { 73 | // @ts-ignore-next-line 74 | o2[2].a = 'hello' 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /observable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Observable class prototype. 3 | * 4 | * @module observable 5 | */ 6 | 7 | import * as map from './map.js' 8 | import * as set from './set.js' 9 | import * as array from './array.js' 10 | 11 | /** 12 | * Handles named events. 13 | * @experimental 14 | * 15 | * This is basically a (better typed) duplicate of Observable, which will replace Observable in the 16 | * next release. 17 | * 18 | * @template {{[key in keyof EVENTS]: function(...any):void}} EVENTS 19 | */ 20 | export class ObservableV2 { 21 | constructor () { 22 | /** 23 | * Some desc. 24 | * @type {Map>} 25 | */ 26 | this._observers = map.create() 27 | } 28 | 29 | /** 30 | * @template {keyof EVENTS & string} NAME 31 | * @param {NAME} name 32 | * @param {EVENTS[NAME]} f 33 | */ 34 | on (name, f) { 35 | map.setIfUndefined(this._observers, /** @type {string} */ (name), set.create).add(f) 36 | return f 37 | } 38 | 39 | /** 40 | * @template {keyof EVENTS & string} NAME 41 | * @param {NAME} name 42 | * @param {EVENTS[NAME]} f 43 | */ 44 | once (name, f) { 45 | /** 46 | * @param {...any} args 47 | */ 48 | const _f = (...args) => { 49 | this.off(name, /** @type {any} */ (_f)) 50 | f(...args) 51 | } 52 | this.on(name, /** @type {any} */ (_f)) 53 | } 54 | 55 | /** 56 | * @template {keyof EVENTS & string} NAME 57 | * @param {NAME} name 58 | * @param {EVENTS[NAME]} f 59 | */ 60 | off (name, f) { 61 | const observers = this._observers.get(name) 62 | if (observers !== undefined) { 63 | observers.delete(f) 64 | if (observers.size === 0) { 65 | this._observers.delete(name) 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Emit a named event. All registered event listeners that listen to the 72 | * specified name will receive the event. 73 | * 74 | * @todo This should catch exceptions 75 | * 76 | * @template {keyof EVENTS & string} NAME 77 | * @param {NAME} name The event name. 78 | * @param {Parameters} args The arguments that are applied to the event listener. 79 | */ 80 | emit (name, args) { 81 | // copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called. 82 | return array.from((this._observers.get(name) || map.create()).values()).forEach(f => f(...args)) 83 | } 84 | 85 | destroy () { 86 | this._observers = map.create() 87 | } 88 | } 89 | 90 | /* c8 ignore start */ 91 | /** 92 | * Handles named events. 93 | * 94 | * @deprecated 95 | * @template N 96 | */ 97 | export class Observable { 98 | constructor () { 99 | /** 100 | * Some desc. 101 | * @type {Map} 102 | */ 103 | this._observers = map.create() 104 | } 105 | 106 | /** 107 | * @param {N} name 108 | * @param {function} f 109 | */ 110 | on (name, f) { 111 | map.setIfUndefined(this._observers, name, set.create).add(f) 112 | } 113 | 114 | /** 115 | * @param {N} name 116 | * @param {function} f 117 | */ 118 | once (name, f) { 119 | /** 120 | * @param {...any} args 121 | */ 122 | const _f = (...args) => { 123 | this.off(name, _f) 124 | f(...args) 125 | } 126 | this.on(name, _f) 127 | } 128 | 129 | /** 130 | * @param {N} name 131 | * @param {function} f 132 | */ 133 | off (name, f) { 134 | const observers = this._observers.get(name) 135 | if (observers !== undefined) { 136 | observers.delete(f) 137 | if (observers.size === 0) { 138 | this._observers.delete(name) 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Emit a named event. All registered event listeners that listen to the 145 | * specified name will receive the event. 146 | * 147 | * @todo This should catch exceptions 148 | * 149 | * @param {N} name The event name. 150 | * @param {Array} args The arguments that are applied to the event listener. 151 | */ 152 | emit (name, args) { 153 | // copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called. 154 | return array.from((this._observers.get(name) || map.create()).values()).forEach(f => f(...args)) 155 | } 156 | 157 | destroy () { 158 | this._observers = map.create() 159 | } 160 | } 161 | /* c8 ignore end */ 162 | -------------------------------------------------------------------------------- /observable.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import { ObservableV2 } from './observable.js' 3 | 4 | /** 5 | * @param {t.TestCase} _tc 6 | */ 7 | export const testTypedObservable = _tc => { 8 | /** 9 | * @type {ObservableV2<{ "hey": function(number, string):any, listen: function(string):any }>} 10 | */ 11 | const o = new ObservableV2() 12 | let calls = 0 13 | /** 14 | * Test "hey" 15 | */ 16 | /** 17 | * @param {number} n 18 | * @param {string} s 19 | */ 20 | const listener = (n, s) => { 21 | t.assert(typeof n === 'number') 22 | t.assert(typeof s === 'string') 23 | calls++ 24 | } 25 | o.on('hey', listener) 26 | o.on('hey', (arg1) => t.assert(typeof arg1 === 'number')) 27 | // o.emit('hey', ['four']) // should emit type error 28 | // o.emit('hey', [4]) // should emit type error 29 | o.emit('hey', [4, 'four']) 30 | t.assert(calls === 1) 31 | o.emit('hey', [5, 'five']) 32 | t.assert(calls === 2) 33 | o.off('hey', listener) 34 | o.emit('hey', [6, 'six']) 35 | t.assert(calls === 2) 36 | /** 37 | * Test "listen" 38 | */ 39 | o.once('listen', n => { 40 | t.assert(typeof n === 'string') 41 | calls++ 42 | }) 43 | // o.emit('listen', [4]) // should emit type error 44 | o.emit('listen', ['four']) 45 | o.emit('listen', ['five']) // shouldn't trigger 46 | t.assert(calls === 3) 47 | o.destroy() 48 | o.emit('hey', [7, 'seven']) 49 | t.assert(calls === 3) 50 | } 51 | -------------------------------------------------------------------------------- /pair.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Working with value pairs. 3 | * 4 | * @module pair 5 | */ 6 | 7 | /** 8 | * @template L,R 9 | */ 10 | export class Pair { 11 | /** 12 | * @param {L} left 13 | * @param {R} right 14 | */ 15 | constructor (left, right) { 16 | this.left = left 17 | this.right = right 18 | } 19 | } 20 | 21 | /** 22 | * @template L,R 23 | * @param {L} left 24 | * @param {R} right 25 | * @return {Pair} 26 | */ 27 | export const create = (left, right) => new Pair(left, right) 28 | 29 | /** 30 | * @template L,R 31 | * @param {R} right 32 | * @param {L} left 33 | * @return {Pair} 34 | */ 35 | export const createReversed = (right, left) => new Pair(left, right) 36 | 37 | /** 38 | * @template L,R 39 | * @param {Array>} arr 40 | * @param {function(L, R):any} f 41 | */ 42 | export const forEach = (arr, f) => arr.forEach(p => f(p.left, p.right)) 43 | 44 | /** 45 | * @template L,R,X 46 | * @param {Array>} arr 47 | * @param {function(L, R):X} f 48 | * @return {Array} 49 | */ 50 | export const map = (arr, f) => arr.map(p => f(p.left, p.right)) 51 | -------------------------------------------------------------------------------- /pair.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as pair from './pair.js' 3 | import * as math from './math.js' 4 | 5 | /** 6 | * @param {t.TestCase} tc 7 | */ 8 | export const testPair = tc => { 9 | const ps = [pair.create(1, 2), pair.create(3, 4), pair.createReversed(6, 5)] 10 | t.describe('Counting elements in pair list') 11 | let countLeft = 0 12 | let countRight = 0 13 | pair.forEach(ps, (left, right) => { 14 | countLeft += left 15 | countRight += right 16 | }) 17 | t.assert(countLeft === 9) 18 | t.assert(countRight === 12) 19 | t.assert(countLeft === pair.map(ps, left => left).reduce(math.add)) 20 | t.assert(countRight === pair.map(ps, (left, right) => right).reduce(math.add)) 21 | } 22 | -------------------------------------------------------------------------------- /performance.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export const measure = performance.measure.bind(performance) 4 | export const now = performance.now.bind(performance) 5 | export const mark = performance.mark.bind(performance) 6 | -------------------------------------------------------------------------------- /performance.node.js: -------------------------------------------------------------------------------- 1 | import { performance } from 'node:perf_hooks' 2 | import { nop } from './function.js' 3 | import * as time from './time.js' 4 | 5 | /** 6 | * @type {typeof performance.measure} 7 | */ 8 | /* c8 ignore next */ 9 | export const measure = performance.measure ? performance.measure.bind(performance) : /** @type {any} */ (nop) 10 | 11 | /** 12 | * @type {typeof performance.now} 13 | */ 14 | /* c8 ignore next */ 15 | export const now = performance.now ? performance.now.bind(performance) : time.getUnixTime 16 | 17 | /** 18 | * @type {typeof performance.mark} 19 | */ 20 | /* c8 ignore next */ 21 | export const mark = performance.mark ? performance.mark.bind(performance) : /** @type {any} */ (nop) 22 | -------------------------------------------------------------------------------- /pledge.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as pledge from './pledge.js' 3 | import * as promise from './promise.js' 4 | 5 | /** 6 | * @param {t.TestCase} _tc 7 | */ 8 | export const testPledgeCoroutine = async _tc => { 9 | let called = false 10 | const p = pledge.coroutine(function * () { 11 | const y = pledge.wait(10).map(() => 42) 12 | const num = yield y 13 | console.log({ num }) 14 | t.assert(num === 42) 15 | called = true 16 | return 42 17 | }) 18 | t.assert(!called) 19 | await p.promise() 20 | t.assert(called) 21 | } 22 | 23 | /** 24 | * @param {t.TestCase} _tc 25 | */ 26 | export const testPledgeVsPromisePerformanceTimeout = async _tc => { 27 | const iterations = 100 28 | const waitTime = 0 29 | await t.measureTimeAsync(`Awaiting ${iterations} callbacks (promise)`, async () => { 30 | for (let i = 0; i < iterations; i++) { 31 | await promise.wait(waitTime) 32 | } 33 | }) 34 | await t.measureTimeAsync(`Awaiting ${iterations} callbacks (pledge)`, () => 35 | pledge.coroutine(function * () { 36 | for (let i = 0; i < iterations; i++) { 37 | yield pledge.wait(waitTime) 38 | } 39 | }).promise() 40 | ) 41 | } 42 | 43 | /** 44 | * @typedef {Promise | number} MaybePromise 45 | */ 46 | 47 | /** 48 | * @param {t.TestCase} _tc 49 | */ 50 | export const testPledgeVsPromisePerformanceResolved = async _tc => { 51 | const iterations = 100 52 | t.measureTime(`Awaiting ${iterations} callbacks (only iterate)`, () => { 53 | for (let i = 0; i < iterations; i++) { /* nop */ } 54 | }) 55 | await t.measureTimeAsync(`Awaiting ${iterations} callbacks (promise)`, async () => { 56 | for (let i = 0; i < iterations; i++) { 57 | await promise.resolve(0) 58 | } 59 | }) 60 | await t.measureTimeAsync(`Awaiting ${iterations} callbacks (await, no resolve)`, async () => { 61 | for (let i = 0; i < iterations; i++) { 62 | /** 63 | * @type {Promise | number} 64 | */ 65 | const x = 0 66 | await x 67 | } 68 | }) 69 | await t.measureTimeAsync(`Awaiting ${iterations} callbacks (pledge)`, () => 70 | pledge.coroutine(function * () { 71 | for (let i = 0; i < iterations; i++) { 72 | yield 0 73 | } 74 | }).promise() 75 | ) 76 | t.measureTime(`Awaiting ${iterations} callbacks (pledge, manual wrap)`, () => { 77 | /** 78 | * @type {pledge.Pledge} 79 | */ 80 | let val = 0 81 | for (let i = 0; i < iterations; i++) { 82 | val = pledge.map(val, _v => 0) 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /prng/Mt19937.js: -------------------------------------------------------------------------------- 1 | import * as binary from '../binary.js' 2 | import * as math from '../math.js' 3 | 4 | /** 5 | * @module prng 6 | */ 7 | const N = 624 8 | const M = 397 9 | 10 | /** 11 | * @param {number} u 12 | * @param {number} v 13 | */ 14 | const twist = (u, v) => ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0)) 15 | 16 | /** 17 | * @param {Uint32Array} state 18 | */ 19 | const nextState = state => { 20 | let p = 0 21 | let j 22 | for (j = N - M + 1; --j; p++) { 23 | state[p] = state[p + M] ^ twist(state[p], state[p + 1]) 24 | } 25 | for (j = M; --j; p++) { 26 | state[p] = state[p + M - N] ^ twist(state[p], state[p + 1]) 27 | } 28 | state[p] = state[p + M - N] ^ twist(state[p], state[0]) 29 | } 30 | 31 | /** 32 | * This is a port of Shawn Cokus's implementation of the original Mersenne Twister algorithm (http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/CODES/MTARCOK/mt19937ar-cok.c). 33 | * MT has a very high period of 2^19937. Though the authors of xorshift describe that a high period is not 34 | * very relevant (http://vigna.di.unimi.it/xorshift/). It is four times slower than xoroshiro128plus and 35 | * needs to recompute its state after generating 624 numbers. 36 | * 37 | * ```js 38 | * const gen = new Mt19937(new Date().getTime()) 39 | * console.log(gen.next()) 40 | * ``` 41 | * 42 | * @public 43 | */ 44 | export class Mt19937 { 45 | /** 46 | * @param {number} seed Unsigned 32 bit number 47 | */ 48 | constructor (seed) { 49 | this.seed = seed 50 | const state = new Uint32Array(N) 51 | state[0] = seed 52 | for (let i = 1; i < N; i++) { 53 | state[i] = (math.imul(1812433253, (state[i - 1] ^ (state[i - 1] >>> 30))) + i) & binary.BITS32 54 | } 55 | this._state = state 56 | this._i = 0 57 | nextState(this._state) 58 | } 59 | 60 | /** 61 | * Generate a random signed integer. 62 | * 63 | * @return {Number} A 32 bit signed integer. 64 | */ 65 | next () { 66 | if (this._i === N) { 67 | // need to compute a new state 68 | nextState(this._state) 69 | this._i = 0 70 | } 71 | let y = this._state[this._i++] 72 | y ^= (y >>> 11) 73 | y ^= (y << 7) & 0x9d2c5680 74 | y ^= (y << 15) & 0xefc60000 75 | y ^= (y >>> 18) 76 | return (y >>> 0) / (binary.BITS32 + 1) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /prng/Xoroshiro128plus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module prng 3 | */ 4 | 5 | import { Xorshift32 } from './Xorshift32.js' 6 | import * as binary from '../binary.js' 7 | 8 | /** 9 | * This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures. 10 | * 11 | * This implementation follows the idea of the original xoroshiro128plus implementation, 12 | * but is optimized for the JavaScript runtime. I.e. 13 | * * The operations are performed on 32bit integers (the original implementation works with 64bit values). 14 | * * The initial 128bit state is computed based on a 32bit seed and Xorshift32. 15 | * * This implementation returns two 32bit values based on the 64bit value that is computed by xoroshiro128plus. 16 | * Caution: The last addition step works slightly different than in the original implementation - the add carry of the 17 | * first 32bit addition is not carried over to the last 32bit. 18 | * 19 | * [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c) 20 | */ 21 | export class Xoroshiro128plus { 22 | /** 23 | * @param {number} seed Unsigned 32 bit number 24 | */ 25 | constructor (seed) { 26 | this.seed = seed 27 | // This is a variant of Xoroshiro128plus to fill the initial state 28 | const xorshift32 = new Xorshift32(seed) 29 | this.state = new Uint32Array(4) 30 | for (let i = 0; i < 4; i++) { 31 | this.state[i] = xorshift32.next() * binary.BITS32 32 | } 33 | this._fresh = true 34 | } 35 | 36 | /** 37 | * @return {number} Float/Double in [0,1) 38 | */ 39 | next () { 40 | const state = this.state 41 | if (this._fresh) { 42 | this._fresh = false 43 | return ((state[0] + state[2]) >>> 0) / (binary.BITS32 + 1) 44 | } else { 45 | this._fresh = true 46 | const s0 = state[0] 47 | const s1 = state[1] 48 | const s2 = state[2] ^ s0 49 | const s3 = state[3] ^ s1 50 | // function js_rotl (x, k) { 51 | // k = k - 32 52 | // const x1 = x[0] 53 | // const x2 = x[1] 54 | // x[0] = x2 << k | x1 >>> (32 - k) 55 | // x[1] = x1 << k | x2 >>> (32 - k) 56 | // } 57 | // rotl(s0, 55) // k = 23 = 55 - 32; j = 9 = 32 - 23 58 | state[0] = (s1 << 23 | s0 >>> 9) ^ s2 ^ (s2 << 14 | s3 >>> 18) 59 | state[1] = (s0 << 23 | s1 >>> 9) ^ s3 ^ (s3 << 14) 60 | // rol(s1, 36) // k = 4 = 36 - 32; j = 23 = 32 - 9 61 | state[2] = s3 << 4 | s2 >>> 28 62 | state[3] = s2 << 4 | s3 >>> 28 63 | return (((state[1] + state[3]) >>> 0) / (binary.BITS32 + 1)) 64 | } 65 | } 66 | } 67 | 68 | /* 69 | // Reference implementation 70 | // Source: http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c 71 | // By David Blackman and Sebastiano Vigna 72 | // Who published the reference implementation under Public Domain (CC0) 73 | 74 | #include 75 | #include 76 | 77 | uint64_t s[2]; 78 | 79 | static inline uint64_t rotl(const uint64_t x, int k) { 80 | return (x << k) | (x >> (64 - k)); 81 | } 82 | 83 | uint64_t next(void) { 84 | const uint64_t s0 = s[0]; 85 | uint64_t s1 = s[1]; 86 | s1 ^= s0; 87 | s[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); // a, b 88 | s[1] = rotl(s1, 36); // c 89 | return (s[0] + s[1]) & 0xFFFFFFFF; 90 | } 91 | 92 | int main(void) 93 | { 94 | int i; 95 | s[0] = 1111 | (1337ul << 32); 96 | s[1] = 1234 | (9999ul << 32); 97 | 98 | printf("1000 outputs of genrand_int31()\n"); 99 | for (i=0; i<100; i++) { 100 | printf("%10lu ", i); 101 | printf("%10lu ", next()); 102 | printf("- %10lu ", s[0] >> 32); 103 | printf("%10lu ", (s[0] << 32) >> 32); 104 | printf("%10lu ", s[1] >> 32); 105 | printf("%10lu ", (s[1] << 32) >> 32); 106 | printf("\n"); 107 | // if (i%5==4) printf("\n"); 108 | } 109 | return 0; 110 | } 111 | */ 112 | -------------------------------------------------------------------------------- /prng/Xorshift32.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module prng 3 | */ 4 | 5 | import * as binary from '../binary.js' 6 | 7 | /** 8 | * Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`. 9 | */ 10 | export class Xorshift32 { 11 | /** 12 | * @param {number} seed Unsigned 32 bit number 13 | */ 14 | constructor (seed) { 15 | this.seed = seed 16 | /** 17 | * @type {number} 18 | */ 19 | this._state = seed 20 | } 21 | 22 | /** 23 | * Generate a random signed integer. 24 | * 25 | * @return {Number} A 32 bit signed integer. 26 | */ 27 | next () { 28 | let x = this._state 29 | x ^= x << 13 30 | x ^= x >> 17 31 | x ^= x << 5 32 | this._state = x 33 | return (x >>> 0) / (binary.BITS32 + 1) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility helpers to work with promises. 3 | * 4 | * @module promise 5 | */ 6 | 7 | import * as time from './time.js' 8 | 9 | /** 10 | * @template T 11 | * @callback PromiseResolve 12 | * @param {T|PromiseLike} [result] 13 | */ 14 | 15 | /** 16 | * @template T 17 | * @param {function(PromiseResolve,function(Error):void):any} f 18 | * @return {Promise} 19 | */ 20 | export const create = f => /** @type {Promise} */ (new Promise(f)) 21 | 22 | /** 23 | * @param {function(function():void,function(Error):void):void} f 24 | * @return {Promise} 25 | */ 26 | export const createEmpty = f => new Promise(f) 27 | 28 | /** 29 | * `Promise.all` wait for all promises in the array to resolve and return the result 30 | * @template {unknown[] | []} PS 31 | * 32 | * @param {PS} ps 33 | * @return {Promise<{ -readonly [P in keyof PS]: Awaited }>} 34 | */ 35 | export const all = Promise.all.bind(Promise) 36 | 37 | /** 38 | * @param {Error} [reason] 39 | * @return {Promise} 40 | */ 41 | export const reject = reason => Promise.reject(reason) 42 | 43 | /** 44 | * @template T 45 | * @param {T|void} res 46 | * @return {Promise} 47 | */ 48 | export const resolve = res => Promise.resolve(res) 49 | 50 | /** 51 | * @template T 52 | * @param {T} res 53 | * @return {Promise} 54 | */ 55 | export const resolveWith = res => Promise.resolve(res) 56 | 57 | /** 58 | * @todo Next version, reorder parameters: check, [timeout, [intervalResolution]] 59 | * @deprecated use untilAsync instead 60 | * 61 | * @param {number} timeout 62 | * @param {function():boolean} check 63 | * @param {number} [intervalResolution] 64 | * @return {Promise} 65 | */ 66 | export const until = (timeout, check, intervalResolution = 10) => create((resolve, reject) => { 67 | const startTime = time.getUnixTime() 68 | const hasTimeout = timeout > 0 69 | const untilInterval = () => { 70 | if (check()) { 71 | clearInterval(intervalHandle) 72 | resolve() 73 | } else if (hasTimeout) { 74 | /* c8 ignore else */ 75 | if (time.getUnixTime() - startTime > timeout) { 76 | clearInterval(intervalHandle) 77 | reject(new Error('Timeout')) 78 | } 79 | } 80 | } 81 | const intervalHandle = setInterval(untilInterval, intervalResolution) 82 | }) 83 | 84 | /** 85 | * @param {()=>Promise|boolean} check 86 | * @param {number} timeout 87 | * @param {number} intervalResolution 88 | * @return {Promise} 89 | */ 90 | export const untilAsync = async (check, timeout = 0, intervalResolution = 10) => { 91 | const startTime = time.getUnixTime() 92 | const noTimeout = timeout <= 0 93 | // eslint-disable-next-line no-unmodified-loop-condition 94 | while (noTimeout || time.getUnixTime() - startTime <= timeout) { 95 | if (await check()) return 96 | await wait(intervalResolution) 97 | } 98 | throw new Error('Timeout') 99 | } 100 | 101 | /** 102 | * @param {number} timeout 103 | * @return {Promise} 104 | */ 105 | export const wait = timeout => create((resolve, _reject) => setTimeout(resolve, timeout)) 106 | 107 | /** 108 | * Checks if an object is a promise using ducktyping. 109 | * 110 | * Promises are often polyfilled, so it makes sense to add some additional guarantees if the user of this 111 | * library has some insane environment where global Promise objects are overwritten. 112 | * 113 | * @param {any} p 114 | * @return {boolean} 115 | */ 116 | export const isPromise = p => p instanceof Promise || (p && p.then && p.catch && p.finally) 117 | -------------------------------------------------------------------------------- /promise.test.js: -------------------------------------------------------------------------------- 1 | import * as promise from './promise.js' 2 | import * as t from './testing.js' 3 | import * as time from './time.js' 4 | import * as error from './error.js' 5 | 6 | /** 7 | * @param {Promise} p 8 | * @param {number} min 9 | * @param {number} max 10 | */ 11 | const measureP = (p, min, max) => { 12 | const start = time.getUnixTime() 13 | return p.then(() => { 14 | const duration = time.getUnixTime() - start 15 | t.assert(duration <= max, 'Expected promise to take less time') 16 | t.assert(duration >= min, 'Expected promise to take more time') 17 | }) 18 | } 19 | 20 | /** 21 | * @template T 22 | * @param {Promise} p 23 | * @return {Promise} 24 | */ 25 | const failsP = p => promise.create((resolve, reject) => p.then(() => reject(error.create('Promise should fail')), resolve)) 26 | 27 | /** 28 | * @param {t.TestCase} _tc 29 | */ 30 | export const testRepeatPromise = async _tc => { 31 | t.assert(promise.createEmpty(r => r()).constructor === Promise, 'p.create() creates a Promise') 32 | t.assert(promise.resolve().constructor === Promise, 'p.reject() creates a Promise') 33 | const rejectedP = promise.reject() 34 | t.assert(rejectedP.constructor === Promise, 'p.reject() creates a Promise') 35 | rejectedP.catch(() => {}) 36 | await promise.createEmpty(r => r()) 37 | await failsP(promise.reject()) 38 | await promise.resolve() 39 | await measureP(promise.wait(10), 7, 1000) 40 | await measureP(failsP(promise.until(15, () => false)), 15, 1000) 41 | await measureP(failsP(promise.untilAsync(() => false, 15)), 15, 1000) 42 | const startTime = time.getUnixTime() 43 | await measureP(promise.until(0, () => (time.getUnixTime() - startTime) > 100), 100, 1000) 44 | const startTime2 = time.getUnixTime() 45 | await measureP(promise.untilAsync(() => (time.getUnixTime() - startTime2) > 100), 100, 1000) 46 | await promise.all([promise.wait(5), promise.wait(10)]) 47 | } 48 | 49 | /** 50 | * @param {t.TestCase} _tc 51 | */ 52 | export const testispromise = _tc => { 53 | t.assert(promise.isPromise(new Promise(() => {}))) 54 | t.assert(promise.isPromise(promise.create(() => {}))) 55 | const rej = promise.reject() 56 | t.assert(promise.isPromise(rej)) 57 | rej.catch(() => {}) 58 | t.assert(promise.isPromise(promise.resolve())) 59 | t.assert(promise.isPromise({ then: () => {}, catch: () => {}, finally: () => {} })) 60 | t.fails(() => { 61 | t.assert(promise.isPromise({ then: () => {}, catch: () => {} })) 62 | }) 63 | } 64 | 65 | /** 66 | * @param {t.TestCase} _tc 67 | */ 68 | export const testTypings = async _tc => { 69 | const ps = await promise.all([promise.resolveWith(4), 'string']) 70 | /** 71 | * @type {number} 72 | */ 73 | const a = ps[0] 74 | /** 75 | * @type {string} 76 | */ 77 | const b = ps[1] 78 | t.assert(typeof a === 'number' && typeof b === 'string') 79 | } 80 | -------------------------------------------------------------------------------- /queue.js: -------------------------------------------------------------------------------- 1 | export class QueueNode { 2 | constructor () { 3 | /** 4 | * @type {QueueNode|null} 5 | */ 6 | this.next = null 7 | } 8 | } 9 | 10 | /** 11 | * @template V 12 | */ 13 | export class QueueValue extends QueueNode { 14 | /** 15 | * @param {V} v 16 | */ 17 | constructor (v) { 18 | super() 19 | this.v = v 20 | } 21 | } 22 | 23 | /** 24 | * @template {QueueNode} N 25 | */ 26 | export class Queue { 27 | constructor () { 28 | /** 29 | * @type {N | null} 30 | */ 31 | this.start = null 32 | /** 33 | * @type {N | null} 34 | */ 35 | this.end = null 36 | } 37 | } 38 | 39 | /** 40 | * @note The queue implementation is experimental and unfinished. 41 | * Don't use this in production yet. 42 | * 43 | * @template {QueueNode} N 44 | * @return {Queue} 45 | */ 46 | export const create = () => new Queue() 47 | 48 | /** 49 | * @param {Queue} queue 50 | */ 51 | export const isEmpty = queue => queue.start === null 52 | 53 | /** 54 | * @template {Queue} Q 55 | * @param {Q} queue 56 | * @param {Q extends Queue ? N : never} n 57 | */ 58 | export const enqueue = (queue, n) => { 59 | if (queue.end !== null) { 60 | queue.end.next = n 61 | queue.end = n 62 | } else { 63 | queue.end = n 64 | queue.start = n 65 | } 66 | } 67 | 68 | /** 69 | * @template {QueueNode} N 70 | * @param {Queue} queue 71 | * @return {N | null} 72 | */ 73 | export const dequeue = queue => { 74 | const n = queue.start 75 | if (n !== null) { 76 | // @ts-ignore 77 | queue.start = n.next 78 | if (queue.start === null) { 79 | queue.end = null 80 | } 81 | return n 82 | } 83 | return null 84 | } 85 | -------------------------------------------------------------------------------- /queue.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as queue from './queue.js' 3 | 4 | /** 5 | * @param {t.TestCase} _tc 6 | */ 7 | export const testEnqueueDequeue = _tc => { 8 | const N = 30 9 | /** 10 | * @type {queue.Queue>} 11 | */ 12 | const q = queue.create() 13 | t.assert(queue.isEmpty(q)) 14 | t.assert(queue.dequeue(q) === null) 15 | for (let i = 0; i < N; i++) { 16 | queue.enqueue(q, new queue.QueueValue(i)) 17 | t.assert(!queue.isEmpty(q)) 18 | } 19 | for (let i = 0; i < N; i++) { 20 | const item = queue.dequeue(q) 21 | t.assert(item !== null && item.v === i) 22 | } 23 | t.assert(queue.isEmpty(q)) 24 | t.assert(queue.dequeue(q) === null) 25 | for (let i = 0; i < N; i++) { 26 | queue.enqueue(q, new queue.QueueValue(i)) 27 | t.assert(!queue.isEmpty(q)) 28 | } 29 | for (let i = 0; i < N; i++) { 30 | const item = queue.dequeue(q) 31 | t.assert(item !== null && item.v === i) 32 | } 33 | t.assert(queue.isEmpty(q)) 34 | t.assert(queue.dequeue(q) === null) 35 | } 36 | -------------------------------------------------------------------------------- /random.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Isomorphic module for true random numbers / buffers / uuids. 3 | * 4 | * Attention: falls back to Math.random if the browser does not support crypto. 5 | * 6 | * @module random 7 | */ 8 | 9 | import * as math from './math.js' 10 | import * as binary from './binary.js' 11 | import { getRandomValues } from 'lib0/webcrypto' 12 | 13 | export const rand = Math.random 14 | 15 | export const uint32 = () => getRandomValues(new Uint32Array(1))[0] 16 | 17 | export const uint53 = () => { 18 | const arr = getRandomValues(new Uint32Array(8)) 19 | return (arr[0] & binary.BITS21) * (binary.BITS32 + 1) + (arr[1] >>> 0) 20 | } 21 | 22 | /** 23 | * @template T 24 | * @param {Array} arr 25 | * @return {T} 26 | */ 27 | export const oneOf = arr => arr[math.floor(rand() * arr.length)] 28 | 29 | // @ts-ignore 30 | const uuidv4Template = [1e7] + -1e3 + -4e3 + -8e3 + -1e11 31 | 32 | /** 33 | * @return {string} 34 | */ 35 | export const uuidv4 = () => uuidv4Template.replace(/[018]/g, /** @param {number} c */ c => 36 | (c ^ uint32() & 15 >> c / 4).toString(16) 37 | ) 38 | -------------------------------------------------------------------------------- /random.test.js: -------------------------------------------------------------------------------- 1 | import * as random from './random.js' 2 | import * as t from './testing.js' 3 | import * as binary from './binary.js' 4 | import * as math from './math.js' 5 | import * as number from './number.js' 6 | 7 | /** 8 | * @param {t.TestCase} tc 9 | */ 10 | export const testRandom = tc => { 11 | const res = random.oneOf([1, 2, 3]) 12 | t.assert(res > 0) 13 | } 14 | 15 | /** 16 | * @param {t.TestCase} tc 17 | */ 18 | export const testUint32 = tc => { 19 | const iterations = 10000 20 | let largest = 0 21 | let smallest = number.HIGHEST_INT32 22 | let newNum = 0 23 | let lenSum = 0 24 | let ones = 0 25 | for (let i = 0; i < iterations; i++) { 26 | newNum = random.uint32() 27 | lenSum += newNum.toString().length 28 | ones += newNum.toString(2).split('').filter(x => x === '1').length 29 | if (newNum > largest) { 30 | largest = newNum 31 | } 32 | if (newNum < smallest) { 33 | smallest = newNum 34 | } 35 | } 36 | t.info(`Largest number generated is ${largest} (0x${largest.toString(16)})`) 37 | t.info(`Smallest number generated is ${smallest} (0x${smallest.toString(16)})`) 38 | t.info(`Average decimal length of number is ${lenSum / iterations}`) 39 | t.info(`Average number of 1s in number is ${ones / iterations} (expecting ~16)`) 40 | t.assert(((largest & binary.BITS32) >>> 0) === largest, 'Largest number is 32 bits long.') 41 | t.assert(((smallest & binary.BITS32) >>> 0) === smallest, 'Smallest number is 32 bits long.') 42 | } 43 | 44 | /** 45 | * @param {t.TestCase} tc 46 | */ 47 | export const testUint53 = tc => { 48 | const iterations = 10000 49 | let largest = 0 50 | let smallest = number.MAX_SAFE_INTEGER 51 | let newNum = 0 52 | let lenSum = 0 53 | let ones = 0 54 | for (let i = 0; i < iterations; i++) { 55 | newNum = random.uint53() 56 | lenSum += newNum.toString().length 57 | ones += newNum.toString(2).split('').filter(x => x === '1').length 58 | if (newNum > largest) { 59 | largest = newNum 60 | } 61 | if (newNum < smallest) { 62 | smallest = newNum 63 | } 64 | } 65 | t.info(`Largest number generated is ${largest}`) 66 | t.info(`Smallest number generated is ${smallest}`) 67 | t.info(`Average decimal length of number is ${lenSum / iterations}`) 68 | t.info(`Average number of 1s in number is ${ones / iterations} (expecting ~26.5)`) 69 | t.assert(largest > number.MAX_SAFE_INTEGER * 0.9) 70 | } 71 | 72 | /** 73 | * @param {t.TestCase} tc 74 | */ 75 | export const testUuidv4 = tc => { 76 | t.info(`Generated a UUIDv4: ${random.uuidv4()}`) 77 | } 78 | 79 | /** 80 | * @param {t.TestCase} tc 81 | */ 82 | export const testUuidv4Overlaps = tc => { 83 | t.skip(!t.production) 84 | const iterations = t.extensive ? 1000000 : 10000 85 | const uuids = new Set() 86 | for (let i = 0; i < iterations; i++) { 87 | const uuid = random.uuidv4() 88 | if (uuids.has(uuid)) { 89 | t.fail('uuid already exists') 90 | } else { 91 | uuids.add(uuid) 92 | } 93 | if (uuids.size % (iterations / 20) === 0) { 94 | t.info(`${math.round(uuids.size * 100 / iterations)}% complete`) 95 | } 96 | } 97 | t.assert(uuids.size === iterations) 98 | } 99 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | 3 | const roots = ['./', './crypto/', './hash/'] 4 | 5 | const files = roots.map(root => fs.readdirSync(root).map(f => root + f)).flat().filter(file => /(? new Set() 8 | 9 | /** 10 | * @template T 11 | * @param {Set} set 12 | * @return {Array} 13 | */ 14 | export const toArray = set => Array.from(set) 15 | 16 | /** 17 | * @template T 18 | * @param {Set} set 19 | * @return {T} 20 | */ 21 | export const first = set => 22 | set.values().next().value ?? undefined 23 | 24 | /** 25 | * @template T 26 | * @param {Iterable} entries 27 | * @return {Set} 28 | */ 29 | export const from = entries => new Set(entries) 30 | -------------------------------------------------------------------------------- /set.test.js: -------------------------------------------------------------------------------- 1 | import * as t from './testing.js' 2 | import * as set from './set.js' 3 | 4 | /** 5 | * @param {t.TestCase} _tc 6 | */ 7 | export const testFirst = _tc => { 8 | const two = set.from(['a', 'b']) 9 | const one = set.from(['b']) 10 | const zero = set.create() 11 | t.assert(set.first(two) === 'a') 12 | t.assert(set.first(one) === 'b') 13 | t.assert(set.first(zero) === undefined) 14 | t.compare(set.toArray(one), ['b']) 15 | } 16 | -------------------------------------------------------------------------------- /sort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Efficient sort implementations. 3 | * 4 | * Note: These sort implementations were created to compare different sorting algorithms in JavaScript. 5 | * Don't use them if you don't know what you are doing. Native Array.sort is almost always a better choice. 6 | * 7 | * @module sort 8 | */ 9 | 10 | import * as math from './math.js' 11 | 12 | /** 13 | * @template T 14 | * @param {Array} arr 15 | * @param {number} lo 16 | * @param {number} hi 17 | * @param {function(T,T):number} compare 18 | */ 19 | export const _insertionSort = (arr, lo, hi, compare) => { 20 | for (let i = lo + 1; i <= hi; i++) { 21 | for (let j = i; j > 0 && compare(arr[j - 1], arr[j]) > 0; j--) { 22 | const tmp = arr[j] 23 | arr[j] = arr[j - 1] 24 | arr[j - 1] = tmp 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * @template T 31 | * @param {Array} arr 32 | * @param {function(T,T):number} compare 33 | * @return {void} 34 | */ 35 | export const insertionSort = (arr, compare) => { 36 | _insertionSort(arr, 0, arr.length - 1, compare) 37 | } 38 | 39 | /** 40 | * @template T 41 | * @param {Array} arr 42 | * @param {number} lo 43 | * @param {number} hi 44 | * @param {function(T,T):number} compare 45 | */ 46 | const _quickSort = (arr, lo, hi, compare) => { 47 | if (hi - lo < 42) { 48 | _insertionSort(arr, lo, hi, compare) 49 | } else { 50 | const pivot = arr[math.floor((lo + hi) / 2)] 51 | let i = lo 52 | let j = hi 53 | while (true) { 54 | while (compare(pivot, arr[i]) > 0) { 55 | i++ 56 | } 57 | while (compare(arr[j], pivot) > 0) { 58 | j-- 59 | } 60 | if (i >= j) { 61 | break 62 | } 63 | // swap arr[i] with arr[j] 64 | // and increment i and j 65 | const arri = arr[i] 66 | arr[i++] = arr[j] 67 | arr[j--] = arri 68 | } 69 | _quickSort(arr, lo, j, compare) 70 | _quickSort(arr, j + 1, hi, compare) 71 | } 72 | } 73 | 74 | /** 75 | * This algorithm beats Array.prototype.sort in Chrome only with arrays with 10 million entries. 76 | * In most cases [].sort will do just fine. Make sure to performance test your use-case before you 77 | * integrate this algorithm. 78 | * 79 | * Note that Chrome's sort is now a stable algorithm (Timsort). Quicksort is not stable. 80 | * 81 | * @template T 82 | * @param {Array} arr 83 | * @param {function(T,T):number} compare 84 | * @return {void} 85 | */ 86 | export const quicksort = (arr, compare) => { 87 | _quickSort(arr, 0, arr.length - 1, compare) 88 | } 89 | -------------------------------------------------------------------------------- /statistics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility helpers for generating statistics. 3 | * 4 | * @module statistics 5 | */ 6 | 7 | import * as math from './math.js' 8 | 9 | /** 10 | * @param {Array} arr Array of values 11 | * @return {number} Returns null if the array is empty 12 | */ 13 | export const median = arr => arr.length === 0 ? NaN : (arr.length % 2 === 1 ? arr[(arr.length - 1) / 2] : (arr[math.floor((arr.length - 1) / 2)] + arr[math.ceil((arr.length - 1) / 2)]) / 2) 14 | 15 | /** 16 | * @param {Array} arr 17 | * @return {number} 18 | */ 19 | export const average = arr => arr.reduce(math.add, 0) / arr.length 20 | -------------------------------------------------------------------------------- /statistics.test.js: -------------------------------------------------------------------------------- 1 | import * as statistics from './statistics.js' 2 | import * as t from './testing.js' 3 | import * as math from './math.js' 4 | 5 | /** 6 | * @param {t.TestCase} tc 7 | */ 8 | export const testMedian = tc => { 9 | t.assert(math.isNaN(statistics.median([])), 'median([]) = NaN') 10 | t.assert(statistics.median([1]) === 1, 'median([x]) = x') 11 | t.assert(statistics.median([1, 2, 3]) === 2, 'median([a,b,c]) = b') 12 | t.assert(statistics.median([1, 2, 3, 4]) === (2 + 3) / 2, 'median([a,b,c,d]) = (b+c)/2') 13 | t.assert(statistics.median([1, 2, 3, 4, 5]) === 3, 'median([a,b,c,d,e]) = c') 14 | t.assert(statistics.median([1, 2, 3, 4, 5, 6]) === (3 + 4) / 2, 'median([a,b,c,d,e,f]) = (c+d)/2') 15 | } 16 | -------------------------------------------------------------------------------- /storage.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | /** 4 | * Isomorphic variable storage. 5 | * 6 | * Uses LocalStorage in the browser and falls back to in-memory storage. 7 | * 8 | * @module storage 9 | */ 10 | 11 | /* c8 ignore start */ 12 | class VarStoragePolyfill { 13 | constructor () { 14 | this.map = new Map() 15 | } 16 | 17 | /** 18 | * @param {string} key 19 | * @param {any} newValue 20 | */ 21 | setItem (key, newValue) { 22 | this.map.set(key, newValue) 23 | } 24 | 25 | /** 26 | * @param {string} key 27 | */ 28 | getItem (key) { 29 | return this.map.get(key) 30 | } 31 | } 32 | /* c8 ignore stop */ 33 | 34 | /** 35 | * @type {any} 36 | */ 37 | let _localStorage = new VarStoragePolyfill() 38 | let usePolyfill = true 39 | 40 | /* c8 ignore start */ 41 | try { 42 | // if the same-origin rule is violated, accessing localStorage might thrown an error 43 | if (typeof localStorage !== 'undefined' && localStorage) { 44 | _localStorage = localStorage 45 | usePolyfill = false 46 | } 47 | } catch (e) { } 48 | /* c8 ignore stop */ 49 | 50 | /** 51 | * This is basically localStorage in browser, or a polyfill in nodejs 52 | */ 53 | /* c8 ignore next */ 54 | export const varStorage = _localStorage 55 | 56 | /** 57 | * A polyfill for `addEventListener('storage', event => {..})` that does nothing if the polyfill is being used. 58 | * 59 | * @param {function({ key: string, newValue: string, oldValue: string }): void} eventHandler 60 | * @function 61 | */ 62 | /* c8 ignore next */ 63 | export const onChange = eventHandler => usePolyfill || addEventListener('storage', /** @type {any} */ (eventHandler)) 64 | 65 | /** 66 | * A polyfill for `removeEventListener('storage', event => {..})` that does nothing if the polyfill is being used. 67 | * 68 | * @param {function({ key: string, newValue: string, oldValue: string }): void} eventHandler 69 | * @function 70 | */ 71 | /* c8 ignore next */ 72 | export const offChange = eventHandler => usePolyfill || removeEventListener('storage', /** @type {any} */ (eventHandler)) 73 | -------------------------------------------------------------------------------- /storage.test.js: -------------------------------------------------------------------------------- 1 | import * as storage from './storage.js' 2 | import * as t from './testing.js' 3 | 4 | /** 5 | * @param {t.TestCase} tc 6 | */ 7 | export const testStorageModule = tc => { 8 | const s = storage.varStorage 9 | /** 10 | * @type {any} 11 | */ 12 | let lastEvent = null 13 | storage.onChange(event => { 14 | lastEvent = event 15 | }) 16 | s.setItem('key', 'value') 17 | t.assert(lastEvent === null) 18 | } 19 | -------------------------------------------------------------------------------- /string.js: -------------------------------------------------------------------------------- 1 | import * as array from './array.js' 2 | 3 | /** 4 | * Utility module to work with strings. 5 | * 6 | * @module string 7 | */ 8 | 9 | export const fromCharCode = String.fromCharCode 10 | export const fromCodePoint = String.fromCodePoint 11 | 12 | /** 13 | * The largest utf16 character. 14 | * Corresponds to Uint8Array([255, 255]) or charcodeof(2x2^8) 15 | */ 16 | export const MAX_UTF16_CHARACTER = fromCharCode(65535) 17 | 18 | /** 19 | * @param {string} s 20 | * @return {string} 21 | */ 22 | const toLowerCase = s => s.toLowerCase() 23 | 24 | const trimLeftRegex = /^\s*/g 25 | 26 | /** 27 | * @param {string} s 28 | * @return {string} 29 | */ 30 | export const trimLeft = s => s.replace(trimLeftRegex, '') 31 | 32 | const fromCamelCaseRegex = /([A-Z])/g 33 | 34 | /** 35 | * @param {string} s 36 | * @param {string} separator 37 | * @return {string} 38 | */ 39 | export const fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, match => `${separator}${toLowerCase(match)}`)) 40 | 41 | /** 42 | * Compute the utf8ByteLength 43 | * @param {string} str 44 | * @return {number} 45 | */ 46 | export const utf8ByteLength = str => unescape(encodeURIComponent(str)).length 47 | 48 | /** 49 | * @param {string} str 50 | * @return {Uint8Array} 51 | */ 52 | export const _encodeUtf8Polyfill = str => { 53 | const encodedString = unescape(encodeURIComponent(str)) 54 | const len = encodedString.length 55 | const buf = new Uint8Array(len) 56 | for (let i = 0; i < len; i++) { 57 | buf[i] = /** @type {number} */ (encodedString.codePointAt(i)) 58 | } 59 | return buf 60 | } 61 | 62 | /* c8 ignore next */ 63 | export const utf8TextEncoder = /** @type {TextEncoder} */ (typeof TextEncoder !== 'undefined' ? new TextEncoder() : null) 64 | 65 | /** 66 | * @param {string} str 67 | * @return {Uint8Array} 68 | */ 69 | export const _encodeUtf8Native = str => utf8TextEncoder.encode(str) 70 | 71 | /** 72 | * @param {string} str 73 | * @return {Uint8Array} 74 | */ 75 | /* c8 ignore next */ 76 | export const encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill 77 | 78 | /** 79 | * @param {Uint8Array} buf 80 | * @return {string} 81 | */ 82 | export const _decodeUtf8Polyfill = buf => { 83 | let remainingLen = buf.length 84 | let encodedString = '' 85 | let bufPos = 0 86 | while (remainingLen > 0) { 87 | const nextLen = remainingLen < 10000 ? remainingLen : 10000 88 | const bytes = buf.subarray(bufPos, bufPos + nextLen) 89 | bufPos += nextLen 90 | // Starting with ES5.1 we can supply a generic array-like object as arguments 91 | encodedString += String.fromCodePoint.apply(null, /** @type {any} */ (bytes)) 92 | remainingLen -= nextLen 93 | } 94 | return decodeURIComponent(escape(encodedString)) 95 | } 96 | 97 | /* c8 ignore next */ 98 | export let utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }) 99 | 100 | /* c8 ignore start */ 101 | if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) { 102 | // Safari doesn't handle BOM correctly. 103 | // This fixes a bug in Safari 13.0.5 where it produces a BOM the first time it is called. 104 | // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the first call and 105 | // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the second call 106 | // Another issue is that from then on no BOM chars are recognized anymore 107 | /* c8 ignore next */ 108 | utf8TextDecoder = null 109 | } 110 | /* c8 ignore stop */ 111 | 112 | /** 113 | * @param {Uint8Array} buf 114 | * @return {string} 115 | */ 116 | export const _decodeUtf8Native = buf => /** @type {TextDecoder} */ (utf8TextDecoder).decode(buf) 117 | 118 | /** 119 | * @param {Uint8Array} buf 120 | * @return {string} 121 | */ 122 | /* c8 ignore next */ 123 | export const decodeUtf8 = utf8TextDecoder ? _decodeUtf8Native : _decodeUtf8Polyfill 124 | 125 | /** 126 | * @param {string} str The initial string 127 | * @param {number} index Starting position 128 | * @param {number} remove Number of characters to remove 129 | * @param {string} insert New content to insert 130 | */ 131 | export const splice = (str, index, remove, insert = '') => str.slice(0, index) + insert + str.slice(index + remove) 132 | 133 | /** 134 | * @param {string} source 135 | * @param {number} n 136 | */ 137 | export const repeat = (source, n) => array.unfold(n, () => source).join('') 138 | 139 | /** 140 | * Escape HTML characters &,<,>,'," to their respective HTML entities &,<,>,'," 141 | * 142 | * @param {string} str 143 | */ 144 | export const escapeHTML = str => 145 | str.replace(/[&<>'"]/g, r => /** @type {string} */ ({ 146 | '&': '&', 147 | '<': '<', 148 | '>': '>', 149 | "'": ''', 150 | '"': '"' 151 | }[r])) 152 | 153 | /** 154 | * Reverse of `escapeHTML` 155 | * 156 | * @param {string} str 157 | */ 158 | export const unescapeHTML = str => 159 | str.replace(/&|<|>|'|"/g, r => /** @type {string} */ ({ 160 | '&': '&', 161 | '<': '<', 162 | '>': '>', 163 | ''': "'", 164 | '"': '"' 165 | }[r])) 166 | -------------------------------------------------------------------------------- /string.test.js: -------------------------------------------------------------------------------- 1 | import * as prng from './prng.js' 2 | import * as string from './string.js' 3 | import * as t from './testing.js' 4 | 5 | /** 6 | * @param {t.TestCase} _tc 7 | */ 8 | export const testUtilities = _tc => { 9 | t.assert(string.repeat('1', 3) === '111') 10 | t.assert(string.repeat('1', 0) === '') 11 | t.assert(string.repeat('1', 1) === '1') 12 | } 13 | 14 | /** 15 | * @param {t.TestCase} _tc 16 | */ 17 | export const testLowercaseTransformation = _tc => { 18 | t.compareStrings(string.fromCamelCase('ThisIsATest', ' '), 'this is a test') 19 | t.compareStrings(string.fromCamelCase('Testing', ' '), 'testing') 20 | t.compareStrings(string.fromCamelCase('testingThis', ' '), 'testing this') 21 | t.compareStrings(string.fromCamelCase('testYAY', ' '), 'test y a y') 22 | } 23 | 24 | /** 25 | * @param {t.TestCase} tc 26 | */ 27 | export const testRepeatStringUtf8Encoding = tc => { 28 | t.skip(!string.utf8TextDecoder) 29 | const str = prng.utf16String(tc.prng, 1000000) 30 | let nativeResult, polyfilledResult 31 | t.measureTime('TextEncoder utf8 encoding', () => { 32 | nativeResult = string._encodeUtf8Native(str) 33 | }) 34 | t.measureTime('Polyfilled utf8 encoding', () => { 35 | polyfilledResult = string._encodeUtf8Polyfill(str) 36 | }) 37 | t.compare(nativeResult, polyfilledResult, 'Encoded utf8 buffers match') 38 | } 39 | 40 | /** 41 | * @param {t.TestCase} tc 42 | */ 43 | export const testRepeatStringUtf8Decoding = tc => { 44 | t.skip(!string.utf8TextDecoder) 45 | const buf = string.encodeUtf8(prng.utf16String(tc.prng, 1000000)) 46 | let nativeResult, polyfilledResult 47 | t.measureTime('TextEncoder utf8 decoding', () => { 48 | nativeResult = string._decodeUtf8Native(buf) 49 | }) 50 | t.measureTime('Polyfilled utf8 decoding', () => { 51 | polyfilledResult = string._decodeUtf8Polyfill(buf) 52 | }) 53 | t.compare(nativeResult, polyfilledResult, 'Decoded utf8 buffers match') 54 | } 55 | 56 | /** 57 | * @param {t.TestCase} _tc 58 | */ 59 | export const testBomEncodingDecoding = _tc => { 60 | const bomStr = 'bom' 61 | t.assert(bomStr.length === 4) 62 | const polyfilledResult = string._decodeUtf8Polyfill(string._encodeUtf8Polyfill(bomStr)) 63 | t.assert(polyfilledResult.length === 4) 64 | t.assert(polyfilledResult === bomStr) 65 | if (string.utf8TextDecoder) { 66 | const nativeResult = string._decodeUtf8Native(string._encodeUtf8Native(bomStr)) 67 | t.assert(nativeResult === polyfilledResult) 68 | } 69 | } 70 | 71 | /** 72 | * @param {t.TestCase} _tc 73 | */ 74 | export const testSplice = _tc => { 75 | const initial = 'xyz' 76 | t.compareStrings(string.splice(initial, 0, 2), 'z') 77 | t.compareStrings(string.splice(initial, 0, 2, 'u'), 'uz') 78 | } 79 | 80 | /** 81 | * @param {t.TestCase} _tc 82 | */ 83 | export const testHtmlEscape = _tc => { 84 | const cases = [ 85 | { s: 'hello