diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65eef93 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +db diff --git a/demo.html b/demo.html index 6c69bec..b9aace2 100644 --- a/demo.html +++ b/demo.html @@ -1,28 +1,49 @@ -

Hi guys!

- -You can view the raw /feed, or play with the UI below. - -

Feed

-

-
-
- - - - - - + + + + +

Hi guys!

+ + You can view the raw /feed, or play with the UI below. + +

Feed

+ +
+ + + + + + + + diff --git a/demo.js b/demo.js index 00f1f99..323f060 100644 --- a/demo.js +++ b/demo.js @@ -1,13 +1,16 @@ var fs = require('fs'), - app = require('express')(), + express = require('express'), braidmail = require('./index.js') +var app = express() + app.use(free_the_cors) // Host some simple HTML sendfile = (f) => (req, res) => res.sendFile(f, {root:'.'}) app.get('/', sendfile('demo.html')) +app.use('/public', express.static('public')) app.use(braidmail) // Spin up the server diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d20f12e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1235 @@ +{ + "name": "braidmail", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "braidmail", + "version": "0.1.0", + "dependencies": { + "braid-http": "^0.3.7", + "diffhtml": "^1.0.0-beta.30", + "express": "^4.19.2", + "statebus": "^6.1.28" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/bcrypt-nodejs": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/bcrypt-nodejs/-/bcrypt-nodejs-0.0.3.tgz", + "integrity": "sha512-NmTbLm867btBHCBZ222FQXkQKzecB0KG6pTXFa6NeTVZaSnLfCsx7EK2PL3J+kX8xJThUquEBbhimRCKKZX9zA==", + "deprecated": "bcrypt-nodejs is no longer actively maintained. Please use bcrypt or bcryptjs. See https://github.com/kelektiv/node.bcrypt.js/wiki/bcrypt-vs-brypt.js to learn more about these two options" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/braid-http": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/braid-http/-/braid-http-0.3.7.tgz", + "integrity": "sha512-Z+Klf4Sc1G4Z52eOLyhk5YsOhypCumMFNIXWO7bopwYvUvBWaVdVABwGsyYUUUFwurPvUXN905PBNm8sd5BHHg==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^2.6.1", + "node-web-streams": "^0.2.2", + "parse-headers": "^2.0.3" + } + }, + "node_modules/braidify": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/braidify/-/braidify-0.1.23.tgz", + "integrity": "sha512-nHiXGsziiBzkxtR0A5EZwW8ws05U9tVt7VCHwl87IUj16ixHaWkBU58OBz6a8wVynbFWLHyq1AiJXQwUV0rqnw==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^2.6.1", + "node-web-streams": "^0.2.2", + "parse-headers": "^2.0.3" + } + }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/coffeescript": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.7.0.tgz", + "integrity": "sha512-hzWp6TUE2d/jCcN67LrW1eh5b/rSDKQK6oD6VMLlggYVUUFexgTH9z3dNYihzX4RMhze5FTUsUmOXViJKFQR/A==", + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diffhtml": { + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/diffhtml/-/diffhtml-1.0.0-beta.30.tgz", + "integrity": "sha512-yrBteaq309reltj+kAnGqsnpJyQSqmCkd5LAJNAcWNmlgvu+PbiUm9bNxdRYi21BQsQsljTxrjs+AWeeHHghWA==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-web-streams": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/node-web-streams/-/node-web-streams-0.2.2.tgz", + "integrity": "sha512-TKWGEUb0AgDA+8+YFDYms2fgsTK87etvMpjJW9qdieXQwcn8IgIFHL2Xohorw7c19TP3dC+Ttur+oO7/c9h6vg==", + "dependencies": { + "is-stream": "^1.1.0", + "web-streams-polyfill": "git://github.com/gwicke/web-streams-polyfill#spec_performance_improvements" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/statebus": { + "version": "6.1.28", + "resolved": "https://registry.npmjs.org/statebus/-/statebus-6.1.28.tgz", + "integrity": "sha512-FSkKlzO2WYXeAqYJ859XYJ1/3Ar87V3R4MnkVzLv3LInrIhvMJzjBecNWsAXWJiJjd2F9AbXoA7MWWYaRAgS+w==", + "dependencies": { + "bcrypt-nodejs": "0.0.3", + "braidify": "^0.1.18", + "chokidar": "^3.4.0", + "coffeescript": "^2.3.1", + "cookie": "^0.3.1", + "express": "^4.15.3", + "sockjs": "^0.3.18", + "websocket": "^1.0.24" + } + }, + "node_modules/statebus/node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "1.2.2", + "resolved": "git+ssh://git@github.com/gwicke/web-streams-polyfill.git#42c488428adea1dc0c0245014e4896ad456b1ded", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "engines": { + "node": ">=0.10.32" + } + } + } +} diff --git a/package.json b/package.json index d478c56..a2db98d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "repository": "braid-org/braidmail", "dependencies": { "braid-http": "^0.3.7", - "express": "^4.19.2" + "diffhtml": "^1.0.0-beta.30", + "express": "^4.19.2", + "statebus": "^6.1.28" }, "files": [ "index.js" diff --git a/public/_statebus.js b/public/_statebus.js new file mode 100644 index 0000000..77b03c2 --- /dev/null +++ b/public/_statebus.js @@ -0,0 +1,11 @@ +import "./statebus/statebus.js" +import "./statebus/client-library.js" +import "./statebus/braidify-client.js" + +const statebus = window.bus +export const state = statebus.state +export default statebus + +statebus.libs.localstorage('ls/*') +statebus.libs.http_out('/*', '/') +window.state = state diff --git a/public/module.js b/public/module.js new file mode 100644 index 0000000..6b5d775 --- /dev/null +++ b/public/module.js @@ -0,0 +1,158 @@ +import statebus, { state } from 'statebus' +import { innerHTML } from 'diffhtml' + +const logs = {} + +export function insights() { + return logs +} + +function insight(name, link) { + if(!logs[`${name}:${link}`]) { + logs[`${name}:${link}`] = 0 + } + logs[`${name}:${link}`] += 1 +} + +const CREATE_EVENT = 'create' + +const observableEvents = [CREATE_EVENT] + +function update(link, target, compositor) { + insight('module:update', link) + const html = compositor(target) + if(html) innerHTML(target, html) +} + +function draw(link, compositor) { + insight('module:draw', link) + listen(CREATE_EVENT, link, (event) => { + statebus.reactive( + update.bind(null, link, event.target, compositor) + )() + }) +} + +function style(link, stylesheet) { + insight('module:style', link) + const styles = ` + + `; + + document.body.insertAdjacentHTML("beforeend", styles) +} + +export function learn(link) { + insight('module:learn', link) + return state[link] || {} +} + +export function teach(link, knowledge, nuance = (s, p) => ({...s,...p})) { + insight('module:teach', link) + const current = statebus.cache[link] || {} + state[link] = nuance(current.val || {}, knowledge); +} + +export function when(link1, type, link2, callback) { + const link = `${link1} ${link2}` + insight('module:when:'+type, link) + listen(type, link, callback) +} + +export default function module(link, initialState = {}) { + insight('module', link) + teach(link, initialState) + + return { + link, + learn: learn.bind(null, link), + draw: draw.bind(null, link), + style: style.bind(null, link), + when: when.bind(null, link), + teach: teach.bind(null, link), + } +} + +export function subscribe(fun) { + notifications[fun.toString] = fun +} + +export function unsubscribe(fun) { + if(notifications[fun.toString]) { + delete notifications[fun.toString] + } +} + +export function listen(type, link, handler = () => null) { + const callback = (event) => { + if( + event.target && + event.target.matches && + event.target.matches(link) + ) { + + insight('module:listen:'+type, link) + handler.call(null, event); + } + }; + + document.addEventListener(type, callback, true); + + if(observableEvents.includes(type)) { + observe(link); + } + + return function unlisten() { + if(type === CREATE_EVENT) { + disregard(link); + } + + document.removeEventListener(type, callback, true); + } +} + +let links = [] + +function observe(link) { + links = [...new Set([...links, link])]; + maybeCreateReactive([...document.querySelectorAll(link)]) +} + +function disregard(link) { + const index = links.indexOf(link); + if(index >= 0) { + links = [ + ...links.slice(0, index), + ...links.slice(index + 1) + ]; + } +} + +function maybeCreateReactive(targets) { + targets + .filter(x => !x.reactive) + .forEach(dispatchCreate) +} + +function getSubscribers({ target }) { + if(links.length > 0) + return [...target.querySelectorAll(links.join(', '))]; + else + return [] +} + +function dispatchCreate(target) { + insight('module:create', target.localName) + if(!target.id) target.id = self.crypto.randomUUID() + target.dispatchEvent(new Event(CREATE_EVENT)) + target.reactive = true +} + +new MutationObserver((mutationsList) => { + const targets = [...mutationsList] + .map(getSubscribers) + .flatMap(x => x) + maybeCreateReactive(targets) +}).observe(document.body, { childList: true, subtree: true }); diff --git a/public/statebus/braid-http.js b/public/statebus/braid-http.js new file mode 100644 index 0000000..1e1a64e --- /dev/null +++ b/public/statebus/braid-http.js @@ -0,0 +1,583 @@ +var peer = Math.random().toString(36).substr(2) + +// *************************** +// http +// *************************** + +function braidify_http (http) { + // Todo: Wrap .put to add `peer` header + http.normal_get = http.get + http.get = function braid_req (arg1, arg2, arg3) { + var url, options, cb + + // http.get() supports two forms: + // + // - http.get(url[, options][, callback]) + // - http.get(options[, callback]) + // + // We need to know which arguments are which, so let's detect which + // form we are looking at. + + // Detect form #1: http.get(url[, options][, callback]) + if (typeof arg1 === 'string' || arg1 instanceof URL) { + url = arg1 + if (typeof arg2 === 'function') + cb = arg2 + else { + options = arg2 + cb = arg3 + } + } + + // Otherwise it's form #2: http.get(options[, callback]) + else { + options = arg2 + cb = arg3 + } + + options = options || {} + + // Now we know where the `options` are specified, let's set headers. + if (!options.headers) + options.headers = {} + + // Add the subscribe header if this is a subscription + if (options.subscribe) + options.headers.subscribe = 'true' + + // Always add the `peer` header + options.headers.peer = options.headers.peer || peer + + // Wrap the callback to provide our new .on('version', ...) feature + var on_version, + on_error, + orig_cb = cb + cb = (res) => { + res.orig_on = res.on + res.on = (key, f) => { + + // Define .on('version', cb) + if (key === 'version') { + + // If we have an 'version' handler, let's remember it + on_version = f + + // And set up a subscription parser + var parser = subscription_parser((version, error) => { + if (!error) + on_version && on_version(version) + else + on_error && on_error(error) + }) + + // That will run each time we get new data + res.orig_on('data', (chunk) => { + parser.read(chunk.toString()) + }) + } + + // Forward .on('error', cb) and remember the error function + else if (key === 'error') { + on_error = f + res.orig_on(key, f) + } + + // Forward all other .on(*, cb) calls + else res.orig_on(key, f) + } + orig_cb && orig_cb(res) + } + + // Now put the parameters back in their prior order and call the + // underlying .get() function + if (url) { + arg1 = url + if (options) { + arg2 = options + arg3 = cb + } else { + arg2 = cb + } + } else { + arg1 = options + arg2 = cb + } + + return http.normal_get(arg1, arg2, arg3) + } + return http +} + + + +// *************************** +// Fetch +// *************************** + +export async function braid_fetch (url, params = {}) { + // Initialize the headers object + if (!params.headers) + params.headers = new Headers() + if (!(params.headers instanceof Headers)) + params.headers = new Headers(params.headers) + + // Always set the peer + params.headers.set('peer', peer) + + // We provide some shortcuts for Braid params + if (params.version) + params.headers.set('version', JSON.stringify(params.version)) + if (params.parents) + params.headers.set('parents', params.parents.map(JSON.stringify).join(', ')) + if (params.subscribe) + params.headers.set('subscribe', 'true') + + // Prevent browsers from going to disk cache + params.cache = 'no-cache' + + // Prepare patches + if (params.patches) { + console.assert(Array.isArray(params.patches), 'Patches must be array') + console.assert(!params.body, 'Cannot send both patches and body') + + params.patches = params.patches || [] + params.headers.set('patches', params.patches.length) + params.body = (params.patches).map(patch => { + var length = `content-length: ${patch.content.length}` + var range = `content-range: ${patch.unit} ${patch.range}` + return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n` + }).join('\r\n') + } + + // Wrap the AbortController with a new one that we control. + // + // This is because we want to be able to abort the fetch that the user + // passes in. However, the fetch() command uses a silly "AbortController" + // abstraction to abort fetches, which has both a `signal` and a + // `controller`, and only passes the signal to fetch(), but we need the + // `controller` to abort the fetch itself. + + var original_signal = params.signal + var underlying_aborter = new AbortController() + params.signal = underlying_aborter.signal + if (original_signal) + original_signal.addEventListener( + 'abort', + () => underlying_aborter.abort() + ) + + // Now we run the original fetch.... + var res = await fetch(url, params) + + // And customize the response with a couple methods for getting + // the braid subscription data: + res.subscribe = start_subscription + res.subscription = {[Symbol.asyncIterator]: iterator} + + + // Now we define the subscription function we just used: + function start_subscription (cb, error) { + if (!res.ok) + throw new Error('Request returned not ok', res) + + if (res.bodyUsed) + // TODO: check if this needs a return + throw new Error('This response\'s body has already been read', res) + + // Parse the streamed response + handle_fetch_stream( + res.body, + + // Each time something happens, we'll either get a new + // version back, or an error. + (result, err) => { + if (!err) + // Yay! We got a new version! Tell the callback! + cb(result) + else { + // This error handling code runs if the connection + // closes, or if there is unparseable stuff in the + // streamed response. + + // In any case, we want to be sure to abort the + // underlying fetch. + underlying_aborter.abort() + + // Then send the error upstream. + if (error) + error(err) + else + throw 'Unhandled network error in subscription' + } + } + ) + } + + + // And the iterator for use with "for async (...)" + function iterator () { + // We'll keep this state while our iterator runs + var initialized = false, + inbox = [], + resolve = null, + reject = null + + return { + async next() { + // If we've already received a version, return it + if (inbox.length > 0) + return {done: false, value: inbox.shift()} + + // Otherwise, let's set up a promise to resolve when we get the next item + var promise = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + + // Start the subscription, if we haven't already + if (!initialized) { + initialized = true + + // The subscription will call whichever resolve and + // reject functions the current promise is waiting for + start_subscription(x => resolve(x), + x => reject(x) ) + } + + // Now wait for the subscription to resolve or reject the promise. + var result = await promise + + // Anything we get from here out we should add to the inbox + resolve = (new_version) => inbox.push(new_version) + reject = (err) => {throw err} + + return { done: false, value: result } + } + } + } + + return res +} + +// Parse a stream of versions from the incoming bytes +async function handle_fetch_stream (stream, cb) { + if (is_nodejs) + stream = to_whatwg_stream(stream) + + // Set up a reader + var reader = stream.getReader(), + decoder = new TextDecoder('utf-8'), + parser = subscription_parser(cb) + + while (true) { + var versions = [] + + try { + // Read the next chunk of stream! + var {done, value} = await reader.read() + + // Check if this connection has been closed! + if (done) { + console.debug("Connection closed.") + cb(null, 'Connection closed') + return + } + + // Tell the parser to process some more stream + parser.read(decoder.decode(value)) + } + + catch (e) { + cb(null, e) + return + } + } +} + + + +// **************************** +// Braid-HTTP Subscription Parser +// **************************** + + +var subscription_parser = (cb) => ({ + // A parser keeps some parse state + state: {input: ''}, + + // And reports back new versions as soon as they are ready + cb: cb, + + // You give it new input information as soon as you get it, and it will + // report back with new versions as soon as it finds them. + read (input) { + + // Store the new input! + this.state.input += input + + // Now loop through the input and parse until we hit a dead end + do { + this.state = parse_version (this.state) + + // Maybe we parsed a version! That's cool! + if (this.state.result === 'success') { + this.cb({ + version: this.state.version, + parents: this.state.parents, + body: this.state.body, + patches: this.state.patches + }) + + // Reset the parser for the next version! + this.state = {input: this.state.input} + } + + // Or maybe there's an error to report upstream + else if (this.state.result === 'error') { + this.cb(null, this.state.message) + return + } + + // We stop once we've run out of parseable input. + } while (this.state.result !== 'waiting' && this.state.input.trim() !== '') + } +}) + + +// **************************** +// General parsing functions +// **************************** +// +// Each of these functions takes parsing state as input, mutates the state, +// and returns the new state. +// +// Depending on the parse result, each parse function returns: +// +// parse_ (state) +// => {result: 'waiting', ...} If it parsed part of an item, but neeeds more input +// => {result: 'success', ...} If it parses an entire item +// => {result: 'error', ...} If there is a syntax error in the input + + +function parse_version (state) { + // If we don't have headers yet, let's try to parse some + if (!state.headers) { + var parsed = parse_headers(state.input) + + // If header-parsing fails, send the error upstream + if (parsed.result === 'error') + return parsed + if (parsed.result === 'waiting') { + state.result = 'waiting' + return state + } + + state.headers = parsed.headers + state.version = state.headers.version + state.parents = state.headers.parents + + // Take the parsed headers out of the buffer + state.input = parsed.input + } + + // We have headers now! Try parsing more body. + return parse_body(state) +} + +function swallow_blank_lines (input) { + var blank_lines = /(\r\n|\n)*/.exec(input)[0] + return input.substr(blank_lines.length) +} + +// Parsing helpers +function parse_headers (input) { + input = swallow_blank_lines(input) + + // First, find the start & end block of the headers. The headers start + // when there are no longer newlines, and end at the first double-newline. + + // Look for the double-newline at the end of the headers + var headers_end = input.match(/(\r?\n)\r?\n/) + + // ...if we found none, then we need to wait for more input to complete + // the headers.. + if (!headers_end) + return {result: 'waiting'} + + // We now know where the headers are to parse! + var headers_length = headers_end.index + headers_end[1].length, + headers_source = input.substring(0, headers_length) + + // Let's parse them! First define some variables: + var headers = {}, + header_regex = /([\w-_]+):\s?(.*)\r?\n/gy, // Parses one line a time + match, + found_last_match = false + + // And now loop through the block, matching one line at a time + while (match = header_regex.exec(headers_source)) { + // console.log('Header match:', match && [match[1], match[2]]) + headers[match[1].toLowerCase()] = match[2] + + // This might be the last line of the headers block! + if (header_regex.lastIndex === headers_length) + found_last_match = true + } + + // If the regex failed before we got to the end of the block, throw error: + if (!found_last_match) + return { + result: 'error', + message: 'Parse error in headers: "' + + JSON.stringify(headers_source.substr(header_regex.lastIndex)) + '"', + headers_so_far: headers, + last_index: header_regex.lastIndex, headers_length + } + + // Success! Let's parse special headers + if ('version' in headers) + headers.version = JSON.parse(headers.version) + if ('parents' in headers) + headers.parents = JSON.parse('['+headers.parents+']') + if ('patches' in headers) + headers.patches = JSON.parse(headers.patches) + + // Update the input + input = input.substring(headers_length) + + // Swallow the final blank line ending the headers + if (input.substr(0, 2) === '\r\n') + // Swallow \r\n + input = input.substr(2) + else + // Swallow \n + input = input.substr(1) + + // And return the parsed result + return { result: 'success', headers, input } +} + +function parse_body (state) { + // Parse Body Snapshot + + var content_length = parseInt(state.headers['content-length']) + if (content_length !== NaN) { + if (content_length > state.input.length) { + state.result = 'waiting' + return state + } + + var consumed_length = content_length + 2 + state.result = 'success' + state.body = state.input.substring(0, content_length) + state.input = state.input.substring(consumed_length) + return state + } + + // Parse Patches + + else if (state.headers.patches) { + state.patches = state.patches || [] + + var last_patch = state.patches[state.patches.length-1] + + // Parse patches until the final patch has its content filled + while (!(state.patches.length === state.headers.patches + && 'content' in last_patch)) { + + state.input = state.input.trimStart() + + // Are we starting a new patch? + if (!last_patch || 'content' in last_patch) { + last_patch = {} + state.patches.push(last_patch) + } + + // Parse patch headers + if (!('headers' in last_patch)) { + var parsed = parse_headers(state.input) + + // If header-parsing fails, send the error upstream + if (parsed.result === 'error') + return parsed + if (parsed.result === 'waiting') { + state.result = 'waiting' + return state + } + + // We parsed patch headers! Update state. + last_patch.headers = parsed.headers + state.input = parsed.input + } + + // Todo: support arbitrary patches, not just range-patch + + // Parse Range Patch format + { + if (!('content-length' in last_patch.headers)) + return { + result: 'error', + message: 'no content-length in patch', + patch: last_patch, input: state.input + } + + if (!('content-range' in last_patch.headers)) + return { + result: 'error', + message: 'no content-range in patch', + patch: last_patch, input: state.input + } + + var content_length = parseInt(last_patch.headers['content-length']) + + // Does input have the entire patch contents yet? + if (state.input.length < content_length) { + state.result = 'waiting' + return state + } + + // Content-range is of the form ' ' e.g. 'json .index' + + var match = last_patch.headers['content-range'].match(/(\S+) (.*)/) + if (!match) + return { + result: 'error', + message: 'cannot parse content-range in patch', + patch: last_patch, input: state.input + } + + last_patch.unit = match[1] + last_patch.range = match[2] + last_patch.content = state.input.substr(0, content_length) + + // Consume the parsed input + state.input = state.input.substring(content_length) + } + } + + state.result = 'success' + return state + } + + return { + result: 'error', + message: 'cannot parse body without content-length or patches header' + } +} + + +// **************************** +// Exports +// **************************** + +if (typeof module !== 'undefined' && module.exports) + module.exports = { + fetch: braid_fetch, + http: braidify_http, + subscription_parser, + parse_version, + parse_headers, + parse_body + } + + diff --git a/public/statebus/braidify-client.js b/public/statebus/braidify-client.js new file mode 100644 index 0000000..9730b84 --- /dev/null +++ b/public/statebus/braidify-client.js @@ -0,0 +1,634 @@ +var peer = Math.random().toString(36).substr(2) + +// *************************** +// http +// *************************** + +function braidify_http (http) { + // Todo: Wrap .put to add `peer` header + http.normal_get = http.get + http.get = function braid_req (arg1, arg2, arg3) { + var url, options, cb + + // http.get() supports two forms: + // + // - http.get(url[, options][, callback]) + // - http.get(options[, callback]) + // + // We need to know which arguments are which, so let's detect which + // form we are looking at. + + // Detect form #1: http.get(url[, options][, callback]) + if (typeof arg1 === 'string' || arg1 instanceof URL) { + url = arg1 + if (typeof arg2 === 'function') + cb = arg2 + else { + options = arg2 + cb = arg3 + } + } + + // Otherwise it's form #2: http.get(options[, callback]) + else { + options = arg2 + cb = arg3 + } + + options = options || {} + + // Now we know where the `options` are specified, let's set headers. + if (!options.headers) + options.headers = {} + + // Add the subscribe header if this is a subscription + if (options.subscribe) + options.headers.subscribe = 'true' + + // Always add the `peer` header + options.headers.peer = options.headers.peer || peer + + // Wrap the callback to provide our new .on('version', ...) feature + var on_version, + on_error, + orig_cb = cb + cb = (res) => { + res.orig_on = res.on + res.on = (key, f) => { + + // Define .on('version', cb) + if (key === 'version') { + + // If we have an 'version' handler, let's remember it + on_version = f + + // And set up a subscription parser + var parser = subscription_parser((version, error) => { + if (!error) + on_version && on_version(version) + else + on_error && on_error(error) + }) + + // That will run each time we get new data + res.orig_on('data', (chunk) => { + parser.read(chunk.toString()) + }) + } + + // Forward .on('error', cb) and remember the error function + else if (key === 'error') { + on_error = f + res.orig_on(key, f) + } + + // Forward all other .on(*, cb) calls + else res.orig_on(key, f) + } + orig_cb && orig_cb(res) + } + + // Now put the parameters back in their prior order and call the + // underlying .get() function + if (url) { + arg1 = url + if (options) { + arg2 = options + arg3 = cb + } else { + arg2 = cb + } + } else { + arg1 = options + arg2 = cb + } + + return http.normal_get(arg1, arg2, arg3) + } + return http +} + + + +// *************************** +// Fetch +// *************************** + +var normal_fetch, + AbortController, + Headers, + is_nodejs = typeof window === 'undefined' + +if (is_nodejs) { + // Nodejs + + // Note that reconnect logic doesn't work in node-fetch, because it + // doesn't call the .catch() handler when the stream fails. + // + // See https://github.com/node-fetch/node-fetch/issues/753 + + normal_fetch = require('node-fetch') + AbortController = require('abort-controller') + Headers = normal_fetch.Headers + var to_whatwg_stream = require('node-web-streams').toWebReadableStream +} else { + // Web Browser + normal_fetch = window.fetch + AbortController = window.AbortController + Headers = window.Headers + window.fetch = braid_fetch + window.braid_fetch = braid_fetch +} + + +function braid_fetch (url, params = {}) { + // Initialize the headers object + if (!params.headers) + params.headers = new Headers() + if (!(params.headers instanceof Headers)) + params.headers = new Headers(params.headers) + + // Always set the peer + //params.headers.set('peer', peer) + + // We provide some shortcuts for Braid params + if (params.version) + params.headers.set('version', JSON.stringify(params.version)) + if (params.parents) + params.headers.set('parents', params.parents.map(JSON.stringify).join(', ')) + if (params.subscribe) + params.headers.set('subscribe', 'true') + + // Prevent browsers from going to disk cache + params.cache = 'no-cache' + + // Prepare patches + if (params.patches) { + console.assert(Array.isArray(params.patches), 'Patches must be array') + console.assert(!params.body, 'Cannot send both patches and body') + + params.patches = params.patches || [] + params.headers.set('patches', params.patches.length) + params.body = (params.patches).map(patch => { + var length = `content-length: ${patch.content.length}` + var range = `content-range: ${patch.unit} ${patch.range}` + return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n` + }).join('\r\n') + } + + // Wrap the AbortController with a new one that we control. + // + // This is because we want to be able to abort the fetch that the user + // passes in. However, the fetch() command uses a silly "AbortController" + // abstraction to abort fetches, which has both a `signal` and a + // `controller`, and only passes the signal to fetch(), but we need the + // `controller` to abort the fetch itself. + + var original_signal = params.signal + var underlying_aborter = new AbortController() + params.signal = underlying_aborter.signal + if (original_signal) + original_signal.addEventListener( + 'abort', + () => underlying_aborter.abort() + ) + + // Wrap the original fetch's promise with a custom promise. + // + // This promise includes an additional .andThen(cb) method. It calls the + // cb multiple times, and then if there's any crash, it'll call the + // original promise's .catch(cb) clause. + // + // We couldn't just augment the original promise with the .andThen() + // method, because there is no way of calling the .catch() method of the + // original promise ourselves. You cannot get access to a promise's + // internal callback method that has been set by some other code. + + var andThen, iterator + var promise = new Promise((resolve, reject) => { + + // Run the actual fetch here! + var fetched = normal_fetch(url, params) + + function start_subscription (cb, error) { + fetched.then(function (res) { + if (!res.ok) + error(new Error('Subscription request failed', res)) + + // Parse the streamed response + handle_fetch_stream( + res.body, + + // Each time something happens, we'll either get a new + // version back, or an error. + (result, err) => { + if (!err) + // Yay! We got a new version! Tell the callback! + cb(result) + else { + // This error handling code runs if the connection + // closes, or if there is unparseable stuff in the + // streamed response. + + // In any case, we want to be sure to abort the + // underlying fetch. + underlying_aborter.abort() + + // Then send the error upstream. + error(err) + } + } + ) + }) + // This catch will run if the initial fetch request fails to + // connect. The error handling code above, on the other hand, + // runs if the response while the connection is held open. + .catch(error) + } + + // If this is a subscribe, then: + // - include the .andThen() method + // - add an iterator to support "for await (var x of fetch(..))" + if (params.subscribe) { + + // The andThen function just calls cb with each new version + andThen = cb => { + start_subscription(cb, reject) + return promise + } + + // The iterator + iterator = () => ({ + initialized: false, + + // Each time next() is called, it creates a promise and stores + // the resolve and reject functions here. + resolve: null, + reject: null, + + async next() { + // Start the subscription + if (!this.initialized) { + this.initialized = true + + // The subscription will call whichever resolve and + // reject functions the current promise is waiting for + start_subscription(x => this.resolve(x), + x => this.reject(x) ) + } + + // Now create a new promise, and wait for the subscription + // (above) to resolve or reject it. + var result = await new Promise((resolve, reject) => { + this.resolve = resolve + this.reject = reject + }) + + // Sanity check: I want to make sure that the subscription + // doesn't try to give us a new version before we've gone + // through another loop of the iterator and created the + // new promise. + var tellme = 'Error! Please tell toomim@gmail.com that this happened.' + this.resolve = () => {throw tellme} + this.reject = () => {throw tellme} + + return { done: false, value: result } + } + }) + } + + // But if this wasn't a `subscribe` request, then we just wrap the + // underlying promise with our superpromise directly: + else fetched.then(resolve).catch(reject) + + // ... and we're done. + }) + + promise.andThen = andThen + promise[Symbol.asyncIterator] = iterator + + return promise +} + +// Parse a stream of versions from the incoming bytes +async function handle_fetch_stream (stream, cb) { + if (is_nodejs) + stream = to_whatwg_stream(stream) + + // Set up a reader + var reader = stream.getReader(), + decoder = new TextDecoder('utf-8'), + parser = subscription_parser(cb) + + while (true) { + var versions = [] + + try { + // Read the next chunk of stream! + var {done, value} = await reader.read() + + // Check if this connection has been closed! + if (done) { + console.debug("Connection closed.") + cb(null, 'Connection closed') + return + } + + // Tell the parser to process some more stream + parser.read(decoder.decode(value)) + } + + catch (e) { + cb(null, e) + return + } + } +} + + + +// **************************** +// Braid-HTTP Subscription Parser +// **************************** + + +var subscription_parser = (cb) => ({ + // A parser keeps some parse state + state: {input: ''}, + + // And reports back new versions as soon as they are ready + cb: cb, + + // You give it new input information as soon as you get it, and it will + // report back with new versions as soon as it finds them. + read (input) { + + // Store the new input! + this.state.input += input + + // Now loop through the input and parse until we hit a dead end + do { + this.state = parse_version (this.state) + + // Maybe we parsed a version! That's cool! + if (this.state.result === 'success') { + this.cb({ + version: this.state.version, + parents: this.state.parents, + body: this.state.body, + patches: this.state.patches + }) + + // Reset the parser for the next version! + this.state = {input: this.state.input} + } + + // Or maybe there's an error to report upstream + else if (this.state.result === 'error') { + this.cb(null, this.state.message) + return + } + + // We stop once we've run out of parseable input. + } while (this.state.result !== 'waiting' && this.state.input.trim() !== '') + } +}) + + +// **************************** +// General parsing functions +// **************************** +// +// Each of these functions takes parsing state as input, mutates the state, +// and returns the new state. +// +// Depending on the parse result, each parse function returns: +// +// parse_ (state) +// => {result: 'waiting', ...} If it parsed part of an item, but neeeds more input +// => {result: 'success', ...} If it parses an entire item +// => {result: 'error', ...} If there is a syntax error in the input + + +function parse_version (state) { + // If we don't have headers yet, let's try to parse some + if (!state.headers) { + var parsed = parse_headers(state.input) + + // If header-parsing fails, send the error upstream + if (parsed.result === 'error') + return parsed + if (parsed.result === 'waiting') { + state.result = 'waiting' + return state + } + + state.headers = parsed.headers + state.version = state.headers.version + state.parents = state.headers.parents + + // Take the parsed headers out of the buffer + state.input = parsed.input + } + + // We have headers now! Try parsing more body. + return parse_body(state) +} + +function swallow_blank_lines (input) { + var blank_lines = /(\r\n|\n)*/.exec(input)[0] + return input.substr(blank_lines.length) +} + +// Parsing helpers +function parse_headers (input) { + input = swallow_blank_lines(input) + + // First, find the start & end block of the headers. The headers start + // when there are no longer newlines, and end at the first double-newline. + + // Look for the double-newline at the end of the headers + var headers_end = input.match(/(\r?\n)\r?\n/) + + // ...if we found none, then we need to wait for more input to complete + // the headers.. + if (!headers_end) + return {result: 'waiting'} + + // We now know where the headers are to parse! + var headers_length = headers_end.index + headers_end[1].length, + headers_source = input.substring(0, headers_length) + + // Let's parse them! First define some variables: + var headers = {}, + header_regex = /([\w-_]+):\s?(.*)\r?\n/gy, // Parses one line a time + match, + found_last_match = false + + // And now loop through the block, matching one line at a time + while (match = header_regex.exec(headers_source)) { + // console.log('Header match:', match && [match[1], match[2]]) + headers[match[1].toLowerCase()] = match[2] + + // This might be the last line of the headers block! + if (header_regex.lastIndex === headers_length) + found_last_match = true + } + + // If the regex failed before we got to the end of the block, throw error: + if (!found_last_match) + return { + result: 'error', + message: 'Parse error in headers: "' + + JSON.stringify(headers_source.substr(header_regex.lastIndex)) + '"', + headers_so_far: headers, + last_index: header_regex.lastIndex, headers_length + } + + // Success! Let's parse special headers + if ('version' in headers) + headers.version = JSON.parse(headers.version) + if ('parents' in headers) + headers.parents = JSON.parse('['+headers.parents+']') + if ('patches' in headers) + headers.patches = JSON.parse(headers.patches) + + // Update the input + input = input.substring(headers_length) + + // Swallow the final blank line ending the headers + if (input.substr(0, 2) === '\r\n') + // Swallow \r\n + input = input.substr(2) + else + // Swallow \n + input = input.substr(1) + + // And return the parsed result + return { result: 'success', headers, input } +} + +function parse_body (state) { + // Parse Body Snapshot + + var content_length = parseInt(state.headers['content-length']) + if (content_length !== NaN) { + if (content_length > state.input.length) { + state.result = 'waiting' + return state + } + + var consumed_length = content_length + 2 + state.result = 'success' + state.body = state.input.substring(0, content_length) + state.input = state.input.substring(consumed_length) + return state + } + + // Parse Patches + + else if (state.headers.patches) { + state.patches = state.patches || [] + + var last_patch = state.patches[state.patches.length-1] + + // Parse patches until the final patch has its content filled + while (!(state.patches.length === state.headers.patches + && 'content' in last_patch)) { + + state.input = state.input.trimStart() + + // Are we starting a new patch? + if (!last_patch || 'content' in last_patch) { + last_patch = {} + state.patches.push(last_patch) + } + + // Parse patch headers + if (!('headers' in last_patch)) { + var parsed = parse_headers(state.input) + + // If header-parsing fails, send the error upstream + if (parsed.result === 'error') + return parsed + if (parsed.result === 'waiting') { + state.result = 'waiting' + return state + } + + // We parsed patch headers! Update state. + last_patch.headers = parsed.headers + state.input = parsed.input + } + + // Todo: support arbitrary patches, not just range-patch + + // Parse Range Patch format + { + if (!('content-length' in last_patch.headers)) + return { + result: 'error', + message: 'no content-length in patch', + patch: last_patch, input: state.input + } + + if (!('content-range' in last_patch.headers)) + return { + result: 'error', + message: 'no content-range in patch', + patch: last_patch, input: state.input + } + + var content_length = parseInt(last_patch.headers['content-length']) + + // Does input have the entire patch contents yet? + if (state.input.length < content_length) { + state.result = 'waiting' + return state + } + + // Content-range is of the form ' ' e.g. 'json .index' + + var match = last_patch.headers['content-range'].match(/(\S+) (.*)/) + if (!match) + return { + result: 'error', + message: 'cannot parse content-range in patch', + patch: last_patch, input: state.input + } + + last_patch.unit = match[1] + last_patch.range = match[2] + last_patch.content = state.input.substr(0, content_length) + + // Consume the parsed input + state.input = state.input.substring(content_length) + } + } + + state.result = 'success' + return state + } + + return { + result: 'error', + message: 'cannot parse body without content-length or patches header' + } +} + + +// **************************** +// Exports +// **************************** + +if (typeof module !== 'undefined' && module.exports) + module.exports = { + fetch: braid_fetch, + http: braidify_http, + subscription_parser, + parse_version, + parse_headers, + parse_body + } + + diff --git a/public/statebus/client-library.js b/public/statebus/client-library.js new file mode 100644 index 0000000..536f1c0 --- /dev/null +++ b/public/statebus/client-library.js @@ -0,0 +1,1045 @@ +(function () { + var websocket_prefix = (clientjs_option('websocket_path') + || '_connect_to_statebus_') + + // make_client_statebus_maker() + window.bus = window.statebus() + bus.label = 'bus' + + bus.libs = {} + bus.libs.react12 = {} + bus.libs.react17 = {} + + // **************** + // Connecting over the Network + function set_cookie (key, val) { + document.cookie = key + '=' + val + '; Expires=21 Oct 2025 00:0:00 GMT;' + } + function get_cookie (key) { + var c = document.cookie.match('(^|;)\\s*' + key + '\\s*=\\s*([^;]+)'); + return c ? c.pop() : ''; + } + try { document.cookie } catch (e) {get_cookie = set_cookie = function (){}} + function make_websocket (url) { + if (!url.match(/^\w{0,7}:\/\//)) + url = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port : '') + url + + url = url.replace(/^state:\/\//, 'wss://') + url = url.replace(/^istate:\/\//, 'ws://') + url = url.replace(/^statei:\/\//, 'ws://') + + url = url.replace(/^https:\/\//, 'wss://') + url = url.replace(/^http:\/\//, 'ws://') + + // { // Convert to absolute + // var link = document.createElement("a") + // link.href = url + // url = link.href + // } + + return new WebSocket(url + '/' + websocket_prefix + '/websocket') + // return new SockJS(url + '/' + websocket_prefix) + } + function client_creds (server_url) { + // This function is only used for websocket connections. + // http connections set the cookie on the server. + var me = bus.get('ls/me') + bus.log('connect: me is', me) + if (!me.client) { + // Create a client id if we have none yet. + // Either from a cookie set by server, or a new one from scratch. + var c = get_cookie('peer') + me.client = c || (Math.random().toString(36).substring(2) + + Math.random().toString(36).substring(2) + + Math.random().toString(36).substring(2)) + bus.set(me) + } + + set_cookie('peer', me.client) + return {clientid: me.client} + } + + bus.libs.http_out = (prefix, url) => { + var preprefix = prefix.slice(0,-1) + var has_prefix = new RegExp('^' + preprefix) + var is_absolute = /^https?:\/\// + var subscriptions = {} + var put_counter = 0 + + function add_prefix (url) { + return is_absolute.test(url) ? url : preprefix + url } + function rem_prefix (url) { + return has_prefix.test(url) ? url.substr(preprefix.length) : url } + function add_prefixes (obj) { + var keyed = bus.translate_keys(bus.clone(obj), add_prefix) + return bus.translate_links(bus.clone(keyed), add_prefix) + } + function rem_prefixes (obj) { + var keyed = bus.translate_keys(bus.clone(obj), rem_prefix) + return bus.translate_links(bus.clone(keyed), rem_prefix) + } + + var puts = new Map() + function enqueue_put (url, body) { + var id = put_counter++ + puts.set(id, {url: url, body: body, id: id}) + send_put(id) + } + function send_put (id) { + try { + puts.get(id).status = 'sending' + braid_fetch( + puts.get(id).url, + { + method: 'put', + headers: { + 'content-type': 'application/json', + 'put-order': id, + }, + body: puts.get(id).body + } + ).then(function (res) { + if (res.status !== 200) + console.error('Server gave error on PUT:', + e, 'for', puts.get(id).body) + puts.delete(id) + }).catch(function (e) { + console.error('Error on PUT, waiting...', puts.get(id).url) + puts.get(id).status = 'waiting' + }) + } catch (e) { + console.error('Error on PUT, waiting...', puts.get(id).url) + puts.get(id).status = 'waiting' + } + } + function retry_put (id) { + setTimeout(function () {send_put(id)}, 1000) + } + function send_all_puts () { + puts.forEach(function (value, id) { + if (value.status === 'waiting') { + console.log('Sending waiting put', id) + send_put(id) + } + }) + } + + bus(prefix).setter = function (obj, t) { + bus.set.fire(obj) + + var put = { + url: url + rem_prefix(obj.key), + body: JSON.stringify(obj.val) + } + if (t.version) put.version = t.version + if (t.parents) put.parents = t.parents + if (t.patch) put.patch = t.patch + var put_id = put_counter++ + puts.set(put_id, put) + send_put(put_id) + } + + bus(prefix).getter = function (key, t) { + // Subscription can be in states: + // - connecting + // - connected + // - reconnect + // - reconnecting + // - aborted + + // If we have an outstanding get running, then let's tell it to + // re-activate! + if (subscriptions[key]) { + // We should only be here if an existing subscription was + // aborted, but hasn't cleared yet. + console.assert(subscriptions[key].status === 'aborted', + 'Regetting a subscription of status ' + + subscriptions[key].status) + + console.trace('foo') + + // Let's tell it to reconnect when it tries to clear! + subscriptions[key].status = 'reconnect' + } + + // Otherwise, create a new subscription + else + subscribe (key, t) + + function subscribe (key, t) { + var aborter = new AbortController(), + reconnect_attempts = 0 + + // Start the subscription! + braid_fetch( + // URL + url + rem_prefix(key), + + // Options + { + method: 'get', + subscribe: true, + headers: {accept: 'application/json'}, + signal: aborter.signal + } + ).andThen( function (new_version) { + // New update received! + if (subscriptions[key].status === 'connecting') { + console.log('%c[*] opened ' + key, + 'color: blue') + reconnect_attempts = 0 + subscriptions[key].status = 'connected' + send_all_puts() + } + + // Return the update + t.return({ + key: key, + val: add_prefixes(JSON.parse(new_version.body)) + }) + }).catch( function (e) { + if (subscriptions[key].status === 'aborted') { + // Then this get is over and done with! + delete subscriptions[key] + return + } + + // Reconnect! + setTimeout(function () { subscribe(key, t) }, + reconnect_attempts > 0 ? 5000 : 1500) + subscriptions[key].status = 'reconnecting' + }) + + // Remember this subscription + subscriptions[key] = { + aborter: aborter, + status: 'connecting' + } + } + } + bus(prefix).forgetter = function (key) { + subscriptions[key].status = 'aborted' + subscriptions[key].aborter.abort() + } + } + + function http_automount () { + function get_domain (key) { // Returns e.g. "state://foo.com" + var m = key.match(/^https?\:\/\/(([^:\/?#]*)(?:\:([0-9]+))?)/) + return m && m[0] + } + + var old_route = bus.route + var connections = {} + bus.route = function (key, method, arg, t) { + var d = get_domain(key) + if (d && !connections[d]) { + bus.libs.http_out(d + '/*', d + '/') + connections[d] = true + } + + return old_route(key, method, arg, t) + } + } + + + // **************** + // Manipulate Localstorage + bus.libs.localstorage = (prefix) => { + try { localStorage } catch (e) { return } + + // Sets are queued up, to store values with a delay, in batch + var sets_are_pending = false + var pending_sets = {} + + function set_the_pending_sets() { + bus.log('localstore: saving', pending_sets) + for (var k in pending_sets) + localStorage.setItem(k, JSON.stringify(pending_sets[k])) + sets_are_pending = false + } + + bus(prefix).getter = function (key) { + var result = localStorage.getItem(key) + return result ? JSON.parse(result) : {key: key} + } + bus(prefix).setter = function (obj) { + // Do I need to make this recurse into the object? + bus.log('localStore: on_set:', obj.key) + pending_sets[obj.key] = obj + if (!sets_are_pending) { + setTimeout(set_the_pending_sets, 50) + sets_are_pending = true + } + bus.set.fire(obj) + return obj + } + bus(prefix).deleter = function (key) { localStorage.removeItem(key) } + + + // Hm... this update stuff doesn't seem to work on file:/// urls in chrome + function update (event) { + bus.log('Got a localstorage update', event) + bus.dirty(event.key) + //this.get(event.key.substr('statebus '.length)) + } + if (window.addEventListener) window.addEventListener("storage", update, false) + else window.attachEvent("onstorage", update) + } + + // Stores state in the query string, as ?key1={obj...}&key2={obj...} + function url_store (prefix) { + var bus = this + function get_query_string_value (key) { + return unescape(window.location.search.replace( + new RegExp("^(?:.*[&\\?]" + + escape(key).replace(/[\.\+\*]/g, "\\$&") + + "(?:\\=([^&]*))?)?.*$", "i"), + "$1")) + } + + // Initialize data from the URL on load + + // Now the regular shit + var data = get_query_string_value(key) + data = (data && JSON.parse(data)) || {key : key} + // Then I would need to: + // - Change the key prefix + // - Set this into the cache + + bus(prefix).setter = function (obj) { + window.history.replaceState( + '', + '', + document.location.origin + + document.location.pathname + + escape('?'+key+'='+JSON.stringify(obj))) + bus.set.fire(obj) + } + } + + + // **************** + // Wrapper for React Components + + function react_version () { + if (!window.React) return undefined; + return Number(window.React.version.split('.')[0]) + } + + // Newer React requires createReactClass as a separate libary + if (window.React && !React.createClass && window.createReactClass) + React.createClass = createReactClass + + // XXX Currently assumes there's a statebus named "bus" in global + // XXX scope. + + var components = {} // Indexed by 'component/0', 'component/1', etc. + var components_count = 0 + var dirty_components = {} + function create_react_class(component) { + function wrap(name, new_func) { + var old_func = component[name] + component[name] = function wrapper () { return new_func.bind(this)(old_func) } + } + + // Register the component's basic info + wrap((react_version() >= 16 ? 'UNSAFE_' : '') + 'componentWillMount', + function new_cwm (orig_func) { + // if (component.displayName === undefined) + // throw 'Component needs a displayName' + //this.name = component.displayName.toLowerCase().replace(' ', '_') + this.key = 'component/' + components_count++ + components[this.key] = this + + function add_shortcut (obj, shortcut_name, to_key) { + delete obj[shortcut_name] + Object.defineProperty(obj, shortcut_name, { + get: function () { return bus.get(to_key) }, + configurable: true }) + } + add_shortcut(this, 'local', this.key) + + orig_func && orig_func.apply(this, arguments) + + // Make render reactive + var orig_render = this.render + this.render = bus.reactive(function () { + console.assert(this !== window) + if (this.render.called_directly) { + delete dirty_components[this.key] + + // Add reactivity to any keys passed inside objects in props. + for (var k in this.props) + if (this.props.hasOwnProperty(k) + && this.props[k] !== null + && typeof this.props[k] === 'object' + && this.props[k].key) + + bus.get(this.props[k].key) + + // Call the renderer! + return orig_render.apply(this, arguments) + } else { + dirty_components[this.key] = true + schedule_re_render() + } + }) + }) + + wrap('componentWillUnmount', function new_cwu (orig_func) { + orig_func && orig_func.apply(this, arguments) + // Clean up + bus.delete(this.key) + delete components[this.key] + delete dirty_components[this.key] + }) + + function shallow_clone(original) { + var clone = Object.create(Object.getPrototypeOf(original)) + var i, keys = Object.getOwnPropertyNames(original) + for (i=0; i < keys.length; i++){ + Object.defineProperty(clone, keys[i], + Object.getOwnPropertyDescriptor(original, keys[i]) + ) + } + return clone + } + + component.shouldComponentUpdate = function new_scu (next_props, next_state) { + // This component definitely needs to update if it is marked as dirty + if (dirty_components[this.key] !== undefined) return true + + // Otherwise, we'll check to see if its state or props + // have changed. But ignore React's 'children' prop, + // because it often has a circular reference. + next_props = shallow_clone(next_props) + this_props = shallow_clone(this.props) + + delete next_props['children']; delete this_props['children'] + // delete next_props['kids']; delete this_props['kids'] + + next_props = bus.clone(next_props) + this_props = bus.clone(this_props) + + + return !bus.deep_equals([next_state, next_props], [this.state, this_props]) + + // TODO: + // + // - Check children too. Right now we just silently fail + // on components with children. WTF? + // + // - A better method might be to mark a component dirty when + // it receives new props in the + // componentWillReceiveProps React method. + } + + component.loading = function loading () { + return this.render.loading() + } + + // Now create the actual React class with this definition, and + // return it. + var react_class = React.createClass(component) + var result = function (props, children) { + props = props || {} + props['data-key'] = props.key + props['data-widget'] = component.displayName + + return React.createElement(react_class, props, children) + } + Object.defineProperty(result, 'name', + {value: component.displayName, writable: false}) + return result + } + + // ***************** + // Re-rendering react components + var re_render_scheduled = false + var re_rendering = false + function schedule_re_render() { + if (!re_render_scheduled) { + requestAnimationFrame(function () { + re_render_scheduled = false + + // Re-renders dirty components + for (var comp_key in dirty_components) { + if (dirty_components[comp_key] // Since another component's update might update this + && components[comp_key]) // Since another component might unmount this + + try { + re_rendering = true + components[comp_key].forceUpdate() + } finally { + re_rendering = false + } + } + }) + re_render_scheduled = true + } + } + + // ############################################################################## + // ### + // ### Full-featured single-file app methods + // ### + + // function make_client_statebus_maker () { + // var extra_stuff = ['make_websocket client_creds', + // 'url_store components'].join(' ').split(' ') + // if (window.statebus) { + // var orig_statebus = statebus + // window.statebus = function make_client_bus () { + // var bus = orig_statebus() + // for (var i=0; i { + + function better_element(el) { + // To do: + // - Don't put all args into a children array, cause react thinks + // that means they need a key. + + return function () { + var children = [] + var attrs = {style: {}} + + for (var i=0; i' in CSS selectors + return s.replace(//g, ">") + } + window.STYLE = function (s) { + return React.DOM.style({dangerouslySetInnerHTML: {__html: escape_html(s)}}) + } + window.TITLE = function (s) { + return React.DOM.title({dangerouslySetInnerHTML: {__html: escape_html(s)}}) + } + } + + bus.libs.react17.reactive_dom = () => { + // The window.dom object lets the user define new react components as + // functions + window.dom = window.dom || new Proxy({}, { + get: function (o, k) { return o[k] }, + set: function (o, k, v) { + o[k] = v + window[k] = make_component(k, v) + return true + } + }) + + // We'll define functions for all HTML tags... + var function_for_tag = (tag) => + (...args) => { + var children = [] + var attrs = {style: {}} + + for (var i=0; i { + window[tagname.toUpperCase()] = function_for_tag(tagname) + }) + + // We create special functions for INPUT and TEXTAREA, because they + // have to do extra work to maintain the cursor when we use statebus + // instead of React's setState() for state updates. + window.INPUT = function_for_tag(bus.libs.react17.input) + window.TEXTAREA = function_for_tag(bus.libs.react17.textarea) + + + // Improve the functions ^^^ put this above + function better_element(el) { + // To do: + // - Don't put all args into a children array, cause react thinks + // that means they need a key. + + return function () { + var children = [] + var attrs = {style: {}} + + for (var i=0; i { + return React.createElement(component, {...props, forwarded_ref: ref}) + }) + } + + // We create special functions for INPUT and TEXTAREA, because they + // have to do extra work to maintain the cursor when we use statebus + // instead of React's setState() for state updates. + if (window.React && !React.createClass && window.createReactClass) { + bus.libs.react17.input = make_fixed_textbox('input') + bus.libs.react17.textarea = make_fixed_textbox('textarea') + } + function autodetect_args (func) { + if (func.args) return + + // Get an array of the func's params + var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, + params = /([^\s,]+)/g, + s = func.toString().replace(comments, '') + func.args = s.slice(s.indexOf('(')+1, s.indexOf(')')).match(params) || [] + } + + // Load the components + var users_widgets = {} + function make_component(name, func) { + // Define the component + return users_widgets[name] = create_react_class({ + displayName: name, + render: function () { + var args = [] + + // Parse the function's args, and pass props into them directly + autodetect_args(func) + // this.props.kids = this.props.kids || this.props.children + for (var i=0; i because coffeescript library isn\'t present') + return + } + // Compile coffeescript to javascript + var compiled = scripts[i].text + if (scripts[i].getAttribute('type') !== 'statebus-js') + compiled = compile_coffee(scripts[i].text, filename) + if (compiled) + load_client_code(compiled) + } + } + + function dom_to_widget (node) { + if (node.nodeName === '#text') return node.textContent + if (!(node.nodeName in users_widgets)) return node + + node.seen = true + var children = [], props = {} + // Recursively convert children + for (var i=0; i { + bus.libs.react17.reactive_dom() + load_coffee() + if (dom.BODY) + document.addEventListener( + 'DOMContentLoaded', + () => { + var root = document.createElement('root') + document.body.appendChild(root) + ReactDOM.render(BODY(), root) + }, + false + ) + } + + // if (statebus_server !== 'none') { + // if (clientjs_option('braid_mode')) { + // console.log('Using Braid-HTTP!') + // bus.libs.http_out ('/*', statebus_server) + // } else { + // bus.ws_mount ('/*', statebus_server) + // } + // } + + http_automount() + + statebus.compile_coffee = compile_coffee + statebus.load_client_code = load_client_code + + // if (clientjs_option('globals')) { + // // Setup globals + // var globals = ['get', 'set', 'state'] + + // for (var i=0; i tags. +(function(name, definition) { + window[name] = definition() +}('statebus', function() {var statelog_indent = 0; var busses = {}, bus_count = 0, executing_funk, global_funk, funks = {}, clean_timer, symbols, nodejs = typeof window === 'undefined'; function make_bus (options) { + + + // **************** + // Get, Set, Forget, Delete + + function get (key, callback) { + if (typeof key !== 'string' + && !(typeof key === 'object' && typeof key.key === 'string')) + throw ('Error: get(key) called with key = ' + + JSON.stringify(key)) + key = key.key || key // You can pass in an object instead of key + // We should probably disable this in future + bogus_check(key) + + var called_from_reactive_funk = !callback + var funk = callback || executing_funk + + // Initialize callback + if (callback) { + (callback.defined = callback.defined || [] + ).push({as:'get callback', key:key}); + callback.has_seen = callback.has_seen || function (bus, key, version) { + callback.seen_keys = callback.seen_keys || {} + var bus_key = JSON.stringify([bus.id, key]) + var seen_versions = + callback.seen_keys[bus_key] = callback.seen_keys[bus_key] || [] + seen_versions.push(version) + if (seen_versions.length > 50) seen_versions.shift() + } + } + + // ** Subscribe the calling funk ** + + gets_in.add(key, funk_key(funk)) + if (to_be_forgotten[key]) { + clearTimeout(to_be_forgotten[key]) + delete to_be_forgotten[key] + } + + bind(key, 'on_set', funk) + + // ** Call getters upstream ** + + // TODO: checking gets_out[] doesn't count keys that we got which + // arrived nested within a bigger object, because we never explicity + // got those keys. But we don't need to get them now cause we + // already have them. + var getterters = 0 + if (!gets_out[key]) + getterters = bus.route(key, 'getter', key) + + // Now there might be a new value pubbed onto this bus. + // Or there might be a pending get. + // ... or there weren't any getters upstream. + + + // ** Return a value ** + + // If called reactively, we always return a value. + if (called_from_reactive_funk) { + funk.has_seen(bus, key, versions[key]) + backup_cache[key] = backup_cache[key] || {key: key} + return cache[key] = cache[key] || {key: key} + } + + // Otherwise, we want to make sure that a pub gets called on the + // handler. If there's a pending get, then it'll get called later. + // If there was a getter, then it already got called. Otherwise, + // let's call it now. + else if (!pending_gets[key] && getterters === 0) { + // TODO: my intuition suggests that we might prefer to delay this + // .on_set getting called in a setTimeout(f,0), to be consistent + // with other calls to .on_set. + backup_cache[key] = backup_cache[key] || {key: key} + run_handler(funk, 'on_set', cache[key] = cache[key] || {key: key}) + } + } + function get_once (key, cb) { + function cb2 (o) { cb(o); forget(key, cb2) } + // get(key) // This prevents key from being forgotten + get(key, cb2) + } + get.once = get_once + var pending_gets = {} + var gets_out = {} // Maps `key' to `func' iff we've got `key' + var gets_in = new One_To_Many() // Maps `key' to `pub_funcs' subscribed to our key + + var currently_saving + function set (obj, t) { + // First let's handle diffs + if (typeof obj === 'string' && t && t.patch) { + if (typeof t.patch == 'string') t.patch = [t.patch] + // Apply the patch locally + obj = apply_patch(bus.cache[obj] || {key: obj}, t.patch[0]) + } + + if (!('key' in obj) || typeof obj.key !== 'string') { + console.error('Error: set(obj) called on object without a key: ', obj) + console.trace('Bad set(obj)') + } + bogus_check(obj.key) + + t = t || {} + // Make sure it has a version. + t.version = t.version || new_version() + + if ((executing_funk !== global_funk) && executing_funk.loading()) { + abort_changes([obj.key]) + return + } + + if (honking_at(obj.key)) + var message = set_msg(obj, t, 'set') + + // Ignore if nothing happened + if (obj.key && !changed(obj)) { + statelog(obj.key, grey, 'x', message) + return + } else + statelog(obj.key, red, 'o', message) + + try { + statelog_indent++ + var was_saving = currently_saving + currently_saving = obj.key + + // Call the setter() handlers! + var num_handlers = bus.route(obj.key, 'setter', obj, t) + if (num_handlers === 0) { + // And fire if there weren't any! + set.fire(obj, t) + bus.route(obj.key, 'on_set_sync', obj, t) + } + } + finally { + statelog_indent-- + currently_saving = was_saving + } + // TODO: Here's an alternative. Instead of counting the handlers and + // seeing if there are zero, I could just make a setter handler that + // is shadowed by other handlers if I can get later handlers to shadow + // earlier ones. + } + + // set.sync() will set with the version of the current executing reaction + set.sync = function set_sync (obj, t) { + t = bus.clone(t || {}) + // t.version: executing_funk?.transaction?.version + // || executing_funk?.latest_reaction_at + t.version = ((executing_funk + && executing_funk.transaction + && executing_funk.transaction.version) + || (executing_funk + && executing_funk.latest_reaction_at)) + set(obj, t) + } + + // We might eventually want a set.fire.sync() too, which defaults the + // version to `executing_funk?.transaction?.version` + + set.fire = fire + function fire (obj, t) { + t = t || {} + + // Make sure it has a version. + t.version = t.version + || executing_funk && executing_funk.latest_reaction_at + || new_version() + + // Print a statelog entry + if (obj.key && honking_at(obj.key)) { + // Warning: Changes to *nested* objects will *not* be printed out! + // In the future, we'll remove the recursion from fire() so that + // nested objects aren't even changed. + var message = set_msg(obj, t, 'set.fire') + var color, icon + if (currently_saving === obj.key && + !(obj.key && !changed(obj))) { + statelog_indent-- + statelog(obj.key, red, '•', '↵' + + (t.version ? '\t\t\t[' + t.version + ']' : '')) + statelog_indent++ + } else { + // Ignore if nothing happened + if (obj.key && !changed(obj)) { + color = grey + icon = 'x' + if (t.getter) + message = (t.m) || 'Got ' + bus + "('"+obj.key+"')" + if (t.version) message += ' [' + t.version + ']' + statelog(obj.key, color, icon, message) + return + } + + color = red, icon = '•' + if (t.getter || pending_gets[obj.key]) { + color = green + icon = '^' + message = add_diff_msg((t.m)||'Got '+bus+"('"+obj.key+"')", + obj) + if (t.version) message += ' [' + t.version + ']' + } + + statelog(obj.key, color, icon, message) + } + } + // Then we're gonna fire! + + // Recursively add all of obj, and its sub-objects, into the cache + var modified_keys = update_cache(obj, cache) + + delete pending_gets[obj.key] + + if ((executing_funk !== global_funk) && executing_funk.loading()) { + abort_changes(modified_keys) + } else { + // Let's publish these changes! + + // These objects must replace their backups + update_cache(obj, backup_cache) + + // And we mark each changed key as changed so that + // reactions happen to them + for (var i=0; i < modified_keys.length; i++) { + var key = modified_keys[i] + var parents = [versions[key]] // Not stored yet + versions[key] = t.version + mark_changed(key, t) + } + } + } + + set.abort = function (obj, t) { + if (!obj) console.error('No obj', obj) + abort_changes([obj.key]) + statelog(obj.key, yellow, '<', 'Aborting ' + obj.key) + mark_changed(obj.key, t) + } + + var version_count = 0 + function new_version () { + return (bus.label||(id+' ')) + (version_count++).toString(36) + } + + // Now create the statebus object + function bus (arg1, arg2) { + // Called with a function to react to + if (typeof arg1 === 'function') { + var f = reactive(arg1) + f() + return f + } + + // Called with a key to produce a subspace + else return subspace(arg1, arg2) + } + var id = 'bus-' + Math.random().toString(36).substring(7) + bus.toString = function () { return bus.label || 'bus'+this_bus_num || id } + bus.delete_bus = function () { + // // Forget all wildcard handlers + // for (var i=0; i 1) + console.error('Got a {_key: ...} object with additional fields') + obj = bus.cache[obj._key] = bus.cache[obj._key] || {key: obj._key} + } + + // Fold cacheable objects into cache + else if (obj && obj.key) { + bogus_check(obj.key) + + if (cache !== backup_cache) + if (changed(obj)) + modified_keys.add(obj.key) + else + log('Boring modified key', obj.key) + if (!cache[obj.key]) + // This object is new. Let's store it. + cache[obj.key] = obj + + else if (obj !== cache[obj.key]) { + // Else, mutate cache to match the object. + + // First, add/update missing/changed fields to cache + for (var k in obj) + if (cache[obj.key][k] !== obj[k]) + cache[obj.key][k] = obj[k] + + // Then delete extra fields from cache + for (var k in cache[obj.key]) + if (!obj.hasOwnProperty(k)) + delete cache[obj.key][k] + } + obj = cache[obj.key] + } + + return obj + } + deep_map(object, update_object) + return modified_keys.values() + } + + function changed (object) { + return pending_gets[object.key] + || ! cache.hasOwnProperty(object.key) + || !backup_cache.hasOwnProperty(object.key) + || !(deep_equals(object, backup_cache[object.key])) + } + function abort_changes (keys) { + for (var i=0; i < keys.length; i++) + update_cache(backup_cache[keys[i]], cache) + } + + + function forget (key, set_handler, t) { + if (arguments.length === 0) { + // Then we're forgetting the executing funk + console.assert(executing_funk !== global_funk, + 'forget() with no arguments forgets the currently executing reactive function.\nHowever, there is no currently executing reactive function.') + executing_funk.forget() + return + } + bogus_check(key) + + //log('forget:', key, funk_name(set_handler), funk_name(executing_funk)) + set_handler = set_handler || executing_funk + var fkey = funk_key(set_handler) + //console.log('Gets in is', gets_in.hash) + if (!gets_in.has(key, fkey)) { + console.error("***\n****\nTrying to forget lost key", key, + 'from', funk_name(set_handler), fkey, + "that hasn't got that key.") + console.trace() + return + // throw Error('asdfalsdkfajsdf') + } + + gets_in.delete(key, fkey) + unbind(key, 'on_set', set_handler) + + // If this is the last handler listening to this key, then we can + // delete the cache entry, send a forget upstream, and de-activate the + // .on_get handler. + if (!gets_in.has_any(key)) { + clearTimeout(to_be_forgotten[key]) + to_be_forgotten[key] = setTimeout(function () { + // Send a forget upstream + bus.route(key, 'forgetter', key, t) + + // Delete the cache entry...? + // delete cache[key] + // delete backup_cache[key] + delete gets_out[key] + delete to_be_forgotten[key] + }, 200) + } + } + function del (key, t) { + key = key.key || key // Prolly disable this in future + bogus_check(key) + + if ((executing_funk !== global_funk) && executing_funk.loading()) { + abort_changes([key]) + return + } + + statelog(key, yellow, 'v', 'Deleting ' + key) + // Call the deleter handlers + var handlers_called = bus.route(key, 'deleter', key) + if (handlers_called === 0) { + // And go ahead and delete if there aren't any! + delete cache[key] + delete backup_cache[key] + } + + // Call the on_delete handlers + bus.route(key, 'on_delete', cache[key] || {key: key}, t) + + // console.warn("Deleting " + key + "-- Statebus doesn't yet re-run functions subscribed to it, or update versions") + + // Todos: + // + // - Add transactions, so you can check permissions, abort a delete, + // etc. + // - NOTE: I did a crappy implementation of abort just now above! + // But it doesn't work if called after the deleter handler returns. + // - Generalize the code across set and del with a "mutate" + // operation + // + // - Right now we fire the deleter handlers right here. + // + // - Do we want to batch them up and fire them later? + // e.g. we could make a mark_deleted(key) like mark_changed(key) + // + // - We might also record a new version of the state to show that + // it's been deleted, which we can use to cancel echoes from the + // sending bus. + + } + + var changed_keys = new Set() + var dirty_getters = {} // Maps funk_key => version dirtied at + function dirty (key, t) { + statelog(key, brown, '*', bus + ".dirty('"+key+"')") + bogus_check(key) + + var version = (t && t.version) || 'dirty-' + new_version() + + // Find any .getter, and mark as dirty so that it re-runs + var found = false + if (gets_out.hasOwnProperty(key)) + for (var i=0; i a', bus+'.'+event + "('" + (arg.key||arg) + "') is " + triggering + +'\n ' + funk_name(funck)) + } + + if (funk) { + // Then this is an on_set event re-triggering an already-wrapped + // funk. It has its own arg internally that it's calling itself + // with. Let's tell it to re-trigger itself with that arg. + + if (method !== 'on_set') { + console.error(method === 'on_set', 'Funk is being re-triggered, but isn\'t on_set. It is: "' + method + '", oh and funk: ' + funk_name(funk)) + return + } + return funk.react() + + // Hmm... does this do the right thing? Example: + // + // bus('foo').on_set = function (o) {...} + // set({key: 'foo'}) + // set({key: 'foo'}) + // set({key: 'foo'}) + // + // Does this spin up 3 reactive functions? Hmm... + // Well, I think it does, but they all get forgotten once + // they run once, and then are garbage collected. + // + // bus('foo*').on_set = function (o) {...} + // set({key: 'foo1'}) + // set({key: 'foo2'}) + // set({key: 'foo1'}) + // set({key: 'foo3'}) + // + // Does this work ok? Yeah, I think so. + } + + // Alright then. Let's wrap this func with some funk. + + // Fresh get/set/forget/delete handlers will just be regular + // functions. We'll store their arg and transaction and let them + // re-run until they are done re-running. + function key_arg () { return ((typeof arg.key) == 'string') ? arg.key : arg } + function rest_arg () { return (key_arg()).substr(binding.length-1) } + function val_arg () { + console.assert(method === 'setter' || method === 'on_set' || method === 'on_set_sync', + 'Bad method for val_arg()') + // Is there a time I am supposed to return bus.cache[arg]? It used to say: + // arg.key ? arg : bus.cache[arg] + + return arg.key ? (func.use_linked_json ? arg.val : arg) : undefined + } + function vars_arg () { + var r = rest_arg() + try { + return JSON.parse(r) + } catch (e) { + return 'Bad JSON "' + r + '" for key ' + key_arg() + } + } + var f = reactive(function () { + // Initialize transaction + t = clone(t || {}) + + // Add .abort() method + if (method === 'setter' || method === 'deleter') + t.abort = function () { + var key = method === 'setter' ? arg.key : arg + if (f.loading()) return + bus.cache[key] = bus.cache[key] || {key: key} + bus.backup_cache[key] = bus.backup_cache[key] || {key: key} + bus.set.abort(bus.cache[key]) + } + + // Add .done() method + if (method !== 'forgetter') + t.done = function (o) { + var key = method === 'setter' ? arg.key : arg + if (func.use_linked_json) + o = {key, val: o} + bus.log('We are DONE()ing', method, key, o||arg) + + // We use a simple (and crappy?) heuristic to know if the + // setter handler has changed the state: whether the + // programmer passed (o) to the t.done(o) handler. If + // not, we assume it hasn't changed. If so, we assume it + // *has* changed, and thus we change the version of the + // state. I imagine it would be more accurate to diff + // from before the setter handler began with when + // t.done(o) ran. + // + // Note: We will likely solve this in the future by + // preventing .setter() from changing the incoming state, + // except through an explicit .revise() function. + if (o) t.version = new_version() + + if (method === 'deleter') { + delete bus.cache[key] + delete bus.backup_cache[key] + } + else if (method === 'setter') { + bus.set.fire(o || arg, t) + bus.route(key, 'on_set_sync', o || arg, t) + } else { + // Now method === 'getter' + o.key = key + bus.set.fire(o, t) + // And now reset the version cause it could get called again + delete t.version + } + } + + // Alias .return() to .done(), in case that feels better to you + t.return = t.done + + // Prepush Scratch: + // var prepushed = {} + // t.prepush = function (key) { + // prepushed[key] = bus.get(key) + // } + + // Alias t.reget() to bus.dirty() + if (method === 'setter') + t.reget = function () { bus.dirty(arg.key) } + + // Now to call the handler, let's line up the function's special + // named arguemnts like key, o, t, rest, vars, etc. + var args = [] + args[0] = (method in {setter:1, on_set:1, on_set_sync:1}) ? val_arg() : arg + args[1] = t + for (var k in (func.args||{})) { + switch (k) { + case 'key': + args[func.args[k]] = key_arg(); break + case 'rest': + args[func.args[k]] = rest_arg(); break + case 'vars': + args[func.args[k]] = vars_arg(); + break + case 't': + args[func.args[k]] = t; break + case 'obj': + args[func.args[k]] = val_arg(); break + case 'old': + var key = key_arg() + args[func.args[k]] = bus.cache[key] || (bus.cache[key] = {key:key}) + break + } + } + + // Call the raw function here! + var result = func.apply(null, args) + + // We will wanna add in the fancy arg stuff here, with: + // arr = [] + // for (var k of func.args || {}) + // arr[func.args[k]] = + + // Trigger done() or abort() by return value + console.assert(!(result === 'getter' && + (result === 'done' || result === 'abort')), + 'Returning "done" or "abort" is not allowed from getter handlers') + + if (result === 'done') t.done() + if (result === 'abort') t.abort() + + // For get + if (func.use_linked_json) { + if (method === 'getter' && result !== undefined && !f.loading()) { + var obj = {key: arg, val: result} + var new_t = clone(t || {}) + new_t.getter = true + set.fire(obj, new_t) + return result + } + } else { + if (method === 'getter' && result instanceof Object + && !f.loading() // Experimental. + ) { + result.key = arg + var new_t = clone(t || {}) + new_t.getter = true + set.fire(result, new_t) + return result + } + } + + // Set, forget and delete handlers stop re-running once they've + // completed without anything loading. + // ... with f.forget() + if (method !== 'getter' && !f.loading()) + f.forget() + }) + f.proxies_for = func + f.arg = arg + f.transaction = t || {} + + // getter handlers stop re-running when the key is forgotten + if (method === 'getter') { + var key = arg + function handler_done () { + f.forget() + unbind(key, 'forgetter', handler_done) + } + bind(key, 'forgetter', handler_done) + + // // Check if it's doubled-up + // if (gets_out[key]) + // console.error('Two .getter functions are running on the same key', + // key+'!', funk_name(funck), funk_name(gets_out[key])) + + gets_out[key] = gets_out[key] || [] + gets_out[key].push(f) // Record active getter handler + pending_gets[key] = f // Record that the get is pending + } + + if (just_make_it) + return f + + return f() + } + + // route() can be overridden + bus.route = function (key, method, arg, t) { + var handlers = bus.bindings(key, method) + if (handlers.length) + log('route:', bus+'("'+key+'").'+method+'['+handlers.length+'](key:"'+(arg.key||arg)+'")') + // log('route: got bindings', + // funcs.map(function (f) {return funk_key(f)+':'+funk_keyr(f)})) + for (var i=0; i 50) seen_versions.shift() + } + funk.react = function () { + var result + try { + funk.called_directly = false + result = funk() + } finally { + funk.called_directly = true + } + return result + } + funk.forget = function () { + // Todo: This will bug out if an .on_set handler for a key also + // gets that key once, and then doesn't get it again, because + // when it gets the key, that key will end up being a + // getted_key, and will then be forgotten as soon as the funk is + // re-run, and doesn't get it again, and the fact that it is + // defined as an .on_set .on_set handler won't matter anymore. + + if (funk.statebus_id === 'global funk') return + + for (var hash in funk.getted_keys) { + var tmp = JSON.parse(hash), + bus = busses[tmp[0]], key = tmp[1] + if (bus) // Cause it might have been deleted + bus.forget(key, funk) + } + funk.getted_keys = {} + } + funk.loading = function () { + for (var hash in funk.getted_keys) { + var tmp = JSON.parse(hash), + bus = busses[tmp[0]], key = tmp[1] + if (bus // Cause it might have been deleted + && bus.pending_gets[key]) + return true + } + return false + } + + // for backwards compatibility + funk.is_loading = funk.loading + + return funk + } + + function loading_keys (keys) { + // Do any of these keys have outstanding gets? + //console.log('Loading: pending_keys is', pending_gets) + for (var i=0; i + bus.get_once(key, (o) => resolve(o))) + } + + // ****************** + // Proxy + + var symbols = { + is_proxy: Symbol('is_proxy'), + is_link: Symbol('is_link'), + get_json: Symbol('get_json'), + get_base: Symbol('get_base') + } + function make_proxy () { + function item_proxy (base, o) { + + // Primitives pass through unscathed + if (typeof o === 'number' + || typeof o === 'string' + || typeof o === 'boolean' + || o === undefined + || o === null + || typeof o === 'function') + + return o + + // We recursively descend through {key: ...} links + if (typeof o === 'object' && 'link' in o) { + var new_base = bus.get(o.link) + return item_proxy(new_base, new_base.val) + } + + + // For function proxies: + // + // // Javascript won't let us function call a proxy unless the + // // "target" is a function. So we make a dummy target, and + // // don't use it. + // var dummy = function () {} + + + return new Proxy(o, { + get: function get(o, k) { + if (k === 'inspect' || k === 'valueOf') + return undefined + if (k === symbols.is_proxy) + return true + if (typeof k === 'symbol') + return undefined + return item_proxy(base, o[proxied_2_keyed(k)]) + }, + set: function set(o, k, v) { + var value = translate_fields(v, proxied_2_keyed) + o[proxied_2_keyed(k)] = value + bus.set(base) + return true + }, + has: function has(o, k) { + return o.hasOwnProperty(proxied_2_keyed(k)) + }, + deleteProperty: function del (o, k) { + delete o[proxied_2_keyed(k)] + } + // For function proxies: + // + // apply: function apply (o, This, args) { + // return translate_fields(o, keyed_2_proxied) + // } + })} + + + // For function proxies: + // var dummy = function () {} + + // The top-level Proxy object holds HTTP resources + return new Proxy(cache, { + get: function get(o, k) { + if (k === 'inspect' || k === 'valueOf' || typeof k === 'symbol') + return undefined + bogus_check(k) + var base = bus.get(k) + return item_proxy(base, base.val) + }, + set: function set(o, key, val) { + bus.set({key: key, + val: translate_fields(val, proxied_2_keyed)}) + return true + }, + deleteProperty: function del (o, k) { + bus.delete(proxied_2_keyed(k)) + }, + // For function proxies: + // apply: function apply (o, This, args) { + // return 'fluffy' + // } + }) + } + bus.state = make_proxy() + + function link (url) { + var result = bus.get(url) + result[symbols.is_link] = true + return result + } + + + // So chrome can print out proxy objects decently + if (!nodejs) + window.devtoolsFormatters = [{ + header: function (x) { + if (x[symbols.is_proxy]) + return ['span', {style: 'background-color: #fffbe5; padding: 3px;'}, + JSON.stringify(x)] + // For function proxies: + // JSON.stringify(x(), null, 2)] + }, + hasBody: function (x) {return false} + }] + + // ****************** + // Network client + function get_domain (key) { // Returns e.g. "state://foo.com" + var m = key.match(/^i?statei?\:\/\/(([^:\/?#]*)(?:\:([0-9]+))?)/) + return m && m[0] + } + function message_method (m) { + return (m.get && 'get') + || (m.set && 'set') + || (m['delete'] && 'delete') + || (m.forget && 'forget') + } + + function ws_mount (prefix, url, client_creds) { + // Local: state://foo.com/* or /* + var preprefix = prefix.slice(0,-1) + var is_absolute = /^i?statei?:\/\// + var has_prefix = new RegExp('^' + preprefix) + var bus = this + var sock + var attempts = 0 + var outbox = [] + var client_getted_keys = new bus.Set() + var heartbeat + if (url[url.length-1]=='/') url = url.substr(0,url.length-1) + function nlog (s) { + if (nodejs) {console.log(s)} else console.log('%c' + s, 'color: blue') + } + function send (o, pushpop) { + pushpop = pushpop || 'push' + o = rem_prefixes(o) + var m = message_method(o) + if (m == 'get' || m == 'delete' || m == 'forget') + o[m] = rem_prefix(o[m]) + bus.log('ws_mount.send:', JSON.stringify(o)) + outbox[pushpop](JSON.stringify(o)) + flush_outbox() + } + function flush_outbox() { + if (sock.readyState === 1) + while (outbox.length > 0) + + // Debug mode can simulate network latency + if (bus.simulate_network_delay) { + var msg = outbox.shift() + setTimeout((function () { sock.send(msg) }), bus.simulate_network_delay) + } + + // But normally we just send the message immediately + else + sock.send(outbox.shift()) + else + setTimeout(flush_outbox, 400) + } + function add_prefix (key) { + return is_absolute.test(key) ? key : preprefix + key } + function rem_prefix (key) { + return has_prefix.test(key) ? key.substr(preprefix.length) : key } + function add_prefixes (obj) { + var keyed = bus.translate_keys(bus.clone(obj), add_prefix) + return bus.translate_links(bus.clone(keyed), add_prefix) + } + function rem_prefixes (obj) { + var keyed = bus.translate_keys(bus.clone(obj), rem_prefix) + return bus.translate_links(bus.clone(keyed), rem_prefix) + } + + bus(prefix).setter = function (obj, t) { + bus.set.fire(obj) + var x = {set: obj} + if (t.version) x.version = t.version + if (t.parents) x.parents = t.parents + if (t.patch) x.patch = t.patch + if (t.patch) x.set = rem_prefix(x.set.key) + send(x) + } + bus(prefix).getter = function (key) { send({get: key}), + client_getted_keys.add(key) } + bus(prefix).forgetter = function (key) { send({forget: key}), + client_getted_keys.delete(key) } + bus(prefix).deleter = function (key, t) { + t.done() + send({'delete': key}) + } + + function connect () { + nlog('[ ] trying to open ' + url) + sock = bus.make_websocket(url) + sock.onopen = function() { + nlog('[*] opened ' + url) + + // Update state + var peers = bus.get('peers') + peers[url] = peers[url] || {} + peers[url].connected = true + set(peers) + + // Login + var creds = client_creds || (bus.client_creds && bus.client_creds(url)) + if (creds) { + var i = [] + function intro (o) {i.push(JSON.stringify({set: o}))} + if (creds.clientid) + intro({key: 'current_user', val: {client: creds.clientid}}) + if (creds.name && creds.pass) + intro({key: 'current_user', val: {login_as: {name: creds.name, pass: creds.pass}}}) + // Todo: make this kinda thing work: + if (creds.private_key && creds.public_key) { + // Send public_key... start waiting for a + // challenge... look up server's public key, verify + // signature from server's challenge, then respond to + // challenge. + + // This will be used for mailbus + } + outbox = i.concat(outbox); flush_outbox() + } + + // Reconnect + if (attempts > 0) { + // Then we need to reget everything, cause it + // might have changed + var keys = client_getted_keys.values() + for (var i=0; i= 0, 'Index '+subpath+' is too small') + console.assert(slice_end <= curr_obj.length - 1, + 'Index '+subpath+' is too big') + curr_obj[slice_end] = new_stuff + } + + return obj + } + + // Otherwise, descend down the path + console.assert(!slice_start, 'No splices allowed in middle of path') + last_obj = curr_obj + last_field = field + curr_obj = curr_obj[field || slice_end] + path = path.substr(subpath.length) + } + } + + // ****************** + // Utility funcs + function parse (s) {try {return JSON.parse(s)} catch (e) {return {}}} + function One_To_Many() { + var hash = this.hash = {} + var counts = {} + this.get = function (k) { return Object.keys(hash[k] || {}) } + this.add = function (k, v) { + if (hash[k] === undefined) hash[k] = {} + if (counts[k] === undefined) counts[k] = 0 + if (!hash[k][v]) counts[k]++ + hash[k][v] = true + } + this.delete = function (k, v) { delete hash[k][v]; counts[k]-- } + this.delete_all = function (k) { delete hash[k]; delete counts[k] } + this.has = function (k, v) { return hash[k] && hash[k][v] } + this.has_any = function (k) { return counts[k] } + this.del = this.delete // for compatibility; remove this soon + } + function Set () { + var hash = {} + this.add = function (a) { hash[a] = true } + this.has = function (a) { return a in hash } + this.values = function () { return Object.keys(hash) } + this.delete = function (a) { delete hash[a] } + this.clear = function () { hash = {} } + this.del = this.delete // for compatibility; remove this soon + this.all = this.values // for compatibility; remove this soon + } + //Set = window.Set || Set + // function clone(obj) { + // if (obj == null) return obj + // var copy = obj.constructor() + // for (var attr in obj) + // if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr] + // return copy + // } + function clone(item) { + if (!item // null, undefined values check + || item instanceof Number + || item instanceof String + || item instanceof Boolean) + return item + + if (Array.isArray(item)) { + item = item.slice() + for (var i=0; i 1) return '**' + f_string + '**' + + var def = f.defined[0] + switch (def.as) { + case 'handler': + return def.bus+"('"+def.key+"')."+def.method+' = '+f_string + case 'get callback': + return 'get('+def.key+', '+f_string+')' + case 'reactive': + return "reactive('"+f_string+"')" + default: + return 'UNKNOWN Funky Definition!!!... ???' + } + } + + function deps (key) { + // First print out everything waiting for it to pub + var result = 'Deps: ('+key+') fires into:' + var pubbers = bindings(key, 'on_set') + if (pubbers.length === 0) result += ' nothing' + for (var i=0; i{};const r=t?document.querySelector("script[type=esms-options]"):void 0;const s=r?JSON.parse(r.innerHTML):{};Object.assign(s,self.esmsInitOptions||{});let n=!t||!!s.shimMode;const a=globalHook(n&&s.onimport);const i=globalHook(n&&s.resolve);let c=s.fetch?globalHook(s.fetch):fetch;const f=s.meta?globalHook(n&&s.meta):noop;const ne=s.mapOverrides;let oe=s.nonce;if(!oe&&t){const e=document.querySelector("script[nonce]");e&&(oe=e.nonce||e.getAttribute("nonce"))}const ce=globalHook(s.onerror||noop);const le=s.onpolyfill?globalHook(s.onpolyfill):()=>{console.log("%c^^ Module TypeError above is polyfilled and can be ignored ^^","font-weight:900;color:#391")};const{revokeBlobURLs:ue,noLoadEventRetriggers:de,enforceIntegrity:pe}=s;function globalHook(e){return typeof e==="string"?self[e]:e}const he=Array.isArray(s.polyfillEnable)?s.polyfillEnable:[];const me=he.includes("css-modules");const be=he.includes("json-modules");const ke=!navigator.userAgentData&&!!navigator.userAgent.match(/Edge\/\d+\.\d+/);const we=t?document.baseURI:`${location.protocol}//${location.host}${location.pathname.includes("/")?location.pathname.slice(0,location.pathname.lastIndexOf("/")+1):location.pathname}`;const createBlob=(e,t="text/javascript")=>URL.createObjectURL(new Blob([e],{type:t}));let{skip:ge}=s;if(Array.isArray(ge)){const e=ge.map((e=>new URL(e,we).href));ge=t=>e.some((e=>e[e.length-1]==="/"&&t.startsWith(e)||t===e))}else if(typeof ge==="string"){const e=new RegExp(ge);ge=t=>e.test(t)}else ge instanceof RegExp&&(ge=e=>ge.test(e));const eoop=e=>setTimeout((()=>{throw e}));const throwError=t=>{(self.reportError||e&&window.safari&&console.error||eoop)(t),void ce(t)};function fromParent(e){return e?` imported from ${e}`:""}let ve=false;function setImportMapSrcOrLazy(){ve=true}if(!n)if(document.querySelectorAll("script[type=module-shim],script[type=importmap-shim],link[rel=modulepreload-shim]").length)n=true;else{let e=false;for(const t of document.querySelectorAll("script[type=module],script[type=importmap]"))if(e){if(t.type==="importmap"&&e){ve=true;break}}else t.type!=="module"||t.ep||(e=true)}const ye=/\\/g;function asURL(e){try{if(e.indexOf(":")!==-1)return new URL(e).href}catch(e){}}function resolveUrl(e,t){return resolveIfNotPlainOrUrl(e,t)||asURL(e)||resolveIfNotPlainOrUrl("./"+e,t)}function resolveIfNotPlainOrUrl(e,t){const r=t.indexOf("#"),s=t.indexOf("?");r+s>-2&&(t=t.slice(0,r===-1?s:s===-1||s>r?r:s));e.indexOf("\\")!==-1&&(e=e.replace(ye,"/"));if(e[0]==="/"&&e[1]==="/")return t.slice(0,t.indexOf(":")+1)+e;if(e[0]==="."&&(e[1]==="/"||e[1]==="."&&(e[2]==="/"||e.length===2&&(e+="/"))||e.length===1&&(e+="/"))||e[0]==="/"){const r=t.slice(0,t.indexOf(":")+1);if(r==="blob:")throw new TypeError(`Failed to resolve module specifier "${e}". Invalid relative url or base scheme isn't hierarchical.`);let s;if(t[r.length+1]==="/")if(r!=="file:"){s=t.slice(r.length+2);s=s.slice(s.indexOf("/")+1)}else s=t.slice(8);else s=t.slice(r.length+(t[r.length]==="/"));if(e[0]==="/")return t.slice(0,t.length-s.length-1)+e;const n=s.slice(0,s.lastIndexOf("/")+1)+e;const a=[];let i=-1;for(let e=0;e "${e[a]}" does not resolve`)}}let $e=!t&&(0,eval)("u=>import(u)");let Se;const Oe=t&&new Promise((e=>{const t=Object.assign(document.createElement("script"),{src:createBlob("self._d=u=>import(u)"),ep:true});t.setAttribute("nonce",oe);t.addEventListener("load",(()=>{if(!(Se=!!($e=self._d))){let e;window.addEventListener("error",(t=>e=t));$e=(t,r)=>new Promise(((s,n)=>{const a=Object.assign(document.createElement("script"),{type:"module",src:createBlob(`import*as m from'${t}';self._esmsi=m`)});e=void 0;a.ep=true;oe&&a.setAttribute("nonce",oe);a.addEventListener("error",cb);a.addEventListener("load",cb);function cb(i){document.head.removeChild(a);if(self._esmsi){s(self._esmsi,we);self._esmsi=void 0}else{n(!(i instanceof Event)&&i||e&&e.error||new Error(`Error loading ${r&&r.errUrl||t} (${a.src}).`));e=void 0}}document.head.appendChild(a)}))}document.head.removeChild(t);delete self._d;e()}));document.head.appendChild(t)}));let Le=false;let xe=false;const Ae=t&&HTMLScriptElement.supports;let Ce=Ae&&Ae.name==="supports"&&Ae("importmap");let Ue=Se;const Ee="import.meta";const Pe='import"x"assert{type:"css"}';const Ie='import"x"assert{type:"json"}';let Me=Promise.resolve(Oe).then((()=>{if(Se)return t?new Promise((e=>{const t=document.createElement("iframe");t.style.display="none";t.setAttribute("nonce",oe);function cb({data:r}){const s=Array.isArray(r)&&r[0]==="esms";if(s){Ce=r[1];Ue=r[2];xe=r[3];Le=r[4];e();document.head.removeChild(t);window.removeEventListener("message",cb,false)}}window.addEventListener("message",cb,false);const r=`