├── .gitignore ├── .npmignore ├── CHANGELOG ├── README.md ├── dist ├── cache │ ├── browser.js │ ├── index.js │ └── package.json ├── components.js ├── csp.js ├── demux.js ├── fetch.js ├── fp.js ├── hamt.js ├── index.js ├── lazy.js ├── meta.js ├── model.js ├── monad.js ├── mux.js ├── ot.js ├── resource.js ├── router-alt.js ├── router.js ├── store.js └── vdom.js ├── esdoc.json ├── package.json └── src ├── cache ├── browser.js ├── index.js └── package.json ├── components.js ├── csp.js ├── fetch.js ├── fp.js ├── hamt.js ├── index.js ├── lazy.js ├── meta.js ├── model.js ├── monad.js ├── mux.js ├── ot.js ├── resource.js ├── router-alt.js ├── router.js ├── store.js └── vdom.js /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | pids 4 | *.pid 5 | *.seed 6 | lib-cov 7 | coverage 8 | .grunt 9 | .lock-wscript 10 | build/Release 11 | node_modules 12 | dist/esdoc -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | --- 3 | 4 | Dec 2, 2015 5 | 6 | - added `vdom` implementation that can render a VDOM both client and server-side 7 | 8 | Nov 19, 2015 9 | 10 | - added `fp` for transducers and general functional techniques, such as `concatAll`, `concatMap`, transducers, etc. 11 | 12 | Nov 10, 2015 13 | 14 | - added `channel()` CSP implementation 15 | 16 | Nov 8, 2015 17 | 18 | - added notes to README, the [What and What](#what-and-why) and [How to learn this library](#how-to-learn-this-library) sections 19 | - added `mux` implementation and `router` implementation 20 | - numerous bug fixes for the resource 21 | - numerous speed and other enhancements for the various libs 22 | 23 | Nov 1, 2015 24 | 25 | - updates to build system, precompiled to es5 26 | 27 | Oct 22, 2015 28 | 29 | - project started 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Universal Utils 2 | 3 | --- 4 | 5 | [![NPM](https://nodei.co/npm/universal-utils.png)](https://nodei.co/npm/universal-utils/) 6 | ![](https://david-dm.org/matthiasak/universal-utils.svg) 7 | 8 | Small functional problem-solving, event, state-management, and caching utilities. 9 | 10 | #### How to get started 11 | 12 | 1. start your own node project, then `npm i -S universal-utils` 13 | 2. this package is compiled to es5, so you don't need to worry about the Babel require hook, or anything of the sort... just do... 14 | 3. `import * as utils from 'universal-utils'` to use this package in server or client-side 15 | 16 | #### Who? 17 | 18 | Matthew Keas, [@matthiasak](https://twitter.com/@matthiasak) 19 | 20 | #### What and Why? 21 | 22 | These are **tiny** utilities that limit API "surface area" yet __pack a lot of punch__. Each of them represents a use-case from popular libraries, but condensed into very modular, reusable parts. 23 | 24 | By having tiny modules that provide a highly scoped feature-set, I have been able to compose and reuse elements of functional programming, and create and abstract wrappers around functions, and wrappers around other wrappers. This lets me configure my code to an exact need and specification, all the while keeping modules testable and running at lightning speed. 25 | 26 | Functionally-oriented code is all the rage these days, but in this post I want to emphasize that functional programming is a subset of a more important overarching programming paradigm: compositional programming. 27 | 28 | If you've ever used Unix pipes, you'll understand the importance and flexibility of composing small reusable programs to get powerful and emergent behaviors. Similarly, if you program functionally, you'll know how cool it is to compose a bunch of small reusable functions into a fully featured program. 29 | 30 | #### How to learn this dictionary of microlibs 31 | 32 | Since each file in this library is an abstraction of some sort, I will address the simplest pieces first, and then work my way up to the most complex parts. Learn how to use these utilities by following along in this order: 33 | 34 | 1. [package.json](package.json) - take a look at what libraries are installed by default when you require this package. 35 | 2. [index.js](src/index.js) - everything in this repo is simply an exported module by index.js. 36 | 3. [fetch.js](src/fetch.js) - learn how a single `fetch()` function can be used to reuse in-flight network requests to the same URL. 37 | 4. [store.js](src/store.js) - a universal `store()` that maintains immutable pure JSON state (**NOTE: can only have Objects and Arrays, no custom constructors stored in it**) and implements an asynchronous flux/redux pattern that can chain reducer functions together to represent state; learn how to make a simple "flux-like", "redux-like" event-driven `store()`. 38 | 5. [mux.js](src/mux.js) - a universal `mux()` wrapper that can multiplex requests from one application to another ("mux" 10 browser-side network requests into 1 request to be sent to the server); learn how to batch requests together into a single network request, given to an API server to help multiplex chatty programs into fewer requests. 39 | 6. [cache](src/cache) - observe the API of cache implementations... just two methods with similar signatures: 40 | 41 | - `getItem(key, expire=false): Promise` 42 | - `setItem(key, value, timeout=5*60*60*1000): Promise` 43 | 44 | Based on whether the browser or node is `require()`ing this folder, the API will let you cache data in WebSQL/localstorage (browser) or Redis/in-memory (node). 45 | 46 | 7. [resource.js](src/resource.js) - learn how to wrap a store, muxer, fetcher, cache, and other configurations into a universal wrapper that can automatically keep cached API requests in both the client or server. 47 | 8. [router-alt.js](src/router-alt.js) - This is a simple routing library that can be used in lieue of larger, more verbose libraries and implementations out there like page.js or Backbone's `Backbone.Router()`. See also [router.js](src/router.js), an older version. 48 | 9. [csp.js](src/csp.js) - learn how to use a simple `channel()` implementation for using simple go-like coroutines that can `put()` and `take()` values into and out of the channel. 49 | 10. [fp.js](src/fp.js) - learn about some more functional-esque approaches to problem solving, including the use of transducers. 50 | 11. [vdom.js](src/vdom.js) - learn about an ultra tiny, minimal weight and shallow API VDOM implementation. 51 | 12. [ot.js](src/ot.js) - learn how to share and apply micro-transforms as chronological changes b/w multiple data sources (i.e. build live editors like Google Docs). Combine this "opchain" engine with channels, and you can have 'over-the-wire' live editing much like Google Docs provides. 52 | 13. [hamt.js](src/hamt.js) - learn about Hash Array Mapped Tries and persistent data structures in this ultra-minimal implementation of `list`'s and `hashmap`'s with a backing persistent data structure. 53 | 14. [model.js](src/model.js) - learn about building a rules-based, or constructor/type-based engine that validates deeply-nested data structures that follow a certain pattern. Basically, an ORM that validates Plain Old Javascript Objects. 54 | 15. [meta.js](src/meta.js) - learn about programming certain methods that are focused on metaprogramming – functions that manipulate or alter your own code, such as a `mixin()` function that can build mixins for an ES6 `class`. 55 | 56 | #### License 57 | 58 | MIT. 59 | -------------------------------------------------------------------------------- /dist/cache/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var s = window.localStorage; 7 | 8 | var cacheCreator = exports.cacheCreator = function cacheCreator() { 9 | 10 | var getItem = function getItem(key) { 11 | var expire = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; 12 | 13 | try { 14 | var data = JSON.parse(s.getItem(key)); 15 | if (!data || !data.data) throw 'not in cache'; 16 | var expired = expire || +new Date() > data.expiresAt; 17 | if (expired) return Promise.reject(key + ' is expired'); 18 | return Promise.resolve(data.data); 19 | } catch (e) { 20 | return Promise.reject(key + ' not in cache'); 21 | } 22 | }; 23 | 24 | var setItem = function setItem(key, data) { 25 | var timeout = arguments.length <= 2 || arguments[2] === undefined ? 5 * 60 * 60 * 1000 : arguments[2]; 26 | var expiresAt = arguments.length <= 3 || arguments[3] === undefined ? +new Date() + timeout : arguments[3]; 27 | 28 | if (!data) return Promise.reject('data being set on ' + key + ' was null/undefined'); 29 | return new Promise(function (res, rej) { 30 | try { 31 | s.setItem(key, JSON.stringify({ expiresAt: expiresAt, data: data })); 32 | res(true); 33 | } catch (e) { 34 | rej('key ' + key + ' has a value of ' + val + ', which can\'t be serialized'); 35 | } 36 | }); 37 | }; 38 | 39 | var clearAll = function clearAll(key) { 40 | if (!key) s.clear(); 41 | for (var i in s) { 42 | if ((!key || i.indexOf(key) !== -1) && localstorage.hasOwnProperty(i)) s.removeItem(i); 43 | } 44 | return Promise.resolve(true); 45 | }; 46 | 47 | return { getItem: getItem, setItem: setItem, clearAll: clearAll }; 48 | }; 49 | 50 | var cache = exports.cache = cacheCreator(); -------------------------------------------------------------------------------- /dist/cache/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 8 | 9 | var clone = function clone(obj) { 10 | return JSON.parse(JSON.stringify(obj)); 11 | }; 12 | 13 | var cacheCreator = exports.cacheCreator = function cacheCreator() { 14 | var REDIS_URL = process.env.REDIS_URL; 15 | 16 | 17 | if (REDIS_URL) { 18 | var _ret = function () { 19 | 20 | var client = require('redis').createClient(REDIS_URL); 21 | 22 | "ready,connect,error,reconnecting,end".split(',').map(function (event) { 23 | return client.on(event, function (msg) { 24 | return console.log('Redis ' + event + ' ' + (msg ? ' :: ' + msg : '')); 25 | }); 26 | }); 27 | 28 | var getItem = function getItem(key, expire) { 29 | return new Promise(function (res, rej) { 30 | client.get(key, function (err, data) { 31 | if (err || !data) { 32 | return rej(key + ' not in cache'); 33 | } 34 | data = JSON.parse(data); 35 | var expired = expire || +new Date() > data.expiresAt; 36 | if (expired) rej(key + ' is expired'); 37 | res(data.data); 38 | }); 39 | }); 40 | }; 41 | 42 | var setItem = function setItem(key, val) { 43 | var timeout = arguments.length <= 2 || arguments[2] === undefined ? 5 * 60 * 60 * 1000 : arguments[2]; 44 | 45 | var expiresAt = +new Date() + timeout; 46 | return new Promise(function (res, rej) { 47 | client.set(key, JSON.stringify({ expiresAt: expiresAt, data: val }), function () { 48 | res(val); 49 | }); 50 | }); 51 | }; 52 | 53 | var clearAll = function clearAll(key) { 54 | return new Promise(function (res, rej) { 55 | client.keys(key, function () { 56 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 57 | args[_key] = arguments[_key]; 58 | } 59 | 60 | return args.map(function (x) { 61 | return client.del(x); 62 | }); 63 | }); 64 | 65 | res(); 66 | }); 67 | }; 68 | 69 | return { 70 | v: { getItem: getItem, setItem: setItem, clearAll: clearAll } 71 | }; 72 | }(); 73 | 74 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 75 | } else { 76 | var _ret2 = function () { 77 | 78 | var cache = {}; 79 | 80 | var getItem = function getItem(key, expire) { 81 | return new Promise(function (res, rej) { 82 | if (key in cache) { 83 | var data = clone(cache[key]), 84 | expired = expire || data.expiresAt < +new Date(); 85 | if (expired) return rej(key + ' is expired'); 86 | if (!data.data) return rej(key + ' has no data'); 87 | return res(data.data); 88 | } 89 | rej(key + ' not in cache'); 90 | }); 91 | }; 92 | 93 | var setItem = function setItem(key, val) { 94 | var timeout = arguments.length <= 2 || arguments[2] === undefined ? 5 * 60 * 60 * 1000 : arguments[2]; 95 | 96 | var expiresAt = +new Date() + timeout; 97 | var data = { expiresAt: expiresAt, data: val }; 98 | return new Promise(function (res, rej) { 99 | cache[key] = clone(data); 100 | res(clone(data).data); 101 | }); 102 | }; 103 | 104 | var clearAll = function clearAll(key) { 105 | Object.keys(cache).filter(function (n) { 106 | return n.indexOf(key) !== -1; 107 | }).map(function (x) { 108 | return delete cache[x]; 109 | }); 110 | return Promise.resolve(); 111 | }; 112 | 113 | return { 114 | v: { getItem: getItem, setItem: setItem, clearAll: clearAll } 115 | }; 116 | }(); 117 | 118 | if ((typeof _ret2 === 'undefined' ? 'undefined' : _typeof(_ret2)) === "object") return _ret2.v; 119 | } 120 | }; 121 | 122 | var cache = exports.cache = cacheCreator(); -------------------------------------------------------------------------------- /dist/cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser": "browser.js" 3 | } -------------------------------------------------------------------------------- /dist/components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.app = exports.page = exports.hashrouter = exports.injectSVG = exports.loadSVG = exports.imageLoader = exports.scrambler = exports.wait = exports.range = exports.scramble = exports.chars = exports.markdown = exports.trackVisibility = exports.viewportHeight = exports.applinks = exports.chrome = exports.safari = exports.iOS = exports.google_plus = exports.twitter_card = exports.fb_instantarticle = exports.fb_opengraph = exports.googleAnalytics = exports.mobile_metas = exports.theme = exports.head = undefined; 7 | 8 | var _vdom2 = require('./vdom'); 9 | 10 | var _routerAlt = require('./router-alt'); 11 | 12 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } /* huge ups to John Buschea (https://github.com/joshbuchea/HEAD) */ 13 | 14 | 15 | var head = exports.head = function head() { 16 | for (var _len = arguments.length, c = Array(_len), _key = 0; _key < _len; _key++) { 17 | c[_key] = arguments[_key]; 18 | } 19 | 20 | var loaded_once = false; 21 | var config = function config(el) { 22 | return loaded_once = true; 23 | }; 24 | return (0, _vdom2.m)('head', { config: config, shouldUpdate: function shouldUpdate(el) { 25 | return !el; 26 | } }, c); 27 | }; 28 | 29 | // More info: https://developer.chrome.com/multidevice/android/installtohomescreen 30 | var theme = exports.theme = function theme() { 31 | var color = arguments.length <= 0 || arguments[0] === undefined ? 'black' : arguments[0]; 32 | return [(0, _vdom2.m)('meta', { name: 'theme-color', content: color }), (0, _vdom2.m)('meta', { name: 'msapplication-TileColor', content: color })]; 33 | }; 34 | 35 | var mobile_metas = exports.mobile_metas = function mobile_metas() { 36 | var title = arguments.length <= 0 || arguments[0] === undefined ? '' : arguments[0]; 37 | var img = arguments.length <= 1 || arguments[1] === undefined ? 'icon' : arguments[1]; 38 | var manifest = arguments.length <= 2 || arguments[2] === undefined ? 'manifest' : arguments[2]; 39 | return [(0, _vdom2.m)('meta', { charset: 'utf8' }), (0, _vdom2.m)('meta', { 'http-equiv': 'x-ua-compatible', content: 'ie=edge' }), (0, _vdom2.m)('meta', { name: "viewport", content: "width=device-width, initial-scale=1.0, shrink-to-fit=no" }), (0, _vdom2.m)('title', title)].concat(_toConsumableArray(['HandheldFriendly,True', 'MobileOptimized,320', 'mobile-web-app-capable,yes', 'apple-mobile-web-app-capable,yes', 'apple-mobile-web-app-title,' + title, 'msapplication-TileImage,/' + img + '-144x144.png', 'msapplication-square70x70logo,/smalltile.png', 'msapplication-square150x150logo,/mediumtile.png', 'msapplication-wide310x150logo,/widetile.png', 'msapplication-square310x310logo,/largetile.png'].map(function (x) { 40 | return (0, _vdom2.m)('meta', { name: x.split(',')[0], content: x.split(',')[1] }); 41 | })), [ 42 | // ...([512,180,152,144,120,114,76,72].map(x => 43 | // m('link', {rel: 'apple-touch-icon-precomposed', sizes:`${x}x${x}`, href:`/${img}-${x}x${x}.png`}))), 44 | (0, _vdom2.m)('link', { rel: 'apple-touch-icon-precomposed', href: '/' + img + '-180x180.png' }), (0, _vdom2.m)('link', { rel: 'apple-touch-startup-image', href: '/' + img + '-startup.png' }), (0, _vdom2.m)('link', { rel: 'shortcut icon', href: '/' + img + '.ico', type: 'image/x-icon' }), (0, _vdom2.m)('link', { rel: 'manifest', href: '/' + manifest + '.json' })]); 45 | }; 46 | 47 | /** 48 | * Google Analytics 49 | */ 50 | var googleAnalytics = exports.googleAnalytics = function googleAnalytics(id) { 51 | var x = function x() { 52 | window.ga = window.ga || function () { 53 | window.ga.q = (window.ga.q || []).push(arguments); 54 | }; 55 | var ga = window.ga; 56 | ga('create', id, 'auto'); 57 | ga('send', 'pageview'); 58 | }; 59 | return (0, _vdom2.m)('script', { config: x, src: 'https://www.google-analytics.com/analytics.js', l: 1 * new Date(), async: 1 }); 60 | }; 61 | 62 | // Facebook: https://developers.facebook.com/docs/sharing/webmasters#markup 63 | // Open Graph: http://ogp.me/ 64 | 65 | var fb_opengraph = exports.fb_opengraph = function fb_opengraph(app_id, url, title, img, site_name, author) { 66 | return ['fb:app_id,' + app_id, 'og:url,' + url, 'og:type,website', 'og:title,' + title, 'og:image,' + img, 'og:description,' + description, 'og:site_name,' + site_name, 'og:locale,en_US', 'article:author,' + author].map(function (x, i, a) { 67 | var p = arguments.length <= 3 || arguments[3] === undefined ? x.split(',') : arguments[3]; 68 | return (0, _vdom2.m)('meta', { property: p[0], content: p[1] }); 69 | }); 70 | }; 71 | 72 | var fb_instantarticle = exports.fb_instantarticle = function fb_instantarticle(article_url, style) { 73 | return [(0, _vdom2.m)('meta', { property: "op:markup_version", content: "v1.0" }), (0, _vdom2.m)('link', { rel: "canonical", href: article_url }), (0, _vdom2.m)('meta', { property: "fb:article_style", content: style })]; 74 | }; 75 | 76 | // More info: https://dev.twitter.com/cards/getting-started 77 | // Validate: https://dev.twitter.com/docs/cards/validation/validator 78 | var twitter_card = exports.twitter_card = function twitter_card(summary, site_account, individual_account, url, title, description, image) { 79 | return ['twitter:card,' + summary, 'twitter:site,@' + site_account, 'twitter:creator,@' + individual_account, 'twitter:url,' + url, 'twitter:title,' + title, 'twitter:description,' + description, 'twitter:image,' + image].map(function (x, i, a) { 80 | var n = arguments.length <= 3 || arguments[3] === undefined ? x.split(',') : arguments[3]; 81 | return (0, _vdom2.m)('meta', { name: n[0], content: n[1] }); 82 | }); 83 | }; 84 | 85 | var google_plus = exports.google_plus = function google_plus(page, title, desc, img) { 86 | return [(0, _vdom2.m)('link', { href: 'https://plus.google.com/+' + page, rel: 'publisher' }), (0, _vdom2.m)('meta', { itemprop: "name", content: title }), (0, _vdom2.m)('meta', { itemprop: "description", content: desc }), (0, _vdom2.m)('meta', { itemprop: "image", content: img })]; 87 | }; 88 | 89 | // More info: https://developer.apple.com/safari/library/documentation/appleapplications/reference/safarihtmlref/articles/metatags.html 90 | var iOS = exports.iOS = function iOS(app_id, affiliate_id, app_arg) { 91 | var telephone = arguments.length <= 3 || arguments[3] === undefined ? 'yes' : arguments[3]; 92 | var title = arguments[4]; 93 | return [ 94 | // Smart App Banner 95 | 'apple-itunes-app,app-id=' + app_id + ',affiliate-data=' + affiliate_id + ',app-argument=' + app_arg, 96 | 97 | // Disable automatic detection and formatting of possible phone numbers --> 98 | 'format-detection,telephone=' + telephone, 99 | 100 | // Add to Home Screen 101 | 'apple-mobile-web-app-capable,yes', 'apple-mobile-web-app-status-bar-style,black', 'apple-mobile-web-app-title,' + title].map(function (x, i, a) { 102 | var n = arguments.length <= 3 || arguments[3] === undefined ? x.split(',') : arguments[3]; 103 | return (0, _vdom2.m)('meta', { name: n[0], content: n[1] }); 104 | }); 105 | }; 106 | 107 | // Pinned Site - Safari 108 | var safari = exports.safari = function safari() { 109 | var name = arguments.length <= 0 || arguments[0] === undefined ? 'icon' : arguments[0]; 110 | var color = arguments.length <= 1 || arguments[1] === undefined ? 'red' : arguments[1]; 111 | return (0, _vdom2.m)('link', { rel: "mask-icon", href: name + '.svg', color: color }); 112 | }; 113 | 114 | // Disable translation prompt 115 | var chrome = exports.chrome = function chrome(app_id) { 116 | return [(0, _vdom2.m)('link', { rel: "chrome-webstore-item", href: 'https://chrome.google.com/webstore/detail/' + app_id }), (0, _vdom2.m)('meta', { name: 'google', value: 'notranslate' })]; 117 | }; 118 | 119 | var applinks = exports.applinks = function applinks(app_store_id, name, android_pkg, docs_url) { 120 | return [ 121 | // iOS 122 | 'al:ios:url,applinks://docs', 'al:ios:app_store_id,' + app_store_id, 'al:ios:app_name,' + name, 123 | // Android 124 | 'al:android:url,applinks://docs', 'al:android:app_name,' + name, 'al:android:package,' + android_pkg, 125 | // Web Fallback 126 | 'al:web:url,' + docs_url].map(function (x, i, a) { 127 | var n = arguments.length <= 3 || arguments[3] === undefined ? x.split(',') : arguments[3]; 128 | return (0, _vdom2.m)('meta', { property: n[0], content: n[1] }); 129 | }); 130 | }; 131 | 132 | /** 133 | * monitors scrolling to indicate if an element is visible within the viewport 134 | */ 135 | 136 | /* 137 | likely include with your SCSS for a project, that makes these styles hide/show the element: 138 | .invisible { 139 | opacity: 0; 140 | transition-delay: .5s; 141 | transition-duration: .5s; 142 | &.visible { 143 | opacity: 1; 144 | } 145 | } 146 | */ 147 | 148 | var viewportHeight = exports.viewportHeight = function viewportHeight(_) { 149 | return Math.max(document.documentElement.clientHeight, window.innerHeight || 0); 150 | }; 151 | 152 | var trackVisibility = exports.trackVisibility = function trackVisibility(component) { 153 | var el = void 0, 154 | visible = el.getBoundingClientRect instanceof Function ? false : true; 155 | 156 | var onScroll = (0, _vdom2.debounce)(function (ev) { 157 | if (!el.getBoundingClientRect instanceof Function) return; 158 | 159 | var _el$getBoundingClient = el.getBoundingClientRect(); 160 | 161 | var top = _el$getBoundingClient.top; 162 | var height = _el$getBoundingClient.height; 163 | var vh = viewportHeight(); 164 | if (top <= vh && !visible) { 165 | el.className += ' visible'; 166 | visible = true; 167 | } else if (top > vh && visible) { 168 | el.className = el.className.replace(/ visible/g, ''); 169 | visible = false; 170 | } 171 | }, 16.6); 172 | 173 | var startScroll = function startScroll(_el) { 174 | el = _el; 175 | window.addEventListener('scroll', onScroll); 176 | }; 177 | 178 | var endScroll = function endScroll(_) { 179 | return window.removeEventListener('scroll', onScroll); 180 | }; 181 | 182 | (0, _vdom2.rAF)(onScroll); 183 | 184 | return (0, _vdom2.m)('div.invisible', { config: startScroll, unload: endScroll }, component); 185 | }; 186 | 187 | /** 188 | * MARKDEEP / MARKDOWN - convert a section with string content 189 | * into a markdeep/markdown rendered section 190 | */ 191 | 192 | var markdown = exports.markdown = function markdown(content) { 193 | var markdownToHtml = arguments.length <= 1 || arguments[1] === undefined ? function (c) { 194 | return global.markdeep.format(c); 195 | } : arguments[1]; 196 | 197 | var config = function config(element, init) { 198 | element.innerHTML = markdownToHtml(content); 199 | }; 200 | return (0, _vdom2.m)('.markdeep', { config: config }); 201 | }; 202 | 203 | /** 204 | * scrambled text animation 205 | * 206 | * m('span', {config: animatingTextConfig('test me out')}) 207 | */ 208 | var chars = exports.chars = '#*^-+=!f0123456789_'; 209 | var scramble = exports.scramble = function scramble(str) { 210 | var from = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1]; 211 | return str.slice(0, from) + str.slice(from).split('').map(function (x) { 212 | return x === ' ' ? x : chars[range(0, chars.length)]; 213 | }).join(''); 214 | }; 215 | var range = exports.range = function range(min, max) { 216 | return Math.floor(Math.random() * (max - min) + min); 217 | }; 218 | var wait = exports.wait = function wait(ms) { 219 | return new Promise(function (res) { 220 | return setTimeout(res, ms); 221 | }); 222 | }; 223 | 224 | var scrambler = exports.scrambler = function scrambler(str) { 225 | var interval = arguments.length <= 1 || arguments[1] === undefined ? 33 : arguments[1]; 226 | var i = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; 227 | var delay = arguments.length <= 3 || arguments[3] === undefined ? 0 : arguments[3]; 228 | return function (el) { 229 | var start = scramble(str, 0); 230 | var draw = function draw(i) { 231 | return function () { 232 | return el.innerText = str.slice(0, i) + start.slice(i); 233 | }; 234 | }; 235 | while (i++ < str.length) { 236 | wait(delay + i * interval).then(draw(i)); 237 | } 238 | }; 239 | }; 240 | 241 | /** 242 | * load an image in JS, and then animate it in as a background image 243 | * 244 | * imageLoader(url, m('div')) 245 | */ 246 | var imageLoader = exports.imageLoader = function imageLoader(url, comp) { 247 | var x = comp, 248 | image = void 0, 249 | loaded = false; 250 | 251 | while (x instanceof Function) { 252 | x = x(); 253 | }var imgConfig = function imgConfig(el) { 254 | image = new Image(); 255 | 256 | el.style.backgroundImage = 'url(' + url + ')'; 257 | 258 | var done = function done(ev) { 259 | if (loaded) return; 260 | el.className += ' loaded'; 261 | loaded = true; 262 | }; 263 | 264 | image.onload = done; 265 | image.src = url; 266 | }; 267 | 268 | x.config = imgConfig; 269 | return x; 270 | }; 271 | 272 | /** 273 | * load an SVG and inject it onto the page 274 | */ 275 | var loadSVG = exports.loadSVG = function loadSVG(url) { 276 | return fetch(url).then(function (r) { 277 | return r.text(); 278 | }); 279 | }; 280 | var injectSVG = exports.injectSVG = function injectSVG(url) { 281 | return (0, _vdom2.container)(function (data) { 282 | return (0, _vdom2.m)('div', { config: function config(el) { 283 | return el.innerHTML = data.svg; 284 | } }); 285 | }, { svg: loadSVG.bind(null, url) }); 286 | }; 287 | 288 | /** 289 | * hashroute-driven router 290 | */ 291 | // router implementation 292 | var hashrouter = exports.hashrouter = function hashrouter() { 293 | var routes = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 294 | var def = arguments.length <= 1 || arguments[1] === undefined ? '#' : arguments[1]; 295 | var current = arguments[2]; 296 | 297 | var x = (0, _routerAlt.router)(routes, function (_vdom) { 298 | current = _vdom; 299 | (0, _vdom2.update)(); 300 | }); 301 | x.listen(); 302 | x.trigger((window.location.hash || def).slice(1)); 303 | return function () { 304 | return current; 305 | }; 306 | }; 307 | 308 | /** 309 | * page component that returns an entire html component 310 | */ 311 | var page = exports.page = function page(main, title) { 312 | var css = arguments.length <= 2 || arguments[2] === undefined ? '/style.css' : arguments[2]; 313 | var googleAnalyticsId = arguments[3]; 314 | return [head(theme(), mobile_metas(title), (0, _vdom2.m)('link', { type: 'text/css', rel: 'stylesheet', href: css }), googleAnalyticsId && googleAnalytics(googleAnalyticsId)), (0, _vdom2.m)('body', main)]; 315 | }; 316 | 317 | /** 318 | * mount the entire page() component to the DOM 319 | */ 320 | var app = exports.app = function app(routes, def, title, css, analyticsId) { 321 | var router = hashrouter(routes, def); 322 | var p = page(router, title, css, analyticsId); 323 | return function () { 324 | return (0, _vdom2.mount)(p, (0, _vdom2.qs)('html', document)); 325 | }; 326 | }; -------------------------------------------------------------------------------- /dist/csp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 8 | 9 | function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } 10 | 11 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 12 | 13 | /** 14 | * Welcome to CSP in JS! 15 | * 16 | * This is an implementation of Go-style coroutines that access a hidden, 17 | * shared channel for putting data into, and taking it out of, a system. 18 | * 19 | * Channels, in this case, can be a set (for unique values), an array 20 | * (as a stack or a queue), or even some kind of persistent data structure. 21 | * 22 | * CSP (especially in functional platforms like ClojureScript, where the 23 | * `core.async` library provides asynchronous, immutable data-structures) 24 | * typically operates through two operations (overly simplified here): 25 | * 26 | * (1) put(...a) : put a list of items into the channel 27 | * (2) take(x) : take x items from the channel 28 | * 29 | * This implementation uses ES6 generators (and other ES6 features), which are basically functions that 30 | * can return more than one value, and pause after each value yielded. 31 | * 32 | * 33 | */ 34 | 35 | var raf = function raf(cb) { 36 | return requestAnimationFrame ? requestAnimationFrame(cb) : setTimeout(cb, 0); 37 | }; 38 | 39 | var channel = exports.channel = function channel() { 40 | 41 | var c = [], 42 | // channel data is a queue; first in, first out 43 | channel_closed = false, 44 | // is channel closed? 45 | runners = []; // list of iterators to run through 46 | 47 | var not = function not(c, b) { 48 | return c.filter(function (a) { 49 | return a !== b; 50 | }); 51 | }, 52 | // filter for "not b" 53 | each = function each(c, fn) { 54 | return c.forEach(fn); 55 | }; // forEach... 56 | 57 | var put = function put() { 58 | for (var _len = arguments.length, vals = Array(_len), _key = 0; _key < _len; _key++) { 59 | vals[_key] = arguments[_key]; 60 | } 61 | 62 | // put(1,2,3) 63 | c = [].concat(vals, _toConsumableArray(c)); // inserts vals to the front of c 64 | return ["park"].concat(_toConsumableArray(c)); // park this iterator with this data 65 | }, 66 | take = function take() { 67 | var x = arguments.length <= 0 || arguments[0] === undefined ? 1 : arguments[0]; 68 | var taker = arguments.length <= 1 || arguments[1] === undefined ? function () { 69 | for (var _len2 = arguments.length, vals = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 70 | vals[_key2] = arguments[_key2]; 71 | } 72 | 73 | return vals; 74 | } : arguments[1]; 75 | // take(numItems, mapper) 76 | c = taker.apply(undefined, _toConsumableArray(c)); // map/filter for certain values 77 | var diff = c.length - x; // get the last x items 78 | if (diff < 0) return ['park']; 79 | var vals = c.slice(diff).reverse(); // last x items 80 | c = c.slice(0, diff); // remove those x items from channel 81 | return [vals.length !== 0 ? 'continue' : 'park'].concat(_toConsumableArray(vals)); // pipe dat aout from channel 82 | }, 83 | awake = function awake(run) { 84 | return each(not(runners, run), function (a) { 85 | return a(); 86 | }); 87 | }, 88 | // awake other runners 89 | status = function status(next, run) { 90 | // iterator status, runner => run others if not done 91 | var done = next.done; 92 | var value = next.value; 93 | 94 | if (done) runners = not(runners, run); // if iterator done, filter it out 95 | return value || ['park']; // return value (i.e. [state, ...nums]) or default ['park'] 96 | }, 97 | actor = function actor(iter) { 98 | // actor returns a runner that next()'s the iterator 99 | var prev = []; 100 | 101 | var runner = function runner() { 102 | if (channel_closed) return runners = []; // channel closed? delete runners 103 | 104 | var _status = status(iter.next(prev), runner); 105 | 106 | var _status2 = _toArray(_status); 107 | 108 | var state = _status2[0]; 109 | 110 | var vals = _status2.slice(1); // pass values to iterator, iter.next(), and store those new vals from status() 111 | 112 | prev = vals; // store new vals 113 | // raf 114 | (state === 'continue' ? runner : cb)(); // if continue, keep running, else awaken all others except runner 115 | }; 116 | 117 | var cb = awake.bind(null, runner); // awake all runners except runner 118 | 119 | return runner; 120 | }, 121 | spawn = function spawn(gen) { 122 | var runner = actor(gen(put, take)); 123 | runners = [].concat(_toConsumableArray(runners), [runner]); 124 | runner(); 125 | }; 126 | 127 | return { 128 | spawn: spawn, 129 | close: function close() { 130 | channel_closed = true; 131 | } 132 | }; 133 | }; 134 | 135 | /** 136 | API 137 | 138 | channel() 139 | channel.spawn(*function(put, take){...}) -- takes a generator that receives a put and take function 140 | channel.close() -- closes the channel, stops all operations and reclaims memory (one line cleanup!!) 141 | **/ 142 | 143 | /* 144 | let x = channel() // create new channel() 145 | 146 | // for any value in the channel, pull it and log it 147 | x.spawn( function* (put, take) { 148 | while(true){ 149 | let [status, ...vals] = yield take(1, (...vals) => 150 | vals.filter(x => 151 | typeof x === 'number' && x%2===0)) 152 | // if not 10 items available, actor parks, waiting to be signalled again, and also find just evens 153 | 154 | if(vals.length === 1) log(`-------------------taking: ${vals}`) 155 | } 156 | }) 157 | 158 | // put each item in fibonnaci sequence, one at a time 159 | x.spawn( function* (put, take) { 160 | let [x, y] = [0, 1], 161 | next = x+y 162 | 163 | for(var i = 0; i < 30; i++) { 164 | next = x+y 165 | log(`putting: ${next}`) 166 | yield put(next) 167 | x = y 168 | y = next 169 | } 170 | }) 171 | 172 | // immediately, and every .5 seconds, put the date/time into channel 173 | x.spawn(function* insertDate(p, t) { 174 | while(true){ 175 | yield p(new Date) 176 | } 177 | }) 178 | 179 | // close the channel and remove all memory references. Pow! one-line cleanup. 180 | setTimeout(() => x.close(), 2500) 181 | */ 182 | 183 | var fromEvent = exports.fromEvent = function fromEvent(obj, events) { 184 | var c = arguments.length <= 2 || arguments[2] === undefined ? channel() : arguments[2]; 185 | var fn = arguments.length <= 3 || arguments[3] === undefined ? function (e) { 186 | return e; 187 | } : arguments[3]; 188 | 189 | if (!obj.addEventListener) return; 190 | if (!(typeof events === 'string') || !events.length) return; 191 | events = events.split(',').map(function (x) { 192 | return x.trim(); 193 | }).forEach(function (x) { 194 | obj.addEventListener(x, function (e) { 195 | c.spawn(regeneratorRuntime.mark(function _callee(put, take) { 196 | return regeneratorRuntime.wrap(function _callee$(_context) { 197 | while (1) { 198 | switch (_context.prev = _context.next) { 199 | case 0: 200 | _context.next = 2; 201 | return put(fn(e)); 202 | 203 | case 2: 204 | case 'end': 205 | return _context.stop(); 206 | } 207 | } 208 | }, _callee, this); 209 | })); 210 | }); 211 | }); 212 | return c; 213 | }; 214 | 215 | /* 216 | let c1 = fromEvent(document.body, 'mousemove') 217 | c1.spawn(function* (p,t){ 218 | while(true) log(yield t(1)) 219 | }) 220 | */ 221 | 222 | var conj = exports.conj = function conj() { 223 | for (var _len3 = arguments.length, channels = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 224 | channels[_key3] = arguments[_key3]; 225 | } 226 | 227 | var x = channel(), 228 | send = function send() { 229 | for (var _len4 = arguments.length, vals = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 230 | vals[_key4] = arguments[_key4]; 231 | } 232 | 233 | return x.spawn(regeneratorRuntime.mark(function _callee2(p, t) { 234 | return regeneratorRuntime.wrap(function _callee2$(_context2) { 235 | while (1) { 236 | switch (_context2.prev = _context2.next) { 237 | case 0: 238 | p.apply(undefined, vals); 239 | case 1: 240 | case 'end': 241 | return _context2.stop(); 242 | } 243 | } 244 | }, _callee2, this); 245 | })); 246 | }; 247 | 248 | channels.forEach(function (y) { 249 | return y.spawn(regeneratorRuntime.mark(function _callee3(p, t) { 250 | var _t, _t2, status, val; 251 | 252 | return regeneratorRuntime.wrap(function _callee3$(_context3) { 253 | while (1) { 254 | switch (_context3.prev = _context3.next) { 255 | case 0: 256 | if (!true) { 257 | _context3.next = 9; 258 | break; 259 | } 260 | 261 | _t = t(); 262 | _t2 = _slicedToArray(_t, 2); 263 | status = _t2[0]; 264 | val = _t2[1]; 265 | _context3.next = 7; 266 | return val && send(val); 267 | 268 | case 7: 269 | _context3.next = 0; 270 | break; 271 | 272 | case 9: 273 | case 'end': 274 | return _context3.stop(); 275 | } 276 | } 277 | }, _callee3, this); 278 | })); 279 | }); 280 | 281 | return x; 282 | }; -------------------------------------------------------------------------------- /dist/demux.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__esModule = true; 4 | 5 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 6 | 7 | // import fetch, {batch} from './fetch' 8 | 9 | var _store = require('./store'); 10 | 11 | var _store2 = _interopRequireDefault(_store); 12 | 13 | require('isomorphic-fetch'); 14 | var iso_fetch = global.fetch; 15 | 16 | var debounce = function debounce(func, wait, immediate, timeout, p) { 17 | return function () { 18 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 19 | args[_key] = arguments[_key]; 20 | } 21 | 22 | return p || new Promise(function (res) { 23 | var later = function later() { 24 | timeout = null; 25 | p = null; 26 | if (!immediate) res(func.apply(undefined, args)); 27 | }, 28 | callNow = immediate && !timeout; 29 | 30 | clearTimeout(timeout); 31 | timeout = setTimeout(later, wait); 32 | if (callNow) res(func.apply(undefined, args)); 33 | }); 34 | }; 35 | }; 36 | 37 | /** 38 | * Muxed/Demuxed requests will involve pipelined, serialized request objects sent along together in an array. 39 | * 40 | * i.e. [ 41 | * {url: '...', {headers:{...}, form:{...}}}, 42 | * {url: '...', {headers:{...}, form:{...}}}, 43 | * {url: '...', {headers:{...}, form:{...}}}, 44 | * ... 45 | * ] 46 | */ 47 | 48 | var muxer = function muxer(batch_url) { 49 | var timeout = arguments.length <= 1 || arguments[1] === undefined ? 200 : arguments[1]; 50 | var f = arguments.length <= 2 || arguments[2] === undefined ? iso_fetch : arguments[2]; 51 | 52 | var payload = _store2['default']([]); 53 | 54 | // puts url,options,id on payload 55 | var worker = function worker(url, options) { 56 | return payload.dispatch(function (state, next) { 57 | return next(state.concat({ url: url, options: options })).then(function (state) { 58 | return state.length - 1; 59 | }); 60 | }); 61 | }; 62 | 63 | // sends payload after 200ms 64 | var send = debounce(function () { 65 | return f(batch_url, { method: 'POST', body: JSON.stringify(payload.state()) }).then(function (data) { 66 | payload.state([]); // reset payload for next batch of requests 67 | return data; // ordered array of requests 68 | }); 69 | }, timeout); 70 | 71 | return function (url) { 72 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 73 | return( 74 | // add {url,options} to payload 75 | // resolves to data[index] under assumption the endpoint returns 76 | // data in order it was requested 77 | worker(url, options).then(function (index) { 78 | return send().then(function (data) { 79 | return data[index]; 80 | }); 81 | }) 82 | ); 83 | }; 84 | }; 85 | 86 | exports['default'] = muxer; 87 | 88 | // example 89 | // const logf = (...args) => log(args) || fetch(...args) 90 | // const uberfetch = muxer('/api/mux', 200, logf) 91 | // uberfetch('/cows') 92 | // uberfetch('/kittens') 93 | // --- 94 | // logged --> [ 95 | // "/api/mux", 96 | // {"method":"POST","body":"[ 97 | // {url:'/cows', options:{}}, {url:'/kittens',options:{}} 98 | // ] 99 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/fetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 10 | 11 | /* 12 | The `fetch()` module batches in-flight requests, so if at any point in time, anywhere in my front-end or back-end application I have a calls occur to `fetch('http://api.github.com/users/matthiasak')` while another to that URL is "in-flight", the Promise returned by both of those calls will be resolved by a single network request. 13 | */ 14 | 15 | /** 16 | * batches in-flight requests into the same request object 17 | * 18 | * f should be a function with this signature: 19 | * 20 | * f: function(url,options): Promise 21 | */ 22 | var batch = exports.batch = function batch(f) { 23 | var inflight = {}; 24 | 25 | return function (url) { 26 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 27 | var method = options.method; 28 | var key = url + ':' + JSON.stringify(options); 29 | 30 | if ((method || '').toLowerCase() === 'post') return f(url, _extends({}, options, { compress: false })); 31 | 32 | return inflight[key] || (inflight[key] = new Promise(function (res, rej) { 33 | f(url, _extends({}, options, { compress: false })).then(function (d) { 34 | return res(d); 35 | }).catch(function (e) { 36 | return rej(e); 37 | }); 38 | }).then(function (data) { 39 | inflight = _extends({}, inflight, _defineProperty({}, key, undefined)); 40 | return data; 41 | }).catch(function (e) { 42 | return console.error(e, url); 43 | })); 44 | }; 45 | }; 46 | 47 | // a simple wrapper around fetch() 48 | // that enables a Promise to be cancelled (sort of) 49 | // -- 50 | // use this until Promise#abort() is a method, or the WHATWG figures 51 | // out a proper approach/implementation 52 | require('isomorphic-fetch'); 53 | var cancellable = exports.cancellable = function cancellable(f) { 54 | return function () { 55 | var result = f.apply(undefined, arguments), 56 | aborted = false; 57 | 58 | var promise = new Promise(function (res, rej) { 59 | result.then(function (d) { 60 | return aborted ? rej('aborted') : res(d); 61 | }).catch(function (e) { 62 | return rej(e); 63 | }); 64 | }); 65 | 66 | promise.abort = function () { 67 | return aborted = true; 68 | }; 69 | 70 | return promise; 71 | }; 72 | }; 73 | 74 | var whatWGFetch = exports.whatWGFetch = function whatWGFetch() { 75 | var _global; 76 | 77 | return (_global = global).fetch.apply(_global, arguments).then(function (r) { 78 | return r.json(); 79 | }); 80 | }; 81 | 82 | var cacheable = exports.cacheable = function cacheable() { 83 | var fetch = arguments.length <= 0 || arguments[0] === undefined ? whatWGFetch : arguments[0]; 84 | var duration = arguments.length <= 1 || arguments[1] === undefined ? 1000 * 60 * 5 : arguments[1]; 85 | 86 | var cache = {}; 87 | return function (url, opts) { 88 | var c = cache[url]; 89 | if (c && c.timestamp + duration >= +new Date()) return Promise.resolve(c.result); 90 | 91 | return fetch(url, opts).then(function (result) { 92 | cache[url] = { result: result, timestamp: +new Date() }; 93 | return result; 94 | }); 95 | }; 96 | }; 97 | 98 | var fetch = exports.fetch = cancellable(batch(whatWGFetch)); 99 | 100 | // !! usage 101 | // let batching_fetcher = batch(fetch) // fetch API from require('isomorphic-fetch') 102 | // 103 | // !! fetch has the signature of --> function(url:string, options:{}): Promise --> which matches the spec 104 | // !! wrapper functions for database drivers or even $.ajax could even be written to use those instead of 105 | // !! the native fetch() 106 | // 107 | // let url = 'http://api.github.com/user/matthiasak', 108 | // log(data => console.log(data)) 109 | // 110 | // !! the following only sends one network request, because the first request 111 | // !! shares the same URL and would not yet have finished 112 | // 113 | // batching_fetcher(url).then(log) //--> {Object} 114 | // batching_fetcher(url).then(log) //--> {Object} 115 | // 116 | // !! we can pass any number of options to a batched function, that does anything, 117 | // !! as long as it returns a promise 118 | // 119 | // !! by default, POSTs are not batched, whereas GETs are. Clone the repo and modify to your needs. -------------------------------------------------------------------------------- /dist/fp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 10 | 11 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 12 | 13 | var clone = exports.clone = function clone(obj) { 14 | return JSON.parse(JSON.stringify(obj)); 15 | }; 16 | 17 | var eq = exports.eq = function eq(a, b) { 18 | if (a === undefined || b === undefined) return false; 19 | return JSON.stringify(a) === JSON.stringify(b); 20 | }; 21 | 22 | var each = exports.each = function each(arr, fn) { 23 | for (var i = 0, len = arr.length; i < len; i++) { 24 | fn(arr[i], i, arr); 25 | } 26 | }; 27 | 28 | var map = exports.map = function map(arr, fn) { 29 | var result = []; 30 | each(arr, function () { 31 | result = result.concat(fn.apply(undefined, arguments)); 32 | }); 33 | return result; 34 | }; 35 | 36 | var reduce = exports.reduce = function reduce(arr, fn, acc) { 37 | arr = clone(arr); 38 | acc = acc !== undefined ? acc : arr.shift(); 39 | each(arr, function (v, i, arr) { 40 | acc = fn(acc, v, i, arr); 41 | }); 42 | return acc; 43 | }; 44 | 45 | var filter = exports.filter = function filter(arr, fn) { 46 | return reduce(arr, function (acc, v, i, arr) { 47 | return fn(v, i, arr) ? [].concat(_toConsumableArray(acc), [v]) : acc; 48 | }, []); 49 | }; 50 | 51 | var where = exports.where = function where(arr, fn) { 52 | return filter(arr, fn)[0] || null; 53 | }; 54 | 55 | var pluck = exports.pluck = function pluck() { 56 | var keys = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; 57 | var obj = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 58 | return reduce(filter(Object.keys(obj), function (v) { 59 | return keys.indexOf(v) !== -1 && !!obj[v]; 60 | }), function (a, v) { 61 | return _extends({}, a, _defineProperty({}, v, obj[v])); 62 | }, {}); 63 | }; 64 | 65 | var debounce = exports.debounce = function debounce(func, wait) { 66 | var timeout = null, 67 | calls = 0; 68 | return function () { 69 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 70 | args[_key] = arguments[_key]; 71 | } 72 | 73 | clearTimeout(timeout); 74 | timeout = setTimeout(function () { 75 | timeout = null; 76 | func.apply(undefined, args); 77 | }, wait); 78 | }; 79 | }; 80 | 81 | var concat = exports.concat = function concat(arr, v) { 82 | return arr.concat([v]); 83 | }; 84 | 85 | var concatAll = exports.concatAll = function concatAll(arr) { 86 | return reduce(arr, function (acc, v, i, arr) { 87 | return acc.concat(v); 88 | }, []); 89 | }; 90 | 91 | /** 92 | * Function composition 93 | * @param ...fs functions to compose 94 | * @return composed function 95 | **/ 96 | var compose = exports.compose = function compose() { 97 | for (var _len2 = arguments.length, fs = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 98 | fs[_key2] = arguments[_key2]; 99 | } 100 | 101 | return function () { 102 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 103 | args[_key3] = arguments[_key3]; 104 | } 105 | 106 | return fs.reduce(function (g, f) { 107 | return [f.apply(undefined, _toConsumableArray(g))]; 108 | }, args)[0]; 109 | }; 110 | }; 111 | 112 | /** example */ 113 | /* 114 | const ident = x => x, 115 | inc = x => x+1, 116 | dec = x => x-1 117 | 118 | const same = comp(inc, dec, ident) 119 | log(same(1,2,3,4,5)) 120 | */ 121 | 122 | var mapping = exports.mapping = function mapping(mapper) { 123 | return (// mapper: x -> y 124 | function (reducer) { 125 | return (// reducer: (state, value) -> new state 126 | function (result, value) { 127 | return reducer(result, mapper(value)); 128 | } 129 | ); 130 | } 131 | ); 132 | }; 133 | 134 | var filtering = exports.filtering = function filtering(predicate) { 135 | return (// predicate: x -> true/false 136 | function (reducer) { 137 | return (// reducer: (state, value) -> new state 138 | function (result, value) { 139 | return predicate(value) ? reducer(result, value) : result; 140 | } 141 | ); 142 | } 143 | ); 144 | }; 145 | 146 | var concatter = exports.concatter = function concatter(thing, value) { 147 | return thing.concat([value]); 148 | }; 149 | 150 | // example transducer usage: 151 | // const inc = x => x+1 152 | // const greaterThanTwo = x => x>2 153 | // const incGreaterThanTwo = compose( 154 | // mapping(inc), 155 | // filtering(greaterThanTwo) 156 | // ) 157 | // reduce([1,2,3,4], incGreaterThanTwo(concat), []) // => [3,4,5] -------------------------------------------------------------------------------- /dist/hamt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 8 | 9 | // simple fn that returns a map node 10 | var node = function node() { 11 | var val = arguments.length <= 0 || arguments[0] === undefined ? undefined : arguments[0]; 12 | 13 | var result = {}; 14 | if (val) result.val = val; 15 | return result; 16 | }; 17 | 18 | // compute the hamming weight 19 | var hamweight = function hamweight(x) { 20 | x -= x >> 1 & 0x55555555; 21 | x = (x & 0x33333333) + (x >> 2 & 0x33333333); 22 | x = x + (x >> 4) & 0x0f0f0f0f; 23 | x += x >> 8; 24 | x += x >> 16; 25 | return x & 0x7f; 26 | }; 27 | 28 | // hash fn 29 | var hash = exports.hash = function hash(str) { 30 | if (typeof str !== 'string') str = JSON.stringify(str); 31 | var type = typeof str === 'undefined' ? 'undefined' : _typeof(str); 32 | if (type === 'number') return str; 33 | if (type !== 'string') str += ''; 34 | 35 | var hash = 0; 36 | for (var i = 0, len = str.length; i < len; ++i) { 37 | var c = str.charCodeAt(i); 38 | hash = (hash << 5) - hash + c | 0; 39 | } 40 | return hash; 41 | }; 42 | 43 | // compare two hashes 44 | var comp = function comp(a, b) { 45 | return hash(a) === hash(b); 46 | }; 47 | 48 | // get a sub bit vector 49 | var frag = function frag() { 50 | var h = arguments.length <= 0 || arguments[0] === undefined ? 0 : arguments[0]; 51 | var i = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1]; 52 | var range = arguments.length <= 2 || arguments[2] === undefined ? 8 : arguments[2]; 53 | return h >>> range * i & (1 << range) - 1; 54 | }; 55 | 56 | // const toBitmap = x => 1 << x 57 | // const fromBitmap = (bitmap, bit) => popcount(bitmap & (bit - 1)) 58 | var bin = function bin(x) { 59 | return x.toString(2); 60 | }; 61 | 62 | // clone a node 63 | var replicate = exports.replicate = function replicate(o, h) { 64 | var n = node(); 65 | for (var x = 0, _o = o, _n = n; x < 4; x++) { 66 | for (var i in _o) { 67 | if (i !== 'val' && _o.hasOwnProperty(i)) { 68 | _n[i] = _o[i]; // point n[i] to o[i] 69 | } 70 | } 71 | 72 | var __n = node(), 73 | f = frag(h, x); 74 | 75 | _n[f] = __n; 76 | _n = __n; 77 | _o = _o[f] === undefined ? {} : _o[f]; 78 | } 79 | return n; 80 | }; 81 | 82 | var set = exports.set = function set(m, key, val) { 83 | var json = JSON.stringify(val), 84 | h = hash(key), 85 | n = get(m, key); 86 | 87 | if (n === undefined || !comp(n, val)) { 88 | // in deepest level (3), need to create path down to make this change 89 | var r = replicate(m, h); // new subtree 90 | for (var i = 0, _r = r; i < 4; i++) { 91 | _r = _r[frag(h, i)]; 92 | }_r.val = val; 93 | return r; 94 | } 95 | 96 | // else the hash came out to be the same, do nothing 97 | return m; 98 | }; 99 | 100 | var unset = exports.unset = function unset(m, key) { 101 | var h = hash(key), 102 | r = replicate(m, h); // new subtree 103 | for (var i = 0, _r = r; i < 3; i++) { 104 | _r = _r[frag(h, i)]; 105 | }_r[frag(h, 3)] = undefined; 106 | return r; 107 | }; 108 | 109 | var get = exports.get = function get(m, key) { 110 | var h = hash(key); 111 | for (var i = 0, _r = m; i < 4; i++) { 112 | _r = _r[frag(h, i)]; 113 | if (!_r) return undefined; 114 | } 115 | return _r.val; 116 | }; 117 | 118 | var hashmap = exports.hashmap = function hashmap() { 119 | var initial = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 120 | 121 | var result = node(); 122 | for (var i in initial) { 123 | if (initial.hasOwnProperty(i)) result = set(result, i, initial[i]); 124 | } 125 | return result; 126 | }; 127 | 128 | var list = exports.list = function list() { 129 | var initial = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; 130 | 131 | var result = node(); 132 | for (var i = 0, len = initial.length; i < len; i++) { 133 | result = set(result, i, initial[i]); 134 | } 135 | return result; 136 | }; 137 | 138 | var iter = function iter(hashmap) { 139 | var items = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; 140 | 141 | for (var i in hashmap) { 142 | if (hashmap.hasOwnProperty(i)) { 143 | if (i !== 'val') { 144 | iter(hashmap[i], items); 145 | } else { 146 | items.push(hashmap[i]); 147 | } 148 | } 149 | } 150 | return items; 151 | }; 152 | 153 | var identity = function identity(x) { 154 | return x; 155 | }; 156 | 157 | var map = exports.map = function map(hashmap) { 158 | var fn = arguments.length <= 1 || arguments[1] === undefined ? identity : arguments[1]; 159 | var it = arguments.length <= 2 || arguments[2] === undefined ? iter : arguments[2]; 160 | 161 | var items = it(hashmap), 162 | result = []; 163 | for (var i = 0, len = items.length; i < len; i++) { 164 | result.push(fn(items[i])); 165 | } 166 | return result; 167 | }; 168 | 169 | var inOrder = exports.inOrder = function inOrder(hashmap) { 170 | var list = [], 171 | i = 0, 172 | v = void 0; 173 | 174 | while ((v = get(hashmap, i++)) !== undefined) { 175 | list.push(v); 176 | } 177 | 178 | return list; 179 | }; 180 | 181 | var reduce = exports.reduce = function reduce(hashmap, fn, acc) { 182 | var it = arguments.length <= 3 || arguments[3] === undefined ? iter : arguments[3]; 183 | 184 | var items = it(hashmap); 185 | acc = acc === undefined ? items.shift() : acc; 186 | for (var i = 0, len = items.length; i < len; i++) { 187 | acc = fn(acc, items[i]); 188 | } 189 | return acc; 190 | }; 191 | /* 192 | Usage: 193 | 194 | let x = hashmap({'hello':1}) 195 | , y = set(x, 'goodbye', 2) 196 | , z = list(Array(30).fill(true).map((x,i) => i+1)) 197 | , a = hashmap(Array(30).fill(true).reduce((a,x,i) => { 198 | a[i] = i+1 199 | return a 200 | }, {})) 201 | 202 | log( 203 | get(x, 'hello')+'', 204 | x, 205 | unset(x, 'hello'), 206 | get(x, 'goodbye')+'', 207 | get(y, 'hello')+'', 208 | get(y, 'goodbye')+'', 209 | x===y, 210 | comp(a,z), 211 | // map(a, x=>x+1), 212 | // map(z, x=>x+1), 213 | reduce(a, (acc,x)=>acc+x, 0), 214 | reduce(a, (acc,x)=>acc+x, 0) 215 | ) 216 | */ -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.meta = exports.model = exports.hamt = exports.lazy = exports.ot = exports.fp = exports.components = exports.vdom = exports.csp = exports.mux = exports.router = exports.cache = exports.resource = exports.store = exports.fetch = undefined; 7 | 8 | var _fetch2 = require('./fetch'); 9 | 10 | var _fetch = _interopRequireWildcard(_fetch2); 11 | 12 | var _store2 = require('./store'); 13 | 14 | var _store = _interopRequireWildcard(_store2); 15 | 16 | var _resource2 = require('./resource'); 17 | 18 | var _resource = _interopRequireWildcard(_resource2); 19 | 20 | var _cache2 = require('./cache'); 21 | 22 | var _cache = _interopRequireWildcard(_cache2); 23 | 24 | var _routerAlt = require('./router-alt'); 25 | 26 | var _router = _interopRequireWildcard(_routerAlt); 27 | 28 | var _mux2 = require('./mux'); 29 | 30 | var _mux = _interopRequireWildcard(_mux2); 31 | 32 | var _csp2 = require('./csp'); 33 | 34 | var _csp = _interopRequireWildcard(_csp2); 35 | 36 | var _vdom2 = require('./vdom'); 37 | 38 | var _vdom = _interopRequireWildcard(_vdom2); 39 | 40 | var _components2 = require('./components'); 41 | 42 | var _components = _interopRequireWildcard(_components2); 43 | 44 | var _fp2 = require('./fp'); 45 | 46 | var _fp = _interopRequireWildcard(_fp2); 47 | 48 | var _ot2 = require('./ot'); 49 | 50 | var _ot = _interopRequireWildcard(_ot2); 51 | 52 | var _lazy2 = require('./lazy'); 53 | 54 | var _lazy = _interopRequireWildcard(_lazy2); 55 | 56 | var _hamt2 = require('./hamt'); 57 | 58 | var _hamt = _interopRequireWildcard(_hamt2); 59 | 60 | var _model2 = require('./model'); 61 | 62 | var _model = _interopRequireWildcard(_model2); 63 | 64 | var _meta2 = require('./meta'); 65 | 66 | var _meta = _interopRequireWildcard(_meta2); 67 | 68 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 69 | 70 | exports.fetch = _fetch; 71 | exports.store = _store; 72 | exports.resource = _resource; 73 | exports.cache = _cache; 74 | exports.router = _router; 75 | exports.mux = _mux; 76 | exports.csp = _csp; 77 | exports.vdom = _vdom; 78 | exports.components = _components; 79 | exports.fp = _fp; 80 | exports.ot = _ot; 81 | exports.lazy = _lazy; 82 | exports.hamt = _hamt; 83 | exports.model = _model; 84 | exports.meta = _meta; -------------------------------------------------------------------------------- /dist/lazy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 8 | 9 | var flatten = function flatten() { 10 | for (var _len = arguments.length, a = Array(_len), _key = 0; _key < _len; _key++) { 11 | a[_key] = arguments[_key]; 12 | } 13 | 14 | return a.reduce(function (a, v) { 15 | if (v instanceof Array) return [].concat(_toConsumableArray(a), _toConsumableArray(flatten.apply(undefined, _toConsumableArray(v)))); 16 | return a.concat(v); 17 | }, []); 18 | }; 19 | 20 | var iter = exports.iter = function iter() { 21 | for (var _len2 = arguments.length, a = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 22 | a[_key2] = arguments[_key2]; 23 | } 24 | 25 | return wrap(regeneratorRuntime.mark(function _callee() { 26 | var b, i, len; 27 | return regeneratorRuntime.wrap(function _callee$(_context) { 28 | while (1) { 29 | switch (_context.prev = _context.next) { 30 | case 0: 31 | b = flatten(a); 32 | i = 0, len = b.length; 33 | 34 | case 2: 35 | if (!(i < len)) { 36 | _context.next = 8; 37 | break; 38 | } 39 | 40 | _context.next = 5; 41 | return b[i]; 42 | 43 | case 5: 44 | i++; 45 | _context.next = 2; 46 | break; 47 | 48 | case 8: 49 | case 'end': 50 | return _context.stop(); 51 | } 52 | } 53 | }, _callee, this); 54 | })); 55 | }; 56 | 57 | var seq = exports.seq = function seq() { 58 | var start = arguments.length <= 0 || arguments[0] === undefined ? 0 : arguments[0]; 59 | var step = arguments.length <= 1 || arguments[1] === undefined ? 1 : arguments[1]; 60 | var end = arguments[2]; 61 | return wrap(regeneratorRuntime.mark(function _callee2() { 62 | var i; 63 | return regeneratorRuntime.wrap(function _callee2$(_context2) { 64 | while (1) { 65 | switch (_context2.prev = _context2.next) { 66 | case 0: 67 | i = start; 68 | 69 | case 1: 70 | if (!(end === undefined || i <= end)) { 71 | _context2.next = 7; 72 | break; 73 | } 74 | 75 | _context2.next = 4; 76 | return i; 77 | 78 | case 4: 79 | i += step; 80 | _context2.next = 1; 81 | break; 82 | 83 | case 7: 84 | case 'end': 85 | return _context2.stop(); 86 | } 87 | } 88 | }, _callee2, this); 89 | })); 90 | }; 91 | 92 | var lmap = exports.lmap = function lmap(gen, fn) { 93 | return wrap(regeneratorRuntime.mark(function _callee3() { 94 | var _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, x; 95 | 96 | return regeneratorRuntime.wrap(function _callee3$(_context3) { 97 | while (1) { 98 | switch (_context3.prev = _context3.next) { 99 | case 0: 100 | _iteratorNormalCompletion = true; 101 | _didIteratorError = false; 102 | _iteratorError = undefined; 103 | _context3.prev = 3; 104 | _iterator = gen()[Symbol.iterator](); 105 | 106 | case 5: 107 | if (_iteratorNormalCompletion = (_step = _iterator.next()).done) { 108 | _context3.next = 12; 109 | break; 110 | } 111 | 112 | x = _step.value; 113 | _context3.next = 9; 114 | return fn(x); 115 | 116 | case 9: 117 | _iteratorNormalCompletion = true; 118 | _context3.next = 5; 119 | break; 120 | 121 | case 12: 122 | _context3.next = 18; 123 | break; 124 | 125 | case 14: 126 | _context3.prev = 14; 127 | _context3.t0 = _context3['catch'](3); 128 | _didIteratorError = true; 129 | _iteratorError = _context3.t0; 130 | 131 | case 18: 132 | _context3.prev = 18; 133 | _context3.prev = 19; 134 | 135 | if (!_iteratorNormalCompletion && _iterator.return) { 136 | _iterator.return(); 137 | } 138 | 139 | case 21: 140 | _context3.prev = 21; 141 | 142 | if (!_didIteratorError) { 143 | _context3.next = 24; 144 | break; 145 | } 146 | 147 | throw _iteratorError; 148 | 149 | case 24: 150 | return _context3.finish(21); 151 | 152 | case 25: 153 | return _context3.finish(18); 154 | 155 | case 26: 156 | case 'end': 157 | return _context3.stop(); 158 | } 159 | } 160 | }, _callee3, this, [[3, 14, 18, 26], [19,, 21, 25]]); 161 | })); 162 | }; 163 | 164 | var lfilter = exports.lfilter = function lfilter(gen, fn) { 165 | return wrap(regeneratorRuntime.mark(function _callee4() { 166 | var _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _iterator2, _step2, x; 167 | 168 | return regeneratorRuntime.wrap(function _callee4$(_context4) { 169 | while (1) { 170 | switch (_context4.prev = _context4.next) { 171 | case 0: 172 | _iteratorNormalCompletion2 = true; 173 | _didIteratorError2 = false; 174 | _iteratorError2 = undefined; 175 | _context4.prev = 3; 176 | _iterator2 = gen()[Symbol.iterator](); 177 | 178 | case 5: 179 | if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) { 180 | _context4.next = 13; 181 | break; 182 | } 183 | 184 | x = _step2.value; 185 | 186 | if (!fn(x)) { 187 | _context4.next = 10; 188 | break; 189 | } 190 | 191 | _context4.next = 10; 192 | return x; 193 | 194 | case 10: 195 | _iteratorNormalCompletion2 = true; 196 | _context4.next = 5; 197 | break; 198 | 199 | case 13: 200 | _context4.next = 19; 201 | break; 202 | 203 | case 15: 204 | _context4.prev = 15; 205 | _context4.t0 = _context4['catch'](3); 206 | _didIteratorError2 = true; 207 | _iteratorError2 = _context4.t0; 208 | 209 | case 19: 210 | _context4.prev = 19; 211 | _context4.prev = 20; 212 | 213 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 214 | _iterator2.return(); 215 | } 216 | 217 | case 22: 218 | _context4.prev = 22; 219 | 220 | if (!_didIteratorError2) { 221 | _context4.next = 25; 222 | break; 223 | } 224 | 225 | throw _iteratorError2; 226 | 227 | case 25: 228 | return _context4.finish(22); 229 | 230 | case 26: 231 | return _context4.finish(19); 232 | 233 | case 27: 234 | case 'end': 235 | return _context4.stop(); 236 | } 237 | } 238 | }, _callee4, this, [[3, 15, 19, 27], [20,, 22, 26]]); 239 | })); 240 | }; 241 | 242 | var take = exports.take = function take(gen, num) { 243 | return wrap(regeneratorRuntime.mark(function _callee5() { 244 | var it, i; 245 | return regeneratorRuntime.wrap(function _callee5$(_context5) { 246 | while (1) { 247 | switch (_context5.prev = _context5.next) { 248 | case 0: 249 | it = gen(); 250 | i = 0; 251 | 252 | case 2: 253 | if (!(i < num)) { 254 | _context5.next = 8; 255 | break; 256 | } 257 | 258 | _context5.next = 5; 259 | return it.next().value; 260 | 261 | case 5: 262 | i++; 263 | _context5.next = 2; 264 | break; 265 | 266 | case 8: 267 | case 'end': 268 | return _context5.stop(); 269 | } 270 | } 271 | }, _callee5, this); 272 | })); 273 | }; 274 | 275 | var value = exports.value = function value(gen) { 276 | var x = []; 277 | var _iteratorNormalCompletion3 = true; 278 | var _didIteratorError3 = false; 279 | var _iteratorError3 = undefined; 280 | 281 | try { 282 | for (var _iterator3 = gen()[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { 283 | var i = _step3.value; 284 | 285 | x.push(i); 286 | } 287 | } catch (err) { 288 | _didIteratorError3 = true; 289 | _iteratorError3 = err; 290 | } finally { 291 | try { 292 | if (!_iteratorNormalCompletion3 && _iterator3.return) { 293 | _iterator3.return(); 294 | } 295 | } finally { 296 | if (_didIteratorError3) { 297 | throw _iteratorError3; 298 | } 299 | } 300 | } 301 | 302 | return x; 303 | }; 304 | 305 | var wrap = exports.wrap = function wrap(gen) { 306 | var g = regeneratorRuntime.mark(function g() { 307 | return regeneratorRuntime.wrap(function g$(_context6) { 308 | while (1) { 309 | switch (_context6.prev = _context6.next) { 310 | case 0: 311 | return _context6.delegateYield(gen(), 't0', 1); 312 | 313 | case 1: 314 | case 'end': 315 | return _context6.stop(); 316 | } 317 | } 318 | }, g, this); 319 | }); 320 | return [value, lmap, lfilter, take].reduce(function (g, v) { 321 | g[fnName(v)] = v.bind(null, gen); 322 | return g; 323 | }, g); 324 | }; 325 | 326 | var fnName = exports.fnName = function fnName(fn) { 327 | return (/^function (\w+)/.exec(fn + '')[1] 328 | ); 329 | }; 330 | 331 | // create a generator that will step through each item (finite sequence) 332 | // let test = iter(1,2,3,4,5,6,7,8,9,10) 333 | // log(test.value()) // accumulate the output with gen.value() 334 | // log(value(test)) // ... or value(gen) 335 | 336 | // ... or pass in an array, or any combination of values 337 | // let test2 = iter(1,2,3,[4,5,6],[[7,8,9,[10]]]) 338 | // log( test2.value() ) 339 | 340 | // lazily evaluate items with lmap/lfilter 341 | // log( lmap(test, x => x * 2).value() ) 342 | // log( lfilter(test, x => x < 7).value() ) 343 | 344 | // chain lazy operations together 345 | // ... via traditional passing 346 | // log( value(take(lfilter(lmap(test, x=>2*x), x=>x>=10), 2)) ) 347 | // ... or via chaining 348 | // log( test.lmap(x=>2*x).lfilter(x => x>10).value() ) 349 | 350 | // any operation can be told to do "just enough work", or all of it 351 | // log( test.lmap(x => 2*x).lfilter(x => x<10).value() ) // calculates 4 results, returns array of 4 352 | // log( test.lmap(x => 2*x).value().slice(0,4) ) // calculates 10 results, returns array of 4 353 | // log( test.lmap(x => 2*x).lfilter(x => x<10).take(2).value() ) // only calculates 2 items 354 | 355 | // you don't have to think in finite series / arrays 356 | // log( seq(0, 2).lmap(x => Math.pow(x,2)).take(20).value() ) 357 | 358 | // const seqFrom = fn => { 359 | // let g = [] 360 | // fn(val => g.unshift(val)) 361 | // return wrap(function*(){ 362 | // // while(true){ 363 | // // yield g.pop() 364 | // } 365 | // }) 366 | // } 367 | 368 | // let mouse = seqFrom(fn => 369 | // window.addEventListener('mousemove', ({screenX:x, screenY:y}) => 370 | // fn([x,y]))) 371 | 372 | // log(mouse.take(100).value()) -------------------------------------------------------------------------------- /dist/meta.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 8 | 9 | var mixin = exports.mixin = function mixin() { 10 | for (var _len = arguments.length, classes = Array(_len), _key = 0; _key < _len; _key++) { 11 | classes[_key] = arguments[_key]; 12 | } 13 | 14 | var _mixin = function _mixin() { 15 | _classCallCheck(this, _mixin); 16 | }; 17 | 18 | var proto = _mixin.prototype; 19 | 20 | classes.map(function (_ref) { 21 | var p = _ref.prototype; 22 | 23 | Object.getOwnPropertyNames(p).map(function (key) { 24 | var oldFn = proto[key] || function () {}; 25 | proto[key] = function () { 26 | oldFn.apply(undefined, arguments); 27 | return p[key].apply(p, arguments); 28 | }; 29 | }); 30 | }); 31 | 32 | return _mixin; 33 | }; -------------------------------------------------------------------------------- /dist/model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 8 | 9 | var is = exports.is = function is(type, value) { 10 | if (type && type.isValid instanceof Function) { 11 | return type.isValid(value); 12 | } else if (type === String && (value instanceof String || typeof value === 'string') || type === Number && (value instanceof Number || typeof value === 'number') || type === Boolean && (value instanceof Boolean || typeof value === 'boolean') || type === Function && (value instanceof Function || typeof value === 'function') || type === Object && (value instanceof Object || (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') || type === undefined) { 13 | return true; 14 | } 15 | 16 | return false; 17 | }; 18 | 19 | var check = function check(types, required, data) { 20 | Object.keys(types).forEach(function (key) { 21 | var t = types[key], 22 | value = data[key]; 23 | 24 | if (required[key] || value !== undefined) { 25 | if (!(t instanceof Array)) t = [t]; 26 | 27 | var i = t.reduce(function (a, _type) { 28 | return a || is(_type, value); 29 | }, false); 30 | if (!i) { 31 | throw '{' + key + ': ' + JSON.stringify(value) + '} is not one of ' + t.map(function (x) { 32 | return '\n - ' + x; 33 | }); 34 | } 35 | } 36 | }); 37 | 38 | return true; 39 | }; 40 | 41 | var Model = exports.Model = function Model() { 42 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 43 | args[_key] = arguments[_key]; 44 | } 45 | 46 | var types = void 0, 47 | required = void 0, 48 | logic = void 0; 49 | args.map(function (x) { 50 | if (x instanceof Function && !logic) { 51 | logic = x; 52 | } else if ((typeof x === 'undefined' ? 'undefined' : _typeof(x)) === 'object') { 53 | if (!types) { 54 | types = x; 55 | } else if (!required) { 56 | required = x; 57 | } 58 | } 59 | }); 60 | 61 | var isValid = function isValid(data) { 62 | var pipe = logic ? [check, logic] : [check]; 63 | return pipe.reduce(function (a, v) { 64 | return a && v(types || {}, required || {}, data); 65 | }, true); 66 | }; 67 | 68 | var whenValid = function whenValid(data) { 69 | return new Promise(function (res, rej) { 70 | return isValid(data) && res(data); 71 | }); 72 | }; 73 | 74 | return { isValid: isValid, whenValid: whenValid }; 75 | }; 76 | 77 | var ArrayOf = exports.ArrayOf = function ArrayOf(M) { 78 | return Model(function (t, r, data) { 79 | if (!(data instanceof Array)) throw data + ' not an Array'; 80 | data.map(function (x) { 81 | if (!is(M, x)) throw x + ' is not a model instance'; 82 | }); 83 | return true; 84 | }); 85 | }; 86 | 87 | /** 88 | Use it 89 | 90 | 91 | // create a Name model with required first/last, 92 | // but optional middle 93 | let Name = Model({ 94 | first: String, 95 | middle: String, 96 | last: String 97 | }, {first:true, last:true}) 98 | 99 | // create a Tags model with extra checks 100 | let Tags = Model((types,required,data) => { 101 | if(!(data instanceof Array)) throw `${data} not an Array` 102 | data.map(x => { 103 | if(!is(String, x)) 104 | throw `[${data}] contains non-String` 105 | }) 106 | return true 107 | }) 108 | 109 | // create a Price model that just has a business logic fn 110 | let Price = Model((t,r,d) => { 111 | return (d instanceof Number || typeof d === 'number') && d !== 0 112 | }) 113 | 114 | // create an Activity model with a required type and name, 115 | // all others optional 116 | let Activity = Model({ 117 | type: [String, Function, Number], 118 | latlng: Array,//LatLng, 119 | title: String, 120 | tags: Tags, 121 | name: Name, 122 | price: Price 123 | }, {name:true, price: true}) 124 | 125 | // create a new Activity instance, throwing errors if there are 126 | // any invalid fields. 127 | let a = { 128 | tags: ['1','2'], 129 | type: 1, 130 | name: {first:'matt',last:'keas'}, 131 | price: 100.43, 132 | url: 'http://www.google.com' 133 | } 134 | Activity.whenValid(a).then(log).catch(e => log(e+'')) 135 | **/ -------------------------------------------------------------------------------- /dist/monad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 10 | 11 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 12 | 13 | var ident = function ident(x) { 14 | return x; 15 | }; 16 | var keys = function keys(o) { 17 | return Object.keys(o); 18 | }; 19 | var bind = function bind(f, g) { 20 | return f(g()); 21 | }; 22 | 23 | var of = function of(val) { 24 | var isNothing = !val; 25 | var map = function map() { 26 | var f = arguments.length <= 0 || arguments[0] === undefined ? ident : arguments[0]; 27 | 28 | if (val instanceof Array) return isNothing ? of([]) : of(val.filter(function (x) { 29 | return !x.isNothing; 30 | }).map(f)); 31 | 32 | if (val && (typeof val === 'undefined' ? 'undefined' : _typeof(val)) === 'object') return isNothing ? of({}) : of(keys(val).reduce(function (acc, key) { 33 | return _extends({}, acc, _defineProperty({}, key, f(val[key], key))); 34 | }, {})); 35 | 36 | return isNothing ? of(null) : of(f(val)); 37 | }; 38 | 39 | return { 40 | map: map, 41 | isNothing: isNothing, 42 | val: val 43 | }; 44 | }; 45 | 46 | exports.default = of; 47 | 48 | // log( 49 | // of(null) 50 | // .map(x => x+1) 51 | // .map(x => x*3) 52 | // .map(x => x*5 + 10+x) 53 | // .map(x => x+' wha?') 54 | // .val+'' 55 | // ) 56 | 57 | // log( 58 | // of([1,2,3]) 59 | // .map(x => x+1) 60 | // .map(x => x*3) 61 | // .map(x => x*5 + 10+x) 62 | // .map(x => x+' wha?') 63 | // .val+'' 64 | // ) 65 | 66 | // log( 67 | // of({matt:28, ian:30, jeremy: 37}) 68 | // .map(x => x+1) 69 | // .map(x => x*3) 70 | // .map(x => x*5 + 10+x) 71 | // .map(x => x+' wha?') 72 | // .val 73 | // ) -------------------------------------------------------------------------------- /dist/mux.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.muxer = undefined; 7 | 8 | var _fetch = require('./fetch'); 9 | 10 | var _store = require('./store'); 11 | 12 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 13 | 14 | var debounce = function debounce(func, wait, timeout) { 15 | return function () { 16 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 17 | args[_key] = arguments[_key]; 18 | } 19 | 20 | var later = function later() { 21 | timeout = null; 22 | func.apply(undefined, args); 23 | }; 24 | clearTimeout(timeout); 25 | timeout = setTimeout(later, wait); 26 | }; 27 | }; 28 | 29 | /** 30 | * Muxed/Demuxed requests will involve pipelined, serialized request objects sent along together in an array. 31 | * 32 | * i.e. [ 33 | * {url: '...', {headers:{...}, form:{...}}}, 34 | * {url: '...', {headers:{...}, form:{...}}}, 35 | * {url: '...', {headers:{...}, form:{...}}}, 36 | * ... 37 | * ] 38 | */ 39 | 40 | var muxer = exports.muxer = function muxer(batch_url) { 41 | var f = arguments.length <= 1 || arguments[1] === undefined ? _fetch.fetch : arguments[1]; 42 | var wait = arguments.length <= 2 || arguments[2] === undefined ? 60 : arguments[2]; 43 | var max_buffer_size = arguments.length <= 3 || arguments[3] === undefined ? 8 : arguments[3]; 44 | 45 | var payload = (0, _store.store)([]); 46 | 47 | // puts url,options,id on payload 48 | var worker = function worker(url, options) { 49 | return payload.dispatch(function (state, next) { 50 | return next([].concat(_toConsumableArray(state), [{ url: url, options: options }])); 51 | }).then(function (state) { 52 | return state.length - 1; 53 | }); 54 | }; 55 | 56 | var sendImmediate = function sendImmediate() { 57 | var cbs = callbacks; 58 | callbacks = []; 59 | var p = payload.state(); 60 | payload.dispatch(function (state, next) { 61 | return next(state); 62 | }, []); // reset payload for next batch of requests 63 | f(batch_url, { 64 | method: 'POST', 65 | body: JSON.stringify(p), 66 | headers: { 67 | 'Accept': 'application/json', 68 | 'Content-Type': 'application/json' 69 | } 70 | }).then(function (data) { 71 | return cbs.forEach(function (cb) { 72 | return cb(data); 73 | }); 74 | }); // ordered array of requests 75 | }; 76 | 77 | // sends payload after `wait` ms 78 | var send = debounce(sendImmediate, wait); 79 | 80 | var callbacks = []; 81 | var queue = function queue(cb) { 82 | callbacks.push(cb); 83 | // if(callbacks.length >= max_buffer_size) 84 | // sendImmediate() 85 | // else 86 | send(); 87 | }; 88 | 89 | var get = function get(url) { 90 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 91 | return ( 92 | // add {url,options} to payload 93 | // resolves to data[index] under assumption the endpoint returns 94 | // data in order it was requested 95 | worker(url, options).then(function (index) { 96 | return new Promise(function (res) { 97 | return queue(function (data) { 98 | return res(data[index]); 99 | }); 100 | }); 101 | }) 102 | ); 103 | }; 104 | 105 | return (0, _fetch.cancellable)(get); 106 | }; 107 | 108 | // example 109 | // ---------- 110 | // # mocked response from server 111 | // const mock = (url,{body}) => { 112 | // return Promise.resolve(JSON.parse(body).map(({url,options:data}) => { 113 | // switch(url) { 114 | // case '/cows': return {name: 'cow', sound: 'moo', data} 115 | // case '/kittens': return {name: 'cat', sound: 'meow', data} 116 | // } 117 | // })) 118 | // } 119 | // 120 | // # create the muxer, pass in a custom fetch 121 | // const uberfetch = muxer('/api/mux', mock) 122 | // uberfetch('/cows', {age: 5}).then(log) 123 | // uberfetch('/cows', {age: 10}).then(log) 124 | // uberfetch('/cows', {age: 15}).then(log) 125 | // uberfetch('/cows', {age: 20}).then(log) 126 | // uberfetch('/cows', {age: 25}).then(log) 127 | // uberfetch('/cows', {age: 50}).then(log) 128 | // uberfetch('/kittens').then(log) 129 | // uberfetch('/kittens', {wantsMilk: true}).then(log) 130 | // uberfetch('/kittens', {scratchedUpMyCouch: true}).then(log) -------------------------------------------------------------------------------- /dist/ot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.apply = exports.retain = exports.remove = exports.insert = exports.comp = exports.transform = undefined; 7 | 8 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); // inspired by http://operational-transformation.github.io/index.html 9 | 10 | var _fp = require('./fp'); 11 | 12 | // clones each opchain and sums up retain() indexes 13 | var computeIndices = function computeIndices() { 14 | for (var _len = arguments.length, ops = Array(_len), _key = 0; _key < _len; _key++) { 15 | ops[_key] = arguments[_key]; 16 | } 17 | 18 | return ops.map(function (op) { 19 | return op.reduce(function (a, v) { 20 | if (v.retain) { 21 | v.index = a.reduce(function (count, i) { 22 | return count + (i.retain || 0); 23 | }, 0) + v.retain; 24 | } 25 | return a.concat((0, _fp.clone)(v)); 26 | }, []); 27 | }); 28 | }; 29 | 30 | var transform = exports.transform = function transform(_a, _b) { 31 | // tally retains 32 | var _computeIndices = computeIndices(_a, _b); 33 | 34 | var _computeIndices2 = _slicedToArray(_computeIndices, 2); 35 | 36 | var at = _computeIndices2[0]; 37 | var bt = _computeIndices2[1]; 38 | var res = []; 39 | var lastA = null; 40 | var lastB = null; 41 | 42 | // walk through each opchain and combine them 43 | while (at.length || bt.length) { 44 | var a = at[0], 45 | b = bt[0], 46 | aRetain = a && a.retain !== undefined, 47 | bRetain = b && b.retain !== undefined; 48 | 49 | if (a && !aRetain) { 50 | // run until you hit a retain or end 51 | while (a && !aRetain) { 52 | res.push(a); 53 | at.shift(); 54 | a = at[0]; 55 | aRetain = a && a.retain !== undefined; 56 | } 57 | continue; 58 | } else if (b && !bRetain) { 59 | // run until you hit a retain or end 60 | while (b && !bRetain) { 61 | res.push(b); 62 | bt.shift(); 63 | b = bt[0]; 64 | bRetain = b && b.retain !== undefined; 65 | } 66 | continue; 67 | } 68 | 69 | // now a and b are either retain ops or undefined 70 | 71 | if (a && b) { 72 | var lower = a.index <= b.index ? a : b, 73 | diff = Math.abs(a.index - b.index); 74 | if (lower === a) { 75 | b.retain = diff; 76 | res.push(a); 77 | at.shift(); 78 | } else { 79 | a.retain = diff; 80 | res.push(b); 81 | bt.shift(); 82 | } 83 | lastA = a; 84 | lastB = b; 85 | } else if (!a && b) { 86 | res.push(b); 87 | bt.shift(); 88 | lastB = b; 89 | } else if (a && !b) { 90 | res.push(a); 91 | at.shift(); 92 | lastA = a; 93 | } 94 | } 95 | 96 | return res; 97 | }; 98 | // ops.reduce((a,v) => a.concat(...v), []) 99 | 100 | var comp = exports.comp = function comp() { 101 | for (var _len2 = arguments.length, ops = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 102 | ops[_key2] = arguments[_key2]; 103 | } 104 | 105 | return ops.reduce(function (a, v) { 106 | return a.concat(v); 107 | }, []); 108 | }; 109 | 110 | var insert = exports.insert = function insert(text) { 111 | return { insert: text }; 112 | }; 113 | 114 | var remove = exports.remove = function remove(text) { 115 | return { remove: text }; 116 | }; 117 | 118 | var retain = exports.retain = function retain(num) { 119 | return { retain: num }; 120 | }; 121 | 122 | var apply = exports.apply = function apply(str, ops) { 123 | var _i = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; 124 | 125 | var r = ops.reduce(function (_ref, v) { 126 | var a = _ref.a; 127 | var i = _ref.i; 128 | 129 | // log({a,i}, v) 130 | 131 | // at position i, insert v into a 132 | if (v.insert !== undefined) { 133 | return { 134 | a: a.slice(0, i) + v.insert + a.slice(i), 135 | i: i + v.insert.length 136 | }; 137 | } 138 | 139 | // at position i, remove string v 140 | if (v.remove !== undefined) { 141 | var n = a.slice(i).slice(0, v.remove.length); 142 | if (n !== v.remove) throw 'remove error: ' + n + ' not at index i'; 143 | return { 144 | a: a.slice(0, i) + a.slice(i + v.remove.length), 145 | i: Math.max(0, i - v.remove.length) 146 | }; 147 | } 148 | 149 | // at position i, retain v chars 150 | if (v.retain !== undefined) { 151 | if (i + v.retain > a.length) throw 'retain error: not enough characters in string to retain'; 152 | return { a: a, i: i + v.retain }; 153 | } 154 | 155 | throw 'unrecognizable op: ' + JSON.stringify(v); 156 | }, { a: str, i: _i }); 157 | 158 | // uncomment to ensure opchains must represent 159 | // all content within the result 160 | // if(r.i !== r.a.length) 161 | // throw `incomplete operations, expected length ${r.a.length}, but instead is ${r.i}` 162 | 163 | return r.a; 164 | }; 165 | 166 | /** 167 | 168 | HOW TO USE THIS MICROLIB: 169 | ---------------- 170 | 171 | import {transform, comp, insert, remove, retain, apply} 172 | 173 | 1. setup some operations, i.e. p1 adds '-you-' to a string at spot 2, and p2 adds '-i-' to a string at spot 0 174 | 175 | let p1 = comp( 176 | retain(2), 177 | insert('-you-') 178 | ), 179 | p2 = comp( 180 | insert('-i-'), 181 | retain(2), 182 | insert('-us-') 183 | ) 184 | 185 | 2. observe what a transform operation is: simple arrays of a small object representing how to edit something (replays actions in chronological order) 186 | 187 | log(p1) 188 | log(p2) 189 | 190 | 3. observe how to merge two parallel operations into one single operation chain 191 | 192 | log(transform(p1,p2)) 193 | 194 | 4. apply an "opchain" to a string 195 | 196 | log(apply('me', p1)) 197 | log(apply('me', transform(p1,p2))) 198 | 199 | 5. test out interactions within arbiter (https://goo.gl/2iNxDy) 200 | 201 | const css = ` 202 | form { 203 | padding: 1rem; 204 | } 205 | 206 | textarea { 207 | display: block; 208 | width: 100%; 209 | outline: 1px solid #222; 210 | background: #111; 211 | color: #aaa; 212 | font-family: monospace; 213 | font-weight: 100; 214 | padding: .5rem; 215 | } 216 | ` 217 | 218 | const app = () => { 219 | let u = universalUtils, 220 | {m, mount, update, qs, comp, apply, transform, insert, remove, retain} = u, 221 | stream = '' 222 | 223 | const edit = (val='') => 224 | (e) => { 225 | let start = e.srcElement.selectionStart, 226 | end = e.srcElement.selectionEnd, 227 | {which} = e, 228 | v = e.srcElement.value, 229 | difflen = v.length - val.length 230 | 231 | // log([stream, val, v]) 232 | 233 | if(difflen === 0) { 234 | log('length same, but content may have changed - TODO') 235 | } else if(difflen < 0){ 236 | log('content deleted', [start,end], difflen) 237 | stream = apply(val, comp( 238 | retain(start), 239 | remove(val.slice(start, -1*difflen)) 240 | )) 241 | update() 242 | } else { 243 | log('content added', [start,end], difflen) 244 | let beforeInsert = v.slice(0,start-difflen) 245 | stream = apply(val, comp( 246 | retain(beforeInsert.length), 247 | insert(v.slice(start-difflen,start)) 248 | )) 249 | update() 250 | } 251 | 252 | val = v 253 | } 254 | 255 | const t1 = edit(stream) 256 | 257 | const form = () => { 258 | return [ 259 | m('style', {type: 'text/css', config: el => el.innerText=css}), 260 | m('form', m('textarea', {rows: 5, onkeyup:t1, value:stream, placeholder:'type here'})), 261 | m('form', m('textarea#a', {rows: 5, value:stream})) 262 | ] 263 | } 264 | 265 | mount(form, qs()) 266 | } 267 | require('universal-utils').then(app) 268 | 269 | 6. as you can see, there's a lot of implementation involved in consuming this file; when creating the opchain, we have to re-read the value of the text and generate the array of operations that produced that change, and then send that generated opchain over-the-wire. While complex, this allows us to communicate chronological sequences of action taken by a user as they type up a document, making it possible gracefully handle updates to a shared document (i.e. Google Docs) that many people are simultaneously editing. 270 | 271 | **/ -------------------------------------------------------------------------------- /dist/resource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.resource = undefined; 7 | 8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; // The `resource()` module is a mechanism that wraps around the previous modules (`fetch()`, `cache()`, `store()`), 9 | // exposing one primary method `get()`. Example code at end of file. 10 | 11 | 12 | var _store = require('./store'); 13 | 14 | var _cache = require('./cache'); 15 | 16 | var _fetch2 = require('./fetch'); 17 | 18 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 19 | 20 | var resource = exports.resource = function resource() { 21 | var config = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 22 | var defaultState = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 23 | 24 | 25 | var inflight = {}; 26 | 27 | var store = (0, _store.store)(defaultState); 28 | var url = config.url; 29 | var fetch = config.fetch; 30 | var nocache = config.nocache; 31 | var name = config.name; 32 | var cacheDuration = config.cacheDuration; 33 | var f = fetch || _fetch2.fetch; 34 | 35 | var get = function get(id) { 36 | for (var _len = arguments.length, params = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 37 | params[_key - 1] = arguments[_key]; 38 | } 39 | 40 | // generate a key unique to this request for muxing/batching, 41 | // if need be (serialized with the options) 42 | var key = name + ':' + JSON.stringify(id) + ':' + JSON.stringify(params); 43 | 44 | // get an inflight Promise the resolves to the data, keyed by `id`, 45 | // or create a new one 46 | return inflight[key] || (inflight[key] = new Promise(function (res, rej) { 47 | return (nocache ? Promise.reject() : _cache.cache.getItem(key)).then(function (d) { 48 | return res(d); 49 | }).catch(function (error) { 50 | return ( 51 | // whatever fetching mechanism is used (batched, muxed, etc) 52 | // send the resourceName, id, params with the request as options. 53 | // if it is going the node server, node (when demuxing) will use the 54 | // extra options to rebuild the URL 55 | // 56 | // in normal URL requests, we can just carry on as normal 57 | f(url.apply(undefined, [id].concat(params)), { resourceName: name, id: id, params: params }).then(function (d) { 58 | if (!d) throw 'no data returned from ' + key; 59 | return d; 60 | }).then(function (d) { 61 | return store.dispatch(function (state, next) { 62 | var _state = _extends({}, state, _defineProperty({}, key, d)); // make new state 63 | inflight = _extends({}, inflight, _defineProperty({}, key, undefined)); // clear in-flight promise 64 | !nocache && _cache.cache.setItem(key, d, cacheDuration); 65 | next(_state); // store's new state is _state 66 | }).then(function (state) { 67 | return state[key]; 68 | }); 69 | }) // pipe state[_id] to the call to f() 70 | .then(function (d) { 71 | return res(d); 72 | }) // resolve the f(url(id)) 73 | .catch(function (e) { 74 | inflight = _extends({}, inflight, _defineProperty({}, key, undefined)); // in case of fire... 75 | rej(e); 76 | }) 77 | ); 78 | }); 79 | })); 80 | }; 81 | 82 | var clear = function clear(id) { 83 | for (var _len2 = arguments.length, params = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { 84 | params[_key2 - 1] = arguments[_key2]; 85 | } 86 | 87 | if (!id) { 88 | return _cache.cache.clearAll(name + ":"); 89 | } 90 | 91 | // generate a key unique to this request for muxing/batching, 92 | // if need be (serialized with the options) 93 | var key = name + ':' + JSON.stringify(id) + ':' + JSON.stringify(params); 94 | return _cache.cache.setItem(key, null); 95 | }; 96 | 97 | return { name: name, store: store, get: (0, _fetch2.cancellable)(get), clear: clear }; 98 | }; 99 | 100 | // !! Example usage 101 | // 102 | // !! isomorphic/universal usage 103 | // const root = global.document ? window.location.origin : 'http://myapiserverurl.com' 104 | // !! browser talks to node server, node server proxies to API 105 | // const isomorphicfetch = require('isomorphic-fetch') 106 | // !! muxer, that uses isomorphic fetch for transport on the client, but straight up isomorphic fetch() on node 107 | // !! browser will send mux'ed requests to be demux'ed at '/mux' 108 | // const fetch = global.document ? mux('/mux', isomorphicfetch) : isomorphicfetch 109 | // const cacheDuration = 2*60*60*1000 // 2 hours 110 | // !! url functions simply return a string, a call to resource.get(...args) will make a request to url(...args) 111 | // !! imagine LOCATION.location() and SEARCH.search() exist and return strings, too 112 | // const RESOURCES = { 113 | // PROPERTY: resource({ name: 'PROPERTY', fetch, cacheDuration, url: id => `props/${id}` }), 114 | // CATALOG: resource({ name: 'CATALOG', fetch, cacheDuration, url: id => `catalogs/${id}` }), 115 | // LOCATION: resource({ name: 'LOCATION', fetch, cacheDuration, url: (...args) => LOCATION.location(...args) }), 116 | // PRICE: resource({ name: 'PRICE', fetch, cacheDuration, url: id => `prices/${id}` }), 117 | // SEARCH: resource({ name: 'SEARCH', fetch, cacheDuration, nocache: true // don't cache requests to this API 118 | // url: (id, {sort, page, pageSize, hash, ...extraQueryParams}) => SEARCH.search( id, hash, sort, page, pageSize ) 119 | // }) 120 | // } 121 | // !! use it 122 | // RESOURCES.PROPERTY.get(123).then(property => ... draw some React component?) 123 | // RESOURCES.CATALOG.get(71012).then(catalog => ... draw some React component?) 124 | // RESOURCES.LOCATION.get('Houston', 'TX', 77006).then(price => ... draw some React component?) 125 | // !! all of the above are separate requests, but they are sent as A SINGLE REQUEST to /mux in the browser, and sent to the actual API in node 126 | // !! you can also choose to have the browser AND node send mux'ed requests by making the fetch() just be isomorphic fetch, if the node server isn't the API server and your API sever supports it -------------------------------------------------------------------------------- /dist/router-alt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 10 | 11 | var trim = function trim(str) { 12 | return str.replace(/^[\s]+/ig, '').replace(/[\s]+$/ig, ''); 13 | }; 14 | 15 | var router = exports.router = function router(routes) { 16 | var fn = arguments.length <= 1 || arguments[1] === undefined ? function (a, b) { 17 | return a(b); 18 | } : arguments[1]; 19 | 20 | var current = null; 21 | 22 | var listen = function listen(onError) { 23 | window.addEventListener('hashchange', function () { 24 | if (!trigger(window.location.hash.slice(1))) { 25 | onError instanceof Function && onError(window.location.hash.slice(1)); 26 | } 27 | }); 28 | }; 29 | 30 | var trigger = function trigger(path) { 31 | for (var x in routes) { 32 | if (routes.hasOwnProperty(x)) { 33 | var v = match(x, path); 34 | if (v) { 35 | fn(routes[x], v); 36 | return true; 37 | } 38 | } 39 | } 40 | 41 | return false; 42 | }; 43 | 44 | var match = function match(pattern, path) { 45 | var _patterns = pattern.split('/'), 46 | parts = _patterns.map(function (x) { 47 | switch (x[0]) { 48 | case ':': 49 | return '([^/]+)'; 50 | case '*': 51 | return '.*'; 52 | default: 53 | return x; 54 | } 55 | }), 56 | uris = path.split('/'); 57 | 58 | for (var i = 0; i < Math.max(parts.length, uris.length); i++) { 59 | var p = trim(parts[i]), 60 | u = trim(uris[i]), 61 | v = null; 62 | 63 | if (p === '' || u === '') { 64 | v = p === '' && u === ''; 65 | } else { 66 | v = new RegExp(p).exec(u); 67 | } 68 | 69 | if (!v) return false; 70 | } 71 | 72 | return parts.reduce(function (a, v, i) { 73 | if (v[0] === ':') return _extends({}, a, _defineProperty({}, v, uris[i])); 74 | return a; 75 | }, {}); 76 | }; 77 | 78 | return { 79 | add: function add(name, fn) { 80 | return !!(routes[name] = fn); 81 | }, 82 | remove: function remove(name) { 83 | return !!delete routes[name] || true; 84 | }, 85 | listen: listen, 86 | match: match, 87 | trigger: trigger 88 | }; 89 | }; 90 | 91 | // use a router inside a custom Component in React ... 92 | // const app = () => { 93 | // let [React, DOM] = [react, reactDom], 94 | // {Component} = React 95 | 96 | // class Home extends Component { 97 | // constructor(...a){ 98 | // super(...a) 99 | // } 100 | // render(){ 101 | // return

Home Screen

102 | // } 103 | // } 104 | 105 | // export class Router extends Component { 106 | // constructor(...a){ 107 | // super(...a) 108 | 109 | // let p = this.props 110 | 111 | // this.state = { 112 | // routes: p.routes || {}, 113 | // default: p.default || '/' 114 | // } 115 | 116 | // this.router = router(this.state.routes, (el, props) => { 117 | // this.current = el 118 | // }) 119 | 120 | // this.router.trigger(this.state.default) 121 | // } 122 | // render(){ 123 | // return this.current() 124 | // } 125 | // } 126 | 127 | // DOM.render( 129 | // }}/>, document.body) 130 | // } 131 | 132 | // require('react', 'react-dom').then(app) 133 | 134 | // ... or use router outside of a React Component 135 | // let x = router({ 136 | // '/:x/test/:y' : ({x,y}) => log({x,y}), 137 | // '/': () => log('home screen') 138 | // }) 139 | 140 | // log(x.match('/:x/test/:y', '/anything/test/anything')) // test a route pattern with a route 141 | // x.trigger('/') // trigger the / route 142 | // x.trigger('/hi/test/bye') // any named URI segments will be passed to the route callback -------------------------------------------------------------------------------- /dist/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 10 | 11 | // The `router()` module is a simple client-side `hashchange` event router that allows Single Page Apps to effectively map and listen to route changes. For `router()` implementation examples, see the `router.js` file. 12 | // 13 | // Example usage at end of this file. 14 | 15 | var router = exports.router = function router(routes, routeTransform) { 16 | var hashroutes = Object.keys(routes).map(function (route) { 17 | var tokens = route.match(/:(\w+)/ig), 18 | handler = routeTransform(routes[route]); 19 | 20 | var regex = (tokens || []).reduce(function (a, v) { 21 | return a.replace(v, '([^/])+'); 22 | }, route).replace(/(\*)/ig, '([^/])*'); 23 | 24 | return { route: route, regex: regex, handler: handler }; 25 | }); 26 | 27 | // a shortcut method to changing the location hash 28 | var page = function page(path) { 29 | return window.location.hash = path; 30 | }; 31 | 32 | // returns true if a route matches a route object, false otherwise 33 | var checkRoute = function checkRoute(hash, routeObj) { 34 | hash = hash[0] === '#' ? hash.substring(1) : hash; 35 | var route = routeObj.route; 36 | var regex = routeObj.regex; 37 | var handler = routeObj.handler; 38 | var reggie = new RegExp(regex, 'ig'); 39 | 40 | return hash.match(reggie); 41 | }; 42 | 43 | // 1. handles a route change, 44 | // 2. checks for matching routes, 45 | // 3. calls just the first matchign route callback 46 | var handleRoute = function handleRoute() { 47 | var matched = hashroutes.filter(function (obj) { 48 | return checkRoute(window.location.hash, obj); 49 | }), 50 | selected = matched[0]; 51 | 52 | if (!selected) return; 53 | 54 | var route = selected.route; 55 | var regex = selected.regex; 56 | var handler = selected.handler; 57 | var tokens = selected.tokens; 58 | var segments = window.location.hash.split('/'); 59 | var mappedSegments = route.split('/').map(function (segment) { 60 | var match = segment.match(/(\*)|:(\w+)/ig); 61 | return match && match[0]; 62 | }); 63 | var routeCtx = segments.reduce(function (a, v, i) { 64 | var _extends2; 65 | 66 | var mappedSegment = mappedSegments[i]; 67 | var indices = a.indices; 68 | 69 | 70 | if (!mappedSegment) return a; 71 | 72 | if (mappedSegment[0] === ':') mappedSegment = mappedSegment.substring(1);else if (mappedSegment[0] === '*') { 73 | mappedSegment = indices; 74 | indices++; 75 | } 76 | 77 | return _extends({}, a, (_extends2 = {}, _defineProperty(_extends2, mappedSegment, v), _defineProperty(_extends2, 'indices', indices), _extends2)); 78 | }, { indices: 0 }); 79 | 80 | handler(routeCtx); 81 | }; 82 | 83 | window.addEventListener('hashchange', handleRoute); 84 | window.onload = function () { 85 | return handleRoute(); 86 | }; 87 | 88 | return { page: page }; 89 | }; 90 | 91 | /** 92 | * EXAMPLE USAGE 93 | */ 94 | 95 | // routes input is an object map, where routes return a function 96 | // User, Playlist, Search, and Home are React Component classes 97 | // ------------------------------------- 98 | // const routes = { 99 | // 'user/:id': () => User, 100 | // 'playlist/:id': () => Playlist, 101 | // 'search/*': () => Search, 102 | // '*': () => Home 103 | // } 104 | 105 | // when routes are handled, the routeCallback is the function/handler from the route map above, 106 | // where any route data will be passed to the function returned by the routeTransform 107 | // 108 | // in this code, I optionall pulled extra data from location.search (query params like ?test=1&name=bob), 109 | // turn it into an object with some other method unquerify(...) ---> { test: '1', name: 'bob' }, 110 | // and pass both the route options and query params as props to the React component 111 | // ------------------------------------- 112 | // const routeTransform = (routeCallback) => 113 | // (ctx) => { 114 | // let options = {...ctx, ...unquerify(window.location.search)} 115 | // ReactDOM.render( 116 | // {React.createElement(routeCallback(), options)}, 117 | // document.querySelector('.container') 118 | // ) 119 | // } 120 | 121 | // start the routing 122 | // ------------------------------------- 123 | // const myRoutes = router(routes, routeTransform) -------------------------------------------------------------------------------- /dist/store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.store = undefined; 7 | 8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; // The `store()` module is a mechanism to store an immutable object that represents state of an application. Any application may have one or more active stores, so there's no limitation from how you use the data. The store itself provides four methods: `state()`, `dispatch(reducer, state)`, `to(cb)`, `remove(cb)`. 9 | // 10 | // 1. The `store.state()` returns a clone of the internal state object, which is simply a pure copy of JSON of the data. Since this uses pure JSON representation in-lieue of actual Tries and immutable data libraries, this keeps the code footprint tiny, but you can only store pure JSON data in the store. 11 | // 2. The `store.to(cb)` will register `cb` as a callback function, invoking `cb(nextState)` whenever the store's state is updated with `store.dispatch()` (`store.remove(cb)` simply does the opposite, removing the callback from the list of event listeners). 12 | // 3. The biggest method implemented by `store()` is `store.dispatch(reducer, state=store.state())`. By default, the second parameter is the existing state of the `store`, but you can override the state object input, if need be. The key here is the redux-inspired `reducer`, which is a function that **you** write that receives two arguments, `state` and `next()`. You should modify the state object somehow, or create a copy, and pass it into `next(state)` to trigger an update to be sent to listener. For example: 13 | // 14 | // ```js 15 | // const logger = (state) => console.log('state changed! -->', state) 16 | // store.to(logger) 17 | // 18 | // store.distpatch((state, next) => { 19 | // setTimeout(() => { 20 | // let timestamp = +new Date 21 | // next({ ...state, timestamp }) 22 | // }, 2000) 23 | // }) 24 | // ``` 25 | 26 | var _fetch = require('./fetch'); 27 | 28 | var clone = function clone(obj) { 29 | return JSON.parse(JSON.stringify(obj)); 30 | }; 31 | 32 | /** 33 | * 34 | * Event-driven redux-like updates where we use 35 | * reducer functions to update a singular state object 36 | * contained within the store() 37 | * 38 | * Some limitations: You **must** use plain JS objects and arrays 39 | * with this implementation for serialization and cloning support. 40 | * This could eventually use actual immutable data-structures, but 41 | * an implementation change would be required; however if speed 42 | * or correctness is an issue we can try in the future, as immutable 43 | * libraries use data-structures like tries and the like to reduce 44 | * Garbage Collection and blatant in-memory copies with a "structural 45 | * sharing" technique. 46 | * 47 | * - state() 48 | * - dispatch() 49 | * - to() 50 | * - remove() 51 | */ 52 | 53 | var store = exports.store = function store() { 54 | var _state2 = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 55 | 56 | // might have to be changed back to Set() 57 | // if subscribers get garbage collected 58 | // 59 | // WeakSet and WeakMap items are GC'd if 60 | // they have no other reference pointing to them 61 | // other than the WeakMap/WeakSet 62 | var subscribers = new Set(), 63 | actions = {}; 64 | 65 | var instance = { 66 | state: function state() { 67 | return clone(_state2); 68 | }, 69 | dispatch: function dispatch(reducer) { 70 | var _state = arguments.length <= 1 || arguments[1] === undefined ? instance.state() : arguments[1]; 71 | 72 | return new Promise(function (res, rej) { 73 | var next = function next(newState) { 74 | _state2 = clone(newState); 75 | var _iteratorNormalCompletion = true; 76 | var _didIteratorError = false; 77 | var _iteratorError = undefined; 78 | 79 | try { 80 | for (var _iterator = subscribers[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 81 | var s = _step.value; 82 | 83 | s(clone(_state2)); 84 | } 85 | } catch (err) { 86 | _didIteratorError = true; 87 | _iteratorError = err; 88 | } finally { 89 | try { 90 | if (!_iteratorNormalCompletion && _iterator.return) { 91 | _iterator.return(); 92 | } 93 | } finally { 94 | if (_didIteratorError) { 95 | throw _iteratorError; 96 | } 97 | } 98 | } 99 | 100 | res(clone(_state2)); 101 | }; 102 | reducer(_state, next); 103 | }); 104 | }, 105 | to: function to(sub) { 106 | return subscribers.add(sub); 107 | }, 108 | remove: function remove(sub) { 109 | return subscribers.delete(sub); 110 | } 111 | }; 112 | 113 | return _extends({}, instance, { dispatch: (0, _fetch.cancellable)(instance.dispatch) }); 114 | }; 115 | 116 | /* 117 | // Example usage: 118 | // ---------------- 119 | 120 | let photos = store({photos:[]}) 121 | log(photos.state()) 122 | 123 | const printMe = (state) => { 124 | log('-------------- subscriber called', state) 125 | } 126 | 127 | photos.to(printMe) 128 | photos.to(printMe) // can't have duplicate subscribers, printMe only called once per update 129 | photos.to((state) => log('hi')) 130 | 131 | const addRandomPhoto = (state, next) => { 132 | setTimeout(() => { 133 | state = {...state, photos: state.photos.concat('https://media0.giphy.com/media/hD52jjb1kwmlO/200w.gif')} 134 | next(state) 135 | }, 1000) 136 | } 137 | 138 | setInterval(() => photos.dispatch(addRandomPhoto), 500) 139 | 140 | /// example React Component code 141 | // 142 | // 143 | let update = (state) => this.setState(state) 144 | let componentDidMount = () => { 145 | photos.to(update) 146 | } 147 | let componentWillUnmount = () => { 148 | photos.remove(update) 149 | } 150 | */ -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./dist/esdoc", 4 | "plugins": [{ 5 | "name": "esdoc-es7-plugin" 6 | }] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-utils", 3 | "version": "1.0.50", 4 | "description": "functional, event, storage, cache, and other utilities", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir dist; cp ./src/cache/package.json ./dist/cache/package.json;", 8 | "deploy": "npm run build; git add --all .; git commit -am 'new build'; npm version patch; git push origin HEAD; npm publish;", 9 | "watch": "babel src --out-dir dist -w; cp ./src/cache/package.json ./dist/cache/package.json;", 10 | "docs": "esdoc -c esdoc.json", 11 | "docs:surge": "npm run docs; surge dist/esdoc;" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/matthiasak/universal-utils.git" 16 | }, 17 | "author": "Matt Keas (@matthiasak)", 18 | "license": "MIT", 19 | "babel": { 20 | "presets": [ 21 | "es2015", 22 | "stage-0" 23 | ] 24 | }, 25 | "optionalDependencies": { 26 | "universal-utils-vdom-components": "0.x", 27 | "redis": "2.x" 28 | }, 29 | "dependencies": { 30 | "babel-cli": "^6.10.1", 31 | "babel-core": "^6.10.4", 32 | "babel-preset-react": "^6.11.1", 33 | "isomorphic-fetch": "latest" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "6.x", 37 | "babel-core": "6.x", 38 | "babel-polyfill": "6.x", 39 | "babel-preset-es2015": "6.x", 40 | "babel-preset-react": "6.x", 41 | "babel-preset-stage-0": "6.x", 42 | "esdoc": "0.x", 43 | "esdoc-es7-plugin": "0.x", 44 | "surge": "0.x" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/cache/browser.js: -------------------------------------------------------------------------------- 1 | const s = window.localStorage 2 | 3 | export const cacheCreator = () => { 4 | 5 | const getItem = (key, expire=false) => { 6 | try { 7 | let data = JSON.parse(s.getItem(key)) 8 | if(!data || !data.data) throw 'not in cache' 9 | let expired = expire || (+new Date) > data.expiresAt 10 | if(expired) return Promise.reject(`${key} is expired`) 11 | return Promise.resolve(data.data) 12 | } catch(e) { 13 | return Promise.reject(`${key} not in cache`) 14 | } 15 | } 16 | 17 | const setItem = (key, data, timeout=5*60*60*1000, expiresAt=(+new Date + timeout)) => { 18 | if(!data) return Promise.reject(`data being set on ${key} was null/undefined`) 19 | return new Promise((res,rej) => { 20 | try{ 21 | s.setItem(key, JSON.stringify({expiresAt, data})) 22 | res(true) 23 | }catch (e){ 24 | rej(`key ${key} has a value of ${val}, which can't be serialized`) 25 | } 26 | }) 27 | } 28 | 29 | const clearAll = key => { 30 | if(!key) 31 | s.clear() 32 | for(var i in s){ 33 | if((!key || i.indexOf(key) !== -1) && localstorage.hasOwnProperty(i)) 34 | s.removeItem(i) 35 | } 36 | return Promise.resolve(true) 37 | } 38 | 39 | return { getItem, setItem, clearAll } 40 | } 41 | 42 | export const cache = cacheCreator() 43 | -------------------------------------------------------------------------------- /src/cache/index.js: -------------------------------------------------------------------------------- 1 | const clone = (obj) => 2 | JSON.parse(JSON.stringify(obj)) 3 | 4 | export const cacheCreator = () => { 5 | 6 | let {REDIS_URL} = process.env 7 | 8 | if(REDIS_URL) { 9 | 10 | let client = require('redis').createClient(REDIS_URL) 11 | 12 | "ready,connect,error,reconnecting,end".split(',').map(event => 13 | client.on(event, msg => console.log(`Redis ${event} ${msg ? ' :: '+msg : ''}`))) 14 | 15 | const getItem = (key, expire) => { 16 | return new Promise((res,rej) => { 17 | client.get(key, (err,data) => { 18 | if(err || !data) { 19 | return rej(`${key} not in cache`) 20 | } 21 | data = JSON.parse(data) 22 | let expired = expire || (+new Date) > data.expiresAt 23 | if(expired) rej(`${key} is expired`) 24 | res(data.data) 25 | }) 26 | }) 27 | } 28 | 29 | const setItem = (key, val, timeout=5*60*60*1000) => { 30 | const expiresAt = +new Date + timeout 31 | return new Promise((res,rej) => { 32 | client.set(key, JSON.stringify({expiresAt, data:val}), (...args) => { 33 | res(val) 34 | }) 35 | }) 36 | } 37 | 38 | const clearAll = key => 39 | new Promise((res,rej) => { 40 | client.keys(key, (...args) => 41 | args.map(x => 42 | client.del(x))) 43 | 44 | res() 45 | }) 46 | 47 | return { getItem, setItem, clearAll } 48 | 49 | } else { 50 | 51 | let cache = {} 52 | 53 | const getItem = (key, expire) => { 54 | return new Promise((res,rej) => { 55 | if(key in cache) { 56 | let data = clone(cache[key]), 57 | expired = expire || (data.expiresAt < +new Date) 58 | if(expired) return rej(`${key} is expired`) 59 | if(!data.data) return rej(`${key} has no data`) 60 | return res(data.data) 61 | } 62 | rej(`${key} not in cache`) 63 | }) 64 | } 65 | 66 | const setItem = (key, val, timeout=5*60*60*1000) => { 67 | const expiresAt = +new Date + timeout 68 | let data = {expiresAt, data: val} 69 | return new Promise((res,rej) => { 70 | cache[key] = clone(data) 71 | res(clone(data).data) 72 | }) 73 | } 74 | 75 | const clearAll = key => { 76 | Object.keys(cache).filter(n => n.indexOf(key) !== -1).map(x => delete cache[x]) 77 | return Promise.resolve() 78 | } 79 | 80 | return { getItem, setItem, clearAll } 81 | } 82 | } 83 | 84 | export const cache = cacheCreator() 85 | -------------------------------------------------------------------------------- /src/cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser": "browser.js" 3 | } -------------------------------------------------------------------------------- /src/components.js: -------------------------------------------------------------------------------- 1 | /* huge ups to John Buschea (https://github.com/joshbuchea/HEAD) */ 2 | import {debounce,m,html,rAF,mount,update,qs,container} from './vdom' 3 | import {router} from './router-alt' 4 | 5 | export const head = (...c) => { 6 | let loaded_once = false 7 | const config = el => loaded_once = true 8 | return m('head', {config, shouldUpdate: el => !el}, c) 9 | } 10 | 11 | // More info: https://developer.chrome.com/multidevice/android/installtohomescreen 12 | export const theme = (color='black') => [ 13 | m('meta', {name:'theme-color', content:color}), 14 | m('meta', {name:'msapplication-TileColor', content:color}), 15 | ] 16 | 17 | export const mobile_metas = (title='', img='icon', manifest='manifest') => [ 18 | m('meta', {charset:'utf8'}), 19 | m('meta', {'http-equiv':'x-ua-compatible', content:'ie=edge'}), 20 | m('meta', {name:"viewport", content:"width=device-width, initial-scale=1.0, shrink-to-fit=no"}), 21 | m('title', title), 22 | ...([ 23 | 'HandheldFriendly,True', 24 | 'MobileOptimized,320', 25 | 'mobile-web-app-capable,yes', 26 | 'apple-mobile-web-app-capable,yes', 27 | `apple-mobile-web-app-title,${title}`, 28 | `msapplication-TileImage,/${img}-144x144.png`, 29 | `msapplication-square70x70logo,/smalltile.png`, 30 | `msapplication-square150x150logo,/mediumtile.png`, 31 | `msapplication-wide310x150logo,/widetile.png`, 32 | `msapplication-square310x310logo,/largetile.png`, 33 | ].map(x => m('meta', {name: x.split(',')[0], content: x.split(',')[1]}))), 34 | // ...([512,180,152,144,120,114,76,72].map(x => 35 | // m('link', {rel: 'apple-touch-icon-precomposed', sizes:`${x}x${x}`, href:`/${img}-${x}x${x}.png`}))), 36 | m('link', {rel: 'apple-touch-icon-precomposed', href:`/${img}-180x180.png`}), 37 | m('link', {rel: 'apple-touch-startup-image', href:`/${img}-startup.png`}), 38 | m('link', {rel: 'shortcut icon', href:`/${img}.ico`, type:'image/x-icon'}), 39 | m('link', {rel: 'manifest', href:`/${manifest}.json`}) 40 | ] 41 | 42 | /** 43 | * Google Analytics 44 | */ 45 | export const googleAnalytics = id => { 46 | const x = () => { 47 | window.ga = window.ga || function(){ 48 | window.ga.q = (window.ga.q || []).push(arguments) 49 | } 50 | let ga = window.ga 51 | ga('create', id, 'auto') 52 | ga('send', 'pageview') 53 | } 54 | return m('script', {config: x, src:'https://www.google-analytics.com/analytics.js', l: 1 * new Date, async: 1}) 55 | } 56 | 57 | // Facebook: https://developers.facebook.com/docs/sharing/webmasters#markup 58 | // Open Graph: http://ogp.me/ 59 | 60 | export const fb_opengraph = (app_id, url, title, img, site_name, author) => [ 61 | `fb:app_id,${app_id}`, 62 | `og:url,${url}`, 63 | `og:type,website`, 64 | `og:title,${title}`, 65 | `og:image,${img}`, 66 | `og:description,${description}`, 67 | `og:site_name,${site_name}`, 68 | `og:locale,en_US`, 69 | `article:author,${author}`, 70 | ].map((x,i,a,p=x.split(',')) => 71 | m('meta', {property:p[0], content:p[1]})) 72 | 73 | export const fb_instantarticle = (article_url, style) => [ 74 | m('meta', {property:"op:markup_version", content:"v1.0"}), 75 | m('link', {rel:"canonical", href:article_url}), 76 | m('meta', {property:"fb:article_style", content:style}) 77 | ] 78 | 79 | // More info: https://dev.twitter.com/cards/getting-started 80 | // Validate: https://dev.twitter.com/docs/cards/validation/validator 81 | export const twitter_card = (summary,site_account,individual_account,url,title,description,image) => [ 82 | `twitter:card,${summary}`, 83 | `twitter:site,@${site_account}`, 84 | `twitter:creator,@${individual_account}`, 85 | `twitter:url,${url}`, 86 | `twitter:title,${title}`, 87 | `twitter:description,${description}`, 88 | `twitter:image,${image}`, 89 | ].map((x,i,a,n=x.split(',')) => m('meta', {name:n[0], content:n[1]})) 90 | 91 | export const google_plus = (page, title, desc, img) => [ 92 | m('link', {href:`https://plus.google.com/+${page}`, rel:'publisher'}), 93 | m('meta', {itemprop:"name", content:title}), 94 | m('meta', {itemprop:"description", content:desc}), 95 | m('meta', {itemprop:"image", content:img}), 96 | ] 97 | 98 | // More info: https://developer.apple.com/safari/library/documentation/appleapplications/reference/safarihtmlref/articles/metatags.html 99 | export const iOS = (app_id,affiliate_id, app_arg, telephone='yes', title) => 100 | [ 101 | // Smart App Banner 102 | `apple-itunes-app,app-id=${app_id},affiliate-data=${affiliate_id},app-argument=${app_arg}`, 103 | 104 | // Disable automatic detection and formatting of possible phone numbers --> 105 | `format-detection,telephone=${telephone}`, 106 | 107 | // Add to Home Screen 108 | `apple-mobile-web-app-capable,yes`, 109 | `apple-mobile-web-app-status-bar-style,black`, 110 | `apple-mobile-web-app-title,${title}`, 111 | ].map((x,i,a,n=x.split(',')) => 112 | m('meta', {name:n[0], content:n[1]})) 113 | 114 | // Pinned Site - Safari 115 | export const safari = (name='icon',color='red') => m('link', {rel:"mask-icon", href:`${name}.svg`, color}) 116 | 117 | // Disable translation prompt 118 | export const chrome = (app_id) => [ 119 | m('link', {rel:"chrome-webstore-item", href:`https://chrome.google.com/webstore/detail/${app_id}`}), 120 | m('meta', {name:'google', value:'notranslate'}) 121 | ] 122 | 123 | export const applinks = (app_store_id, name, android_pkg, docs_url) => [ 124 | // iOS 125 | `al:ios:url,applinks://docs`, 126 | `al:ios:app_store_id,${app_store_id}`, 127 | `al:ios:app_name,${name}`, 128 | // Android 129 | `al:android:url,applinks://docs`, 130 | `al:android:app_name,${name}`, 131 | `al:android:package,${android_pkg}`, 132 | // Web Fallback 133 | `al:web:url,${docs_url}`, 134 | // More info: http://applinks.org/documentation/ 135 | ].map((x,i,a,n=x.split(',')) => m('meta', {property:n[0], content:n[1]})) 136 | 137 | /** 138 | * monitors scrolling to indicate if an element is visible within the viewport 139 | */ 140 | 141 | /* 142 | likely include with your SCSS for a project, that makes these styles hide/show the element: 143 | .invisible { 144 | opacity: 0; 145 | transition-delay: .5s; 146 | transition-duration: .5s; 147 | &.visible { 148 | opacity: 1; 149 | } 150 | } 151 | */ 152 | 153 | export const viewportHeight = _ => Math.max(document.documentElement.clientHeight, window.innerHeight || 0) 154 | 155 | export const trackVisibility = component => { 156 | let el, 157 | visible = (el.getBoundingClientRect instanceof Function) ? false : true 158 | 159 | const onScroll = debounce(ev => { 160 | if(!el.getBoundingClientRect instanceof Function) return 161 | let {top, height} = el.getBoundingClientRect(), 162 | vh = viewportHeight() 163 | if(top <= vh && !visible) { 164 | el.className += ' visible' 165 | visible = true 166 | } else if(top > vh && visible) { 167 | el.className = el.className.replace(/ visible/g, '') 168 | visible = false 169 | } 170 | }, 16.6) 171 | 172 | const startScroll = _el => { 173 | el = _el 174 | window.addEventListener('scroll', onScroll) 175 | } 176 | 177 | const endScroll = _ => window.removeEventListener('scroll', onScroll) 178 | 179 | rAF(onScroll) 180 | 181 | return m('div.invisible', {config: startScroll, unload: endScroll}, component) 182 | } 183 | 184 | /** 185 | * MARKDEEP / MARKDOWN - convert a section with string content 186 | * into a markdeep/markdown rendered section 187 | */ 188 | 189 | export const markdown = (content, markdownToHtml=(c => global.markdeep.format(c))) => { 190 | const config = (element, init) => { 191 | element.innerHTML = markdownToHtml(content) 192 | } 193 | return m('.markdeep', {config}) 194 | } 195 | 196 | /** 197 | * scrambled text animation 198 | * 199 | * m('span', {config: animatingTextConfig('test me out')}) 200 | */ 201 | export const chars = '#*^-+=!f0123456789_' 202 | export const scramble = (str, from=0) => 203 | str.slice(0,from) + str.slice(from).split('').map(x => x === ' ' ? x : chars[range(0,chars.length)]).join('') 204 | export const range = (min,max) => Math.floor(Math.random()*(max-min)+min) 205 | export const wait = ms => new Promise((res) => setTimeout(res, ms)) 206 | 207 | export const scrambler = (str, interval=33, i=0, delay=0) => el => { 208 | let start = scramble(str, 0) 209 | const draw = i => () => el.innerText = str.slice(0,i)+start.slice(i) 210 | while(i++ < str.length){ 211 | wait(delay+i*interval).then(draw(i)) 212 | } 213 | } 214 | 215 | /** 216 | * load an image in JS, and then animate it in as a background image 217 | * 218 | * imageLoader(url, m('div')) 219 | */ 220 | export const imageLoader = (url, comp) => { 221 | let x = comp, 222 | image, 223 | loaded = false 224 | 225 | while(x instanceof Function) 226 | x = x() 227 | 228 | const imgConfig = el => { 229 | image = new Image() 230 | 231 | el.style.backgroundImage = `url(${url})` 232 | 233 | const done = ev => { 234 | if(loaded) return 235 | el.className += ' loaded' 236 | loaded = true 237 | } 238 | 239 | image.onload = done 240 | image.src = url 241 | } 242 | 243 | x.config = imgConfig 244 | return x 245 | } 246 | 247 | /** 248 | * load an SVG and inject it onto the page 249 | */ 250 | export const loadSVG = url => fetch(url).then(r => r.text()) 251 | export const injectSVG = url => container(data => 252 | m('div', {config: el => el.innerHTML = data.svg}), 253 | {svg: loadSVG.bind(null, url)}) 254 | 255 | /** 256 | * hashroute-driven router 257 | */ 258 | // router implementation 259 | export const hashrouter = ( 260 | routes={}, 261 | def='#', 262 | current 263 | ) => { 264 | let x = router(routes, _vdom => { 265 | current = _vdom 266 | update() 267 | }) 268 | x.listen() 269 | x.trigger((window.location.hash || def).slice(1)) 270 | return () => current 271 | } 272 | 273 | /** 274 | * page component that returns an entire html component 275 | */ 276 | export const page = (main, title, css='/style.css', googleAnalyticsId) => [ 277 | head( 278 | theme(), 279 | mobile_metas(title), 280 | m('link', {type:'text/css', rel:'stylesheet', href:css}), 281 | googleAnalyticsId && googleAnalytics(googleAnalyticsId) 282 | ), 283 | m('body', main) 284 | ] 285 | 286 | /** 287 | * mount the entire page() component to the DOM 288 | */ 289 | export const app = (routes, def, title, css, analyticsId) => { 290 | const router = hashrouter(routes, def) 291 | const p = page(router, title, css, analyticsId) 292 | return () => mount(p, qs('html', document)) 293 | } -------------------------------------------------------------------------------- /src/csp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to CSP in JS! 3 | * 4 | * This is an implementation of Go-style coroutines that access a hidden, 5 | * shared channel for putting data into, and taking it out of, a system. 6 | * 7 | * Channels, in this case, can be a set (for unique values), an array 8 | * (as a stack or a queue), or even some kind of persistent data structure. 9 | * 10 | * CSP (especially in functional platforms like ClojureScript, where the 11 | * `core.async` library provides asynchronous, immutable data-structures) 12 | * typically operates through two operations (overly simplified here): 13 | * 14 | * (1) put(...a) : put a list of items into the channel 15 | * (2) take(x) : take x items from the channel 16 | * 17 | * This implementation uses ES6 generators (and other ES6 features), which are basically functions that 18 | * can return more than one value, and pause after each value yielded. 19 | * 20 | * 21 | */ 22 | 23 | const raf = cb => requestAnimationFrame ? requestAnimationFrame(cb) : setTimeout(cb,0) 24 | 25 | export const channel = () => { 26 | 27 | let c = [], // channel data is a queue; first in, first out 28 | channel_closed = false, // is channel closed? 29 | runners = [] // list of iterators to run through 30 | 31 | const not = (c, b) => c.filter(a => a !== b), // filter for "not b" 32 | each = (c, fn) => c.forEach(fn) // forEach... 33 | 34 | const put = (...vals) => { // put(1,2,3) 35 | c = [...vals, ...c] // inserts vals to the front of c 36 | return ["park", ...c] // park this iterator with this data 37 | }, 38 | take = (x=1, taker=(...vals)=>vals) => { // take(numItems, mapper) 39 | c = taker(...c) // map/filter for certain values 40 | let diff = c.length - x // get the last x items 41 | if(diff < 0) return ['park'] 42 | const vals = c.slice(diff).reverse() // last x items 43 | c = c.slice(0, diff) // remove those x items from channel 44 | return [ vals.length !== 0 ? 'continue' : 'park', ...vals ] // pipe dat aout from channel 45 | }, 46 | awake = (run) => each(not(runners, run), a => a()), // awake other runners 47 | status = (next, run) => { // iterator status, runner => run others if not done 48 | const {done, value} = next 49 | if(done) runners = not(runners, run) // if iterator done, filter it out 50 | return value || ['park'] // return value (i.e. [state, ...nums]) or default ['park'] 51 | }, 52 | actor = iter => { // actor returns a runner that next()'s the iterator 53 | let prev = [] 54 | 55 | const runner = () => { 56 | if(channel_closed) 57 | return (runners = []) // channel closed? delete runners 58 | 59 | const [state, ...vals] = 60 | status(iter.next(prev), runner) // pass values to iterator, iter.next(), and store those new vals from status() 61 | 62 | prev = vals; // store new vals 63 | // raf 64 | ((state === 'continue') ? runner : cb)() // if continue, keep running, else awaken all others except runner 65 | } 66 | 67 | const cb = awake.bind(null, runner) // awake all runners except runner 68 | 69 | return runner 70 | }, 71 | spawn = gen => { 72 | const runner = actor(gen(put, take)) 73 | runners = [...runners, runner] 74 | runner() 75 | } 76 | 77 | return { 78 | spawn, 79 | close: () => { 80 | channel_closed = true 81 | } 82 | } 83 | } 84 | 85 | /** 86 | API 87 | 88 | channel() 89 | channel.spawn(*function(put, take){...}) -- takes a generator that receives a put and take function 90 | channel.close() -- closes the channel, stops all operations and reclaims memory (one line cleanup!!) 91 | **/ 92 | 93 | /* 94 | let x = channel() // create new channel() 95 | 96 | // for any value in the channel, pull it and log it 97 | x.spawn( function* (put, take) { 98 | while(true){ 99 | let [status, ...vals] = yield take(1, (...vals) => 100 | vals.filter(x => 101 | typeof x === 'number' && x%2===0)) 102 | // if not 10 items available, actor parks, waiting to be signalled again, and also find just evens 103 | 104 | if(vals.length === 1) log(`-------------------taking: ${vals}`) 105 | } 106 | }) 107 | 108 | // put each item in fibonnaci sequence, one at a time 109 | x.spawn( function* (put, take) { 110 | let [x, y] = [0, 1], 111 | next = x+y 112 | 113 | for(var i = 0; i < 30; i++) { 114 | next = x+y 115 | log(`putting: ${next}`) 116 | yield put(next) 117 | x = y 118 | y = next 119 | } 120 | }) 121 | 122 | // immediately, and every .5 seconds, put the date/time into channel 123 | x.spawn(function* insertDate(p, t) { 124 | while(true){ 125 | yield p(new Date) 126 | } 127 | }) 128 | 129 | // close the channel and remove all memory references. Pow! one-line cleanup. 130 | setTimeout(() => x.close(), 2500) 131 | */ 132 | 133 | export const fromEvent = (obj, events, c=channel(), fn=e=>e) => { 134 | if(!obj.addEventListener) return 135 | if(!(typeof events === 'string') || !events.length) return 136 | events = events.split(',').map(x => x.trim()).forEach(x => { 137 | obj.addEventListener(x, e => { 138 | c.spawn(function* (put, take){ 139 | yield put(fn(e)) 140 | }) 141 | }) 142 | }) 143 | return c 144 | } 145 | 146 | /* 147 | let c1 = fromEvent(document.body, 'mousemove') 148 | c1.spawn(function* (p,t){ 149 | while(true) log(yield t(1)) 150 | }) 151 | */ 152 | 153 | export const conj = (...channels) => { 154 | const x = channel(), 155 | send = (...vals) => 156 | x.spawn(function* (p,t){ p(...vals) }) 157 | 158 | channels.forEach(y => 159 | y.spawn(function*(p,t){ 160 | while(true){ 161 | let [status, val] = t() 162 | yield (val && send(val)) 163 | } 164 | })) 165 | 166 | return x 167 | } 168 | 169 | -------------------------------------------------------------------------------- /src/fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | The `fetch()` module batches in-flight requests, so if at any point in time, anywhere in my front-end or back-end application I have a calls occur to `fetch('http://api.github.com/users/matthiasak')` while another to that URL is "in-flight", the Promise returned by both of those calls will be resolved by a single network request. 3 | */ 4 | 5 | 6 | /** 7 | * batches in-flight requests into the same request object 8 | * 9 | * f should be a function with this signature: 10 | * 11 | * f: function(url,options): Promise 12 | */ 13 | export const batch = f => { 14 | let inflight = {} 15 | 16 | return (url, options={}) => { 17 | let {method} = options, 18 | key = `${url}:${JSON.stringify(options)}` 19 | 20 | if((method || '').toLowerCase() === 'post') 21 | return f(url, {...options, compress: false}) 22 | 23 | return inflight[key] || 24 | (inflight[key] = 25 | new Promise((res,rej) => { 26 | f(url, {...options, compress: false}) 27 | .then(d => res(d)) 28 | .catch(e => rej(e)) 29 | }) 30 | .then(data => { 31 | inflight = {...inflight, [key]: undefined} 32 | return data 33 | }) 34 | .catch(e => 35 | console.error(e, url))) 36 | } 37 | } 38 | 39 | // a simple wrapper around fetch() 40 | // that enables a Promise to be cancelled (sort of) 41 | // -- 42 | // use this until Promise#abort() is a method, or the WHATWG figures 43 | // out a proper approach/implementation 44 | require('isomorphic-fetch') 45 | export const cancellable = f => 46 | (...args) => { 47 | let result = f(...args), 48 | aborted = false 49 | 50 | let promise = new Promise((res,rej) => { 51 | result 52 | .then(d => aborted ? rej('aborted') : res(d)) 53 | .catch(e => rej(e)) 54 | }) 55 | 56 | promise.abort = () => aborted = true 57 | 58 | return promise } 59 | 60 | export const whatWGFetch = (...args) => 61 | global.fetch(...args) 62 | .then(r => r.json()) 63 | 64 | export const cacheable = (fetch=whatWGFetch, duration=1000*60*5) => { 65 | let cache = {} 66 | return (url,opts) => { 67 | let c = cache[url] 68 | if(c && (c.timestamp + duration >= +new Date)) 69 | return Promise.resolve(c.result) 70 | 71 | return fetch(url,opts).then(result => { 72 | cache[url] = {result, timestamp: +new Date} 73 | return result 74 | }) 75 | } 76 | } 77 | 78 | export const fetch = cancellable(batch(whatWGFetch)) 79 | 80 | // !! usage 81 | // let batching_fetcher = batch(fetch) // fetch API from require('isomorphic-fetch') 82 | // 83 | // !! fetch has the signature of --> function(url:string, options:{}): Promise --> which matches the spec 84 | // !! wrapper functions for database drivers or even $.ajax could even be written to use those instead of 85 | // !! the native fetch() 86 | // 87 | // let url = 'http://api.github.com/user/matthiasak', 88 | // log(data => console.log(data)) 89 | // 90 | // !! the following only sends one network request, because the first request 91 | // !! shares the same URL and would not yet have finished 92 | // 93 | // batching_fetcher(url).then(log) //--> {Object} 94 | // batching_fetcher(url).then(log) //--> {Object} 95 | // 96 | // !! we can pass any number of options to a batched function, that does anything, 97 | // !! as long as it returns a promise 98 | // 99 | // !! by default, POSTs are not batched, whereas GETs are. Clone the repo and modify to your needs. -------------------------------------------------------------------------------- /src/fp.js: -------------------------------------------------------------------------------- 1 | export const clone = obj => 2 | JSON.parse(JSON.stringify(obj)) 3 | 4 | export const eq = (a,b) => { 5 | if(a === undefined || b === undefined) return false 6 | return JSON.stringify(a) === JSON.stringify(b) 7 | } 8 | 9 | export const each = (arr,fn) => { 10 | for(var i = 0, len = arr.length; i { 15 | let result = [] 16 | each(arr, (...args) => { 17 | result = result.concat(fn(...args)) 18 | }) 19 | return result 20 | } 21 | 22 | export const reduce = (arr, fn, acc) => { 23 | arr = clone(arr) 24 | acc = acc !== undefined ? acc : arr.shift() 25 | each(arr, (v,i,arr) => { 26 | acc = fn(acc,v,i,arr) 27 | }) 28 | return acc 29 | } 30 | 31 | export const filter = (arr, fn) => 32 | reduce(arr, (acc,v,i,arr) => 33 | fn(v,i,arr) ? [...acc, v] : acc, []) 34 | 35 | export const where = (arr, fn) => 36 | filter(arr, fn)[0] || null 37 | 38 | export const pluck = (keys=[],obj={}) => 39 | reduce( 40 | filter(Object.keys(obj), v => keys.indexOf(v) !== -1 && !!obj[v]), 41 | (a,v) => { return {...a, [v]: obj[v]} }, 42 | {} 43 | ) 44 | 45 | export const debounce = (func, wait) => { 46 | let timeout = null, 47 | calls = 0 48 | return (...args) => { 49 | clearTimeout(timeout) 50 | timeout = setTimeout(() => { 51 | timeout = null 52 | func(...args) 53 | }, wait) 54 | } 55 | } 56 | 57 | 58 | export const concat = (arr, v) => 59 | arr.concat([v]) 60 | 61 | export const concatAll = arr => 62 | reduce(arr, (acc,v,i,arr) => 63 | acc.concat(v), []) 64 | 65 | /** 66 | * Function composition 67 | * @param ...fs functions to compose 68 | * @return composed function 69 | **/ 70 | export const compose = (...fs) => 71 | (...args) => 72 | fs.reduce((g, f) => 73 | [f(...g)], args)[0] 74 | 75 | /** example */ 76 | /* 77 | const ident = x => x, 78 | inc = x => x+1, 79 | dec = x => x-1 80 | 81 | const same = comp(inc, dec, ident) 82 | log(same(1,2,3,4,5)) 83 | */ 84 | 85 | export const mapping = (mapper) => // mapper: x -> y 86 | (reducer) => // reducer: (state, value) -> new state 87 | (result, value) => 88 | reducer(result, mapper(value)) 89 | 90 | export const filtering = (predicate) => // predicate: x -> true/false 91 | (reducer) => // reducer: (state, value) -> new state 92 | (result, value) => 93 | predicate(value) ? reducer(result, value) : result 94 | 95 | export const concatter = (thing, value) => 96 | thing.concat([value]) 97 | 98 | // example transducer usage: 99 | // const inc = x => x+1 100 | // const greaterThanTwo = x => x>2 101 | // const incGreaterThanTwo = compose( 102 | // mapping(inc), 103 | // filtering(greaterThanTwo) 104 | // ) 105 | // reduce([1,2,3,4], incGreaterThanTwo(concat), []) // => [3,4,5] 106 | 107 | -------------------------------------------------------------------------------- /src/hamt.js: -------------------------------------------------------------------------------- 1 | // simple fn that returns a map node 2 | const node = (val=undefined) => { 3 | let result = {} 4 | if(val) result.val = val 5 | return result 6 | } 7 | 8 | // compute the hamming weight 9 | const hamweight = (x) => { 10 | x -= ((x >> 1) & 0x55555555) 11 | x = (x & 0x33333333) + ((x >> 2) & 0x33333333) 12 | x = (x + (x >> 4)) & 0x0f0f0f0f 13 | x += (x >> 8) 14 | x += (x >> 16) 15 | return (x & 0x7f) 16 | } 17 | 18 | // hash fn 19 | export const hash = str => { 20 | if(typeof str !== 'string') str = JSON.stringify(str) 21 | const type = typeof str 22 | if (type === 'number') return str 23 | if (type !== 'string') str += '' 24 | 25 | let hash = 0 26 | for (let i = 0, len = str.length; i < len; ++i) { 27 | const c = str.charCodeAt(i) 28 | hash = (((hash << 5) - hash) + c) | 0 29 | } 30 | return hash 31 | } 32 | 33 | // compare two hashes 34 | const comp = (a,b) => hash(a) === hash(b) 35 | 36 | // get a sub bit vector 37 | const frag = (h=0, i=0, range=8) => 38 | (h >>> (range*i)) & ((1 << range) - 1) 39 | 40 | // const toBitmap = x => 1 << x 41 | // const fromBitmap = (bitmap, bit) => popcount(bitmap & (bit - 1)) 42 | const bin = x => (x).toString(2) 43 | 44 | // clone a node 45 | export const replicate = (o, h) => { 46 | let n = node() 47 | for(var x=0, _o=o, _n=n; x < 4; x++){ 48 | for(let i in _o){ 49 | if(i !== 'val' && _o.hasOwnProperty(i)){ 50 | _n[i] = _o[i] // point n[i] to o[i] 51 | } 52 | } 53 | 54 | let __n = node(), 55 | f = frag(h, x) 56 | 57 | _n[f] = __n 58 | _n = __n 59 | _o = _o[f] === undefined ? {} : _o[f] 60 | } 61 | return n 62 | } 63 | 64 | export const set = (m, key, val) => { 65 | let json = JSON.stringify(val), 66 | h = hash(key), 67 | n = get(m, key) 68 | 69 | if((n === undefined) || !comp(n, val)){ 70 | // in deepest level (3), need to create path down to make this change 71 | let r = replicate(m, h) // new subtree 72 | for(var i=0, _r=r; i<4; i++) _r = _r[frag(h,i)] 73 | _r.val = val 74 | return r 75 | } 76 | 77 | // else the hash came out to be the same, do nothing 78 | return m 79 | } 80 | 81 | export const unset = (m, key) => { 82 | let h = hash(key), 83 | r = replicate(m, h) // new subtree 84 | for(var i=0, _r=r; i<3; i++) _r = _r[frag(h,i)] 85 | _r[frag(h,3)] = undefined 86 | return r 87 | } 88 | 89 | export const get = (m, key) => { 90 | let h = hash(key) 91 | for(var i = 0, _r = m; i < 4; i++){ 92 | _r = _r[frag(h,i)] 93 | if(!_r) return undefined 94 | } 95 | return _r.val 96 | } 97 | 98 | export const hashmap = (initial = {}) => { 99 | let result = node() 100 | for(let i in initial){ 101 | if(initial.hasOwnProperty(i)) 102 | result = set(result, i, initial[i]) 103 | } 104 | return result 105 | } 106 | 107 | export const list = (initial = []) => { 108 | let result = node() 109 | for(let i = 0, len = initial.length; i < len; i++){ 110 | result = set(result, i, initial[i]) 111 | } 112 | return result 113 | } 114 | 115 | const iter = (hashmap,items=[]) => { 116 | for(let i in hashmap){ 117 | if(hashmap.hasOwnProperty(i)){ 118 | if(i !== 'val'){ 119 | iter(hashmap[i], items) 120 | } else { 121 | items.push(hashmap[i]) 122 | } 123 | } 124 | } 125 | return items 126 | } 127 | 128 | const identity = x=>x 129 | 130 | export const map = (hashmap, fn=identity, it=iter) => { 131 | let items = it(hashmap), 132 | result = [] 133 | for(let i=0,len=items.length;i { 140 | let list = [] 141 | , i = 0 142 | , v 143 | 144 | while((v = get(hashmap, i++)) !== undefined){ 145 | list.push(v) 146 | } 147 | 148 | return list 149 | } 150 | 151 | export const reduce = (hashmap, fn, acc, it=iter) => { 152 | let items = it(hashmap) 153 | acc = (acc === undefined) ? items.shift() : acc 154 | for(let i=0,len=items.length;i i+1)) 165 | , a = hashmap(Array(30).fill(true).reduce((a,x,i) => { 166 | a[i] = i+1 167 | return a 168 | }, {})) 169 | 170 | log( 171 | get(x, 'hello')+'', 172 | x, 173 | unset(x, 'hello'), 174 | get(x, 'goodbye')+'', 175 | get(y, 'hello')+'', 176 | get(y, 'goodbye')+'', 177 | x===y, 178 | comp(a,z), 179 | // map(a, x=>x+1), 180 | // map(z, x=>x+1), 181 | reduce(a, (acc,x)=>acc+x, 0), 182 | reduce(a, (acc,x)=>acc+x, 0) 183 | ) 184 | */ -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * as fetch from './fetch' 2 | export * as store from './store' 3 | export * as resource from './resource' 4 | export * as cache from './cache' 5 | export * as router from './router-alt' 6 | export * as mux from './mux' 7 | export * as csp from './csp' 8 | export * as vdom from './vdom' 9 | export * as components from './components' 10 | export * as fp from './fp' 11 | export * as ot from './ot' 12 | export * as lazy from './lazy' 13 | export * as hamt from './hamt' 14 | export * as model from './model' 15 | export * as meta from './meta' 16 | -------------------------------------------------------------------------------- /src/lazy.js: -------------------------------------------------------------------------------- 1 | const flatten = (...a) => 2 | a.reduce((a,v) => { 3 | if(v instanceof Array) 4 | return [...a, ...flatten(...v)] 5 | return a.concat(v) 6 | }, []) 7 | 8 | export const iter = (...a) => 9 | wrap(function*(){ 10 | let b = flatten(a) 11 | for(let i = 0, len = b.length; i 16 | wrap(function*(){ 17 | let i = start 18 | while(end === undefined || i <= end){ 19 | yield i 20 | i += step 21 | } 22 | }) 23 | 24 | export const lmap = (gen, fn) => 25 | wrap(function*(){ 26 | for(let x of gen()) 27 | yield fn(x) 28 | }) 29 | 30 | export const lfilter = (gen, fn) => 31 | wrap(function*(){ 32 | for(let x of gen()) { 33 | if(fn(x)) yield x 34 | } 35 | }) 36 | 37 | export const take = (gen, num) => 38 | wrap(function*(){ 39 | let it = gen() 40 | for(let i = 0; i { 46 | let x = [] 47 | for(let i of gen()) 48 | x.push(i) 49 | return x 50 | } 51 | 52 | export const wrap = gen => { 53 | let g = function*(){ 54 | yield* gen() 55 | } 56 | return [value,lmap,lfilter,take].reduce((g,v) => { 57 | g[fnName(v)] = v.bind(null, gen) 58 | return g 59 | }, g) 60 | } 61 | 62 | export const fnName = fn => 63 | /^function (\w+)/.exec(fn+'')[1] 64 | 65 | // create a generator that will step through each item (finite sequence) 66 | // let test = iter(1,2,3,4,5,6,7,8,9,10) 67 | // log(test.value()) // accumulate the output with gen.value() 68 | // log(value(test)) // ... or value(gen) 69 | 70 | // ... or pass in an array, or any combination of values 71 | // let test2 = iter(1,2,3,[4,5,6],[[7,8,9,[10]]]) 72 | // log( test2.value() ) 73 | 74 | // lazily evaluate items with lmap/lfilter 75 | // log( lmap(test, x => x * 2).value() ) 76 | // log( lfilter(test, x => x < 7).value() ) 77 | 78 | // chain lazy operations together 79 | // ... via traditional passing 80 | // log( value(take(lfilter(lmap(test, x=>2*x), x=>x>=10), 2)) ) 81 | // ... or via chaining 82 | // log( test.lmap(x=>2*x).lfilter(x => x>10).value() ) 83 | 84 | // any operation can be told to do "just enough work", or all of it 85 | // log( test.lmap(x => 2*x).lfilter(x => x<10).value() ) // calculates 4 results, returns array of 4 86 | // log( test.lmap(x => 2*x).value().slice(0,4) ) // calculates 10 results, returns array of 4 87 | // log( test.lmap(x => 2*x).lfilter(x => x<10).take(2).value() ) // only calculates 2 items 88 | 89 | // you don't have to think in finite series / arrays 90 | // log( seq(0, 2).lmap(x => Math.pow(x,2)).take(20).value() ) 91 | 92 | // const seqFrom = fn => { 93 | // let g = [] 94 | // fn(val => g.unshift(val)) 95 | // return wrap(function*(){ 96 | // // while(true){ 97 | // // yield g.pop() 98 | // } 99 | // }) 100 | // } 101 | 102 | // let mouse = seqFrom(fn => 103 | // window.addEventListener('mousemove', ({screenX:x, screenY:y}) => 104 | // fn([x,y]))) 105 | 106 | // log(mouse.take(100).value()) -------------------------------------------------------------------------------- /src/meta.js: -------------------------------------------------------------------------------- 1 | export const mixin = (...classes) => { 2 | class _mixin {} 3 | let proto = _mixin.prototype 4 | 5 | classes.map(({prototype:p}) => { 6 | Object.getOwnPropertyNames(p).map(key => { 7 | let oldFn = proto[key] || (() => {}) 8 | proto[key] = (...args) => { 9 | oldFn(...args) 10 | return p[key](...args) 11 | } 12 | }) 13 | }) 14 | 15 | return _mixin 16 | } -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | export const is = (type, value) => { 2 | if(type && type.isValid instanceof Function){ 3 | return type.isValid(value) 4 | } else if((type === String && ((value instanceof String) || typeof value === 'string')) 5 | || (type === Number && ((value instanceof Number) || typeof value === 'number')) 6 | || (type === Boolean && ((value instanceof Boolean) || typeof value === 'boolean')) 7 | || (type === Function && ((value instanceof Function) || typeof value === 'function')) 8 | || (type === Object && ((value instanceof Object) || typeof value === 'object')) 9 | || (type === undefined) 10 | ){ 11 | return true 12 | } 13 | 14 | return false 15 | } 16 | 17 | const check = (types, required, data) => { 18 | Object.keys(types).forEach(key => { 19 | let t = types[key], 20 | value = data[key] 21 | 22 | if(required[key] || value !== undefined){ 23 | if(!(t instanceof Array)) t = [t] 24 | 25 | let i = t.reduce((a,_type) => a || is(_type, value), false) 26 | if(!i) { 27 | throw `{${key}: ${JSON.stringify(value)}} is not one of ${t.map(x => `\n - ${x}`)}` 28 | } 29 | } 30 | }) 31 | 32 | return true 33 | } 34 | 35 | export const Model = (...args) => { 36 | let types, required, logic 37 | args.map(x => { 38 | if(x instanceof Function && !logic){ logic = x } 39 | else if(typeof x === 'object') { 40 | if(!types){ types = x } 41 | else if(!required){ required = x } 42 | } 43 | }) 44 | 45 | const isValid = (data) => { 46 | const pipe = logic ? [check, logic] : [check] 47 | return pipe.reduce((a,v) => a && v(types||{},required||{},data), true) 48 | } 49 | 50 | const whenValid = (data) => new Promise((res,rej) => isValid(data) && res(data)) 51 | 52 | return {isValid, whenValid} 53 | } 54 | 55 | export const ArrayOf = (M) => { 56 | return Model((t,r,data) => { 57 | if(!(data instanceof Array)) throw `${data} not an Array` 58 | data.map(x => { 59 | if(!is(M, x)) 60 | throw `${x} is not a model instance` 61 | }) 62 | return true 63 | }) 64 | } 65 | 66 | /** 67 | Use it 68 | 69 | 70 | // create a Name model with required first/last, 71 | // but optional middle 72 | let Name = Model({ 73 | first: String, 74 | middle: String, 75 | last: String 76 | }, {first:true, last:true}) 77 | 78 | // create a Tags model with extra checks 79 | let Tags = Model((types,required,data) => { 80 | if(!(data instanceof Array)) throw `${data} not an Array` 81 | data.map(x => { 82 | if(!is(String, x)) 83 | throw `[${data}] contains non-String` 84 | }) 85 | return true 86 | }) 87 | 88 | // create a Price model that just has a business logic fn 89 | let Price = Model((t,r,d) => { 90 | return (d instanceof Number || typeof d === 'number') && d !== 0 91 | }) 92 | 93 | // create an Activity model with a required type and name, 94 | // all others optional 95 | let Activity = Model({ 96 | type: [String, Function, Number], 97 | latlng: Array,//LatLng, 98 | title: String, 99 | tags: Tags, 100 | name: Name, 101 | price: Price 102 | }, {name:true, price: true}) 103 | 104 | // create a new Activity instance, throwing errors if there are 105 | // any invalid fields. 106 | let a = { 107 | tags: ['1','2'], 108 | type: 1, 109 | name: {first:'matt',last:'keas'}, 110 | price: 100.43, 111 | url: 'http://www.google.com' 112 | } 113 | Activity.whenValid(a).then(log).catch(e => log(e+'')) 114 | **/ -------------------------------------------------------------------------------- /src/monad.js: -------------------------------------------------------------------------------- 1 | const ident = x => x 2 | const keys = o => Object.keys(o) 3 | const bind = (f,g) => f(g()) 4 | 5 | const of = val => { 6 | let isNothing = !val 7 | let map = (f=ident) => { 8 | if(val instanceof Array) 9 | return isNothing ? of([]) : of(val.filter(x => !x.isNothing).map(f)) 10 | 11 | if(val && typeof val === 'object') 12 | return isNothing ? 13 | of({}) : 14 | of(keys(val).reduce((acc,key) => 15 | ({ ...acc, [key]:f(val[key], key) }), {})) 16 | 17 | return isNothing ? of(null) : of(f(val)) 18 | } 19 | 20 | return { 21 | map, 22 | isNothing, 23 | val 24 | } 25 | } 26 | 27 | export default of 28 | 29 | // log( 30 | // of(null) 31 | // .map(x => x+1) 32 | // .map(x => x*3) 33 | // .map(x => x*5 + 10+x) 34 | // .map(x => x+' wha?') 35 | // .val+'' 36 | // ) 37 | 38 | // log( 39 | // of([1,2,3]) 40 | // .map(x => x+1) 41 | // .map(x => x*3) 42 | // .map(x => x*5 + 10+x) 43 | // .map(x => x+' wha?') 44 | // .val+'' 45 | // ) 46 | 47 | // log( 48 | // of({matt:28, ian:30, jeremy: 37}) 49 | // .map(x => x+1) 50 | // .map(x => x*3) 51 | // .map(x => x*5 + 10+x) 52 | // .map(x => x+' wha?') 53 | // .val 54 | // ) -------------------------------------------------------------------------------- /src/mux.js: -------------------------------------------------------------------------------- 1 | import {batch, fetch, cancellable} from './fetch' 2 | import {store} from './store' 3 | 4 | const debounce = (func, wait, timeout) => 5 | (...args) => { 6 | const later = () => { 7 | timeout = null 8 | func(...args) 9 | } 10 | clearTimeout(timeout) 11 | timeout = setTimeout(later, wait) 12 | } 13 | 14 | /** 15 | * Muxed/Demuxed requests will involve pipelined, serialized request objects sent along together in an array. 16 | * 17 | * i.e. [ 18 | * {url: '...', {headers:{...}, form:{...}}}, 19 | * {url: '...', {headers:{...}, form:{...}}}, 20 | * {url: '...', {headers:{...}, form:{...}}}, 21 | * ... 22 | * ] 23 | */ 24 | 25 | export const muxer = (batch_url, f=fetch, wait=60, max_buffer_size=8) => { 26 | const payload = store([]) 27 | 28 | // puts url,options,id on payload 29 | const worker = (url, options) => 30 | payload.dispatch((state, next) => 31 | next([...state, {url, options}]) 32 | ).then(state => state.length-1) 33 | 34 | const sendImmediate = () => { 35 | let cbs = callbacks 36 | callbacks = [] 37 | const p = payload.state() 38 | payload.dispatch((state,next) => next(state), []) // reset payload for next batch of requests 39 | f(batch_url, { 40 | method:'POST', 41 | body:JSON.stringify(p), 42 | headers: { 43 | 'Accept': 'application/json', 44 | 'Content-Type': 'application/json' 45 | }, 46 | }) 47 | .then(data => cbs.forEach(cb => cb(data))) // ordered array of requests 48 | } 49 | 50 | // sends payload after `wait` ms 51 | const send = debounce(sendImmediate, wait) 52 | 53 | let callbacks = [] 54 | const queue = cb => { 55 | callbacks.push(cb) 56 | // if(callbacks.length >= max_buffer_size) 57 | // sendImmediate() 58 | // else 59 | send() 60 | } 61 | 62 | const get = (url, options={}) => 63 | // add {url,options} to payload 64 | // resolves to data[index] under assumption the endpoint returns 65 | // data in order it was requested 66 | worker(url, options).then(index => 67 | new Promise(res => 68 | queue(data => 69 | res(data[index])))) 70 | 71 | return cancellable(get) 72 | } 73 | 74 | // example 75 | // ---------- 76 | // # mocked response from server 77 | // const mock = (url,{body}) => { 78 | // return Promise.resolve(JSON.parse(body).map(({url,options:data}) => { 79 | // switch(url) { 80 | // case '/cows': return {name: 'cow', sound: 'moo', data} 81 | // case '/kittens': return {name: 'cat', sound: 'meow', data} 82 | // } 83 | // })) 84 | // } 85 | // 86 | // # create the muxer, pass in a custom fetch 87 | // const uberfetch = muxer('/api/mux', mock) 88 | // uberfetch('/cows', {age: 5}).then(log) 89 | // uberfetch('/cows', {age: 10}).then(log) 90 | // uberfetch('/cows', {age: 15}).then(log) 91 | // uberfetch('/cows', {age: 20}).then(log) 92 | // uberfetch('/cows', {age: 25}).then(log) 93 | // uberfetch('/cows', {age: 50}).then(log) 94 | // uberfetch('/kittens').then(log) 95 | // uberfetch('/kittens', {wantsMilk: true}).then(log) 96 | // uberfetch('/kittens', {scratchedUpMyCouch: true}).then(log) 97 | -------------------------------------------------------------------------------- /src/ot.js: -------------------------------------------------------------------------------- 1 | // inspired by http://operational-transformation.github.io/index.html 2 | 3 | import {clone, eq} from './fp' 4 | 5 | // clones each opchain and sums up retain() indexes 6 | const computeIndices = (...ops) => 7 | ops.map(op => 8 | op.reduce((a,v) => { 9 | if(v.retain) { 10 | v.index = a.reduce((count, i) => count + (i.retain || 0), 0) + v.retain 11 | } 12 | return a.concat(clone(v)) 13 | }, [])) 14 | 15 | export const transform = (_a,_b) => { 16 | // tally retains 17 | let [at,bt] = computeIndices(_a,_b), 18 | res = [], 19 | lastA = null, 20 | lastB = null 21 | 22 | // walk through each opchain and combine them 23 | while(at.length || bt.length){ 24 | let a = at[0], 25 | b = bt[0], 26 | aRetain = a && a.retain !== undefined, 27 | bRetain = b && b.retain !== undefined 28 | 29 | if(a && !aRetain){ 30 | // run until you hit a retain or end 31 | while(a && !aRetain){ 32 | res.push(a) 33 | at.shift() 34 | a = at[0] 35 | aRetain = a && a.retain !== undefined 36 | } 37 | continue 38 | } else if(b && !bRetain){ 39 | // run until you hit a retain or end 40 | while(b && !bRetain){ 41 | res.push(b) 42 | bt.shift() 43 | b = bt[0] 44 | bRetain = b && b.retain !== undefined 45 | } 46 | continue 47 | } 48 | 49 | // now a and b are either retain ops or undefined 50 | 51 | if(a && b){ 52 | let lower = a.index <= b.index ? a : b, 53 | diff = Math.abs(a.index - b.index) 54 | if(lower === a){ 55 | b.retain = diff 56 | res.push(a) 57 | at.shift() 58 | } else { 59 | a.retain = diff 60 | res.push(b) 61 | bt.shift() 62 | } 63 | lastA = a 64 | lastB = b 65 | } else if(!a && b){ 66 | res.push(b) 67 | bt.shift() 68 | lastB = b 69 | } else if(a && !b){ 70 | res.push(a) 71 | at.shift() 72 | lastA = a 73 | } 74 | } 75 | 76 | return res 77 | } 78 | // ops.reduce((a,v) => a.concat(...v), []) 79 | 80 | export const comp = (...ops) => 81 | ops.reduce((a,v) => a.concat(v), []) 82 | 83 | export const insert = (text) => ({ insert: text }) 84 | 85 | export const remove = (text) => ({ remove: text }) 86 | 87 | export const retain = (num) => ({ retain: num }) 88 | 89 | export const apply = (str, ops, _i=0) => { 90 | let r = ops.reduce(({a,i},v) => { 91 | // log({a,i}, v) 92 | 93 | // at position i, insert v into a 94 | if(v.insert !== undefined) { 95 | return { 96 | a: a.slice(0,i) + v.insert + a.slice(i), 97 | i: i+v.insert.length 98 | } 99 | } 100 | 101 | // at position i, remove string v 102 | if(v.remove !== undefined) { 103 | let n = a.slice(i).slice(0, v.remove.length) 104 | if(n !== v.remove) throw `remove error: ${n} not at index i` 105 | return { 106 | a: a.slice(0,i) + a.slice(i+v.remove.length), 107 | i: Math.max(0,i-v.remove.length) 108 | } 109 | } 110 | 111 | // at position i, retain v chars 112 | if(v.retain !== undefined) { 113 | if(i+v.retain > a.length) 114 | throw `retain error: not enough characters in string to retain` 115 | return {a, i:i+v.retain} 116 | } 117 | 118 | throw `unrecognizable op: ${JSON.stringify(v)}` 119 | }, {a:str,i:_i}) 120 | 121 | // uncomment to ensure opchains must represent 122 | // all content within the result 123 | // if(r.i !== r.a.length) 124 | // throw `incomplete operations, expected length ${r.a.length}, but instead is ${r.i}` 125 | 126 | return r.a 127 | } 128 | 129 | /** 130 | 131 | HOW TO USE THIS MICROLIB: 132 | ---------------- 133 | 134 | import {transform, comp, insert, remove, retain, apply} 135 | 136 | 1. setup some operations, i.e. p1 adds '-you-' to a string at spot 2, and p2 adds '-i-' to a string at spot 0 137 | 138 | let p1 = comp( 139 | retain(2), 140 | insert('-you-') 141 | ), 142 | p2 = comp( 143 | insert('-i-'), 144 | retain(2), 145 | insert('-us-') 146 | ) 147 | 148 | 2. observe what a transform operation is: simple arrays of a small object representing how to edit something (replays actions in chronological order) 149 | 150 | log(p1) 151 | log(p2) 152 | 153 | 3. observe how to merge two parallel operations into one single operation chain 154 | 155 | log(transform(p1,p2)) 156 | 157 | 4. apply an "opchain" to a string 158 | 159 | log(apply('me', p1)) 160 | log(apply('me', transform(p1,p2))) 161 | 162 | 5. test out interactions within arbiter (https://goo.gl/2iNxDy) 163 | 164 | const css = ` 165 | form { 166 | padding: 1rem; 167 | } 168 | 169 | textarea { 170 | display: block; 171 | width: 100%; 172 | outline: 1px solid #222; 173 | background: #111; 174 | color: #aaa; 175 | font-family: monospace; 176 | font-weight: 100; 177 | padding: .5rem; 178 | } 179 | ` 180 | 181 | const app = () => { 182 | let u = universalUtils, 183 | {m, mount, update, qs, comp, apply, transform, insert, remove, retain} = u, 184 | stream = '' 185 | 186 | const edit = (val='') => 187 | (e) => { 188 | let start = e.srcElement.selectionStart, 189 | end = e.srcElement.selectionEnd, 190 | {which} = e, 191 | v = e.srcElement.value, 192 | difflen = v.length - val.length 193 | 194 | // log([stream, val, v]) 195 | 196 | if(difflen === 0) { 197 | log('length same, but content may have changed - TODO') 198 | } else if(difflen < 0){ 199 | log('content deleted', [start,end], difflen) 200 | stream = apply(val, comp( 201 | retain(start), 202 | remove(val.slice(start, -1*difflen)) 203 | )) 204 | update() 205 | } else { 206 | log('content added', [start,end], difflen) 207 | let beforeInsert = v.slice(0,start-difflen) 208 | stream = apply(val, comp( 209 | retain(beforeInsert.length), 210 | insert(v.slice(start-difflen,start)) 211 | )) 212 | update() 213 | } 214 | 215 | val = v 216 | } 217 | 218 | const t1 = edit(stream) 219 | 220 | const form = () => { 221 | return [ 222 | m('style', {type: 'text/css', config: el => el.innerText=css}), 223 | m('form', m('textarea', {rows: 5, onkeyup:t1, value:stream, placeholder:'type here'})), 224 | m('form', m('textarea#a', {rows: 5, value:stream})) 225 | ] 226 | } 227 | 228 | mount(form, qs()) 229 | } 230 | require('universal-utils').then(app) 231 | 232 | 6. as you can see, there's a lot of implementation involved in consuming this file; when creating the opchain, we have to re-read the value of the text and generate the array of operations that produced that change, and then send that generated opchain over-the-wire. While complex, this allows us to communicate chronological sequences of action taken by a user as they type up a document, making it possible gracefully handle updates to a shared document (i.e. Google Docs) that many people are simultaneously editing. 233 | 234 | **/ -------------------------------------------------------------------------------- /src/resource.js: -------------------------------------------------------------------------------- 1 | // The `resource()` module is a mechanism that wraps around the previous modules (`fetch()`, `cache()`, `store()`), 2 | // exposing one primary method `get()`. Example code at end of file. 3 | 4 | 5 | import {store as s} from './store' 6 | import {cache} from './cache' 7 | import {batch, fetch as _fetch, cancellable} from './fetch' 8 | 9 | export const resource = (config={}, defaultState={}) => { 10 | 11 | let inflight = {} 12 | 13 | const store = s(defaultState), 14 | {url, fetch, nocache, name, cacheDuration} = config, 15 | f = fetch || _fetch 16 | 17 | const get = (id, ...params) => { 18 | 19 | // generate a key unique to this request for muxing/batching, 20 | // if need be (serialized with the options) 21 | let key = name+':'+JSON.stringify(id)+':'+JSON.stringify(params) 22 | 23 | // get an inflight Promise the resolves to the data, keyed by `id`, 24 | // or create a new one 25 | return inflight[key] || 26 | (inflight[key] = new Promise((res,rej) => 27 | (nocache ? 28 | Promise.reject() : 29 | cache.getItem(key)) 30 | .then(d => res(d)) 31 | .catch(error => 32 | // whatever fetching mechanism is used (batched, muxed, etc) 33 | // send the resourceName, id, params with the request as options. 34 | // if it is going the node server, node (when demuxing) will use the 35 | // extra options to rebuild the URL 36 | // 37 | // in normal URL requests, we can just carry on as normal 38 | f(url(id, ...params), {resourceName: name, id, params}) 39 | .then((d) => { 40 | if(!d) throw `no data returned from ${key}` 41 | return d 42 | }) 43 | .then(d => 44 | store.dispatch((state, next) => { 45 | let _state = {...state, [key]: d} // make new state 46 | inflight = {...inflight, [key]: undefined} // clear in-flight promise 47 | !nocache && cache.setItem(key, d, cacheDuration) 48 | next(_state) // store's new state is _state 49 | }) 50 | .then(state => state[key]) 51 | ) // pipe state[_id] to the call to f() 52 | .then(d => res(d)) // resolve the f(url(id)) 53 | .catch(e => { 54 | inflight = {...inflight, [key]: undefined} // in case of fire... 55 | rej(e) 56 | })) 57 | )) 58 | } 59 | 60 | const clear = (id, ...params) => { 61 | if(!id) { 62 | return cache.clearAll(name+":") 63 | } 64 | 65 | // generate a key unique to this request for muxing/batching, 66 | // if need be (serialized with the options) 67 | let key = name+':'+JSON.stringify(id)+':'+JSON.stringify(params) 68 | return cache.setItem(key, null) 69 | } 70 | 71 | return { name, store, get: cancellable(get), clear } 72 | } 73 | 74 | // !! Example usage 75 | // 76 | // !! isomorphic/universal usage 77 | // const root = global.document ? window.location.origin : 'http://myapiserverurl.com' 78 | // !! browser talks to node server, node server proxies to API 79 | // const isomorphicfetch = require('isomorphic-fetch') 80 | // !! muxer, that uses isomorphic fetch for transport on the client, but straight up isomorphic fetch() on node 81 | // !! browser will send mux'ed requests to be demux'ed at '/mux' 82 | // const fetch = global.document ? mux('/mux', isomorphicfetch) : isomorphicfetch 83 | // const cacheDuration = 2*60*60*1000 // 2 hours 84 | // !! url functions simply return a string, a call to resource.get(...args) will make a request to url(...args) 85 | // !! imagine LOCATION.location() and SEARCH.search() exist and return strings, too 86 | // const RESOURCES = { 87 | // PROPERTY: resource({ name: 'PROPERTY', fetch, cacheDuration, url: id => `props/${id}` }), 88 | // CATALOG: resource({ name: 'CATALOG', fetch, cacheDuration, url: id => `catalogs/${id}` }), 89 | // LOCATION: resource({ name: 'LOCATION', fetch, cacheDuration, url: (...args) => LOCATION.location(...args) }), 90 | // PRICE: resource({ name: 'PRICE', fetch, cacheDuration, url: id => `prices/${id}` }), 91 | // SEARCH: resource({ name: 'SEARCH', fetch, cacheDuration, nocache: true // don't cache requests to this API 92 | // url: (id, {sort, page, pageSize, hash, ...extraQueryParams}) => SEARCH.search( id, hash, sort, page, pageSize ) 93 | // }) 94 | // } 95 | // !! use it 96 | // RESOURCES.PROPERTY.get(123).then(property => ... draw some React component?) 97 | // RESOURCES.CATALOG.get(71012).then(catalog => ... draw some React component?) 98 | // RESOURCES.LOCATION.get('Houston', 'TX', 77006).then(price => ... draw some React component?) 99 | // !! all of the above are separate requests, but they are sent as A SINGLE REQUEST to /mux in the browser, and sent to the actual API in node 100 | // !! you can also choose to have the browser AND node send mux'ed requests by making the fetch() just be isomorphic fetch, if the node server isn't the API server and your API sever supports it -------------------------------------------------------------------------------- /src/router-alt.js: -------------------------------------------------------------------------------- 1 | const trim = str => str.replace(/^[\s]+/ig, '').replace(/[\s]+$/ig, '') 2 | 3 | export const router = (routes, fn=(a,b)=>a(b)) => { 4 | let current = null 5 | 6 | const listen = (onError) => { 7 | window.addEventListener('hashchange', () => { 8 | if(!trigger(window.location.hash.slice(1))){ 9 | (onError instanceof Function) && onError(window.location.hash.slice(1)) 10 | } 11 | }) 12 | } 13 | 14 | const trigger = path => { 15 | for(let x in routes){ 16 | if(routes.hasOwnProperty(x)){ 17 | let v = match(x,path) 18 | if(v){ 19 | fn(routes[x], v) 20 | return true 21 | } 22 | } 23 | } 24 | 25 | return false 26 | } 27 | 28 | const match = (pattern, path) => { 29 | let _patterns = pattern.split('/'), 30 | parts = _patterns.map(x => { 31 | switch(x[0]){ 32 | case ':': return '([^/]+)' 33 | case '*': return '.*' 34 | default: return x 35 | } 36 | }), 37 | uris = path.split('/') 38 | 39 | for(var i = 0; i < Math.max(parts.length, uris.length); i++){ 40 | let p = trim(parts[i]), 41 | u = trim(uris[i]), 42 | v = null 43 | 44 | if(p === '' || u === ''){ 45 | v = p === '' && u === '' 46 | } else { 47 | v = new RegExp(p).exec(u) 48 | } 49 | 50 | if(!v) return false 51 | } 52 | 53 | return parts.reduce((a,v,i) => { 54 | if(v[0] === ':') 55 | return {...a, [v]: uris[i]} 56 | return a 57 | }, {}) 58 | } 59 | 60 | return { 61 | add: (name, fn) => !!(routes[name] = fn), 62 | remove: name => !!(delete routes[name]) || true, 63 | listen, 64 | match, 65 | trigger 66 | } 67 | } 68 | 69 | // use a router inside a custom Component in React ... 70 | // const app = () => { 71 | // let [React, DOM] = [react, reactDom], 72 | // {Component} = React 73 | 74 | // class Home extends Component { 75 | // constructor(...a){ 76 | // super(...a) 77 | // } 78 | // render(){ 79 | // return

Home Screen

80 | // } 81 | // } 82 | 83 | // export class Router extends Component { 84 | // constructor(...a){ 85 | // super(...a) 86 | 87 | // let p = this.props 88 | 89 | // this.state = { 90 | // routes: p.routes || {}, 91 | // default: p.default || '/' 92 | // } 93 | 94 | // this.router = router(this.state.routes, (el, props) => { 95 | // this.current = el 96 | // }) 97 | 98 | // this.router.trigger(this.state.default) 99 | // } 100 | // render(){ 101 | // return this.current() 102 | // } 103 | // } 104 | 105 | // DOM.render( 107 | // }}/>, document.body) 108 | // } 109 | 110 | // require('react', 'react-dom').then(app) 111 | 112 | // ... or use router outside of a React Component 113 | // let x = router({ 114 | // '/:x/test/:y' : ({x,y}) => log({x,y}), 115 | // '/': () => log('home screen') 116 | // }) 117 | 118 | // log(x.match('/:x/test/:y', '/anything/test/anything')) // test a route pattern with a route 119 | // x.trigger('/') // trigger the / route 120 | // x.trigger('/hi/test/bye') // any named URI segments will be passed to the route callback -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | // The `router()` module is a simple client-side `hashchange` event router that allows Single Page Apps to effectively map and listen to route changes. For `router()` implementation examples, see the `router.js` file. 2 | // 3 | // Example usage at end of this file. 4 | 5 | export const router = (routes, routeTransform) => { 6 | const hashroutes = Object.keys(routes).map(route => { 7 | let tokens = route.match(/:(\w+)/ig), 8 | handler = routeTransform(routes[route]) 9 | 10 | let regex = (tokens || []).reduce((a,v) => a.replace(v, '([^/])+'), route).replace(/(\*)/ig, '([^/])*') 11 | 12 | return { route, regex, handler } 13 | }) 14 | 15 | // a shortcut method to changing the location hash 16 | const page = (path) => 17 | window.location.hash = path 18 | 19 | // returns true if a route matches a route object, false otherwise 20 | const checkRoute = (hash, routeObj) => { 21 | hash = hash[0] === '#' ? hash.substring(1) : hash 22 | let {route, regex, handler} = routeObj, 23 | reggie = new RegExp(regex, 'ig') 24 | 25 | return hash.match(reggie) 26 | } 27 | 28 | // 1. handles a route change, 29 | // 2. checks for matching routes, 30 | // 3. calls just the first matchign route callback 31 | const handleRoute = () => { 32 | let matched = hashroutes.filter(obj => checkRoute(window.location.hash, obj)), 33 | selected = matched[0] 34 | 35 | if(!selected) return 36 | 37 | let { route, regex, handler, tokens } = selected, 38 | segments = window.location.hash.split('/'), 39 | mappedSegments = route.split('/') 40 | .map(segment => { 41 | let match = segment.match(/(\*)|:(\w+)/ig) 42 | return match && match[0] 43 | }), 44 | routeCtx = segments.reduce((a,v,i) => { 45 | let mappedSegment = mappedSegments[i], 46 | {indices} = a 47 | 48 | if(!mappedSegment) return a 49 | 50 | if(mappedSegment[0] === ':') 51 | mappedSegment = mappedSegment.substring(1) 52 | else if(mappedSegment[0] === '*') { 53 | mappedSegment = indices 54 | indices++ 55 | } 56 | 57 | return {...a, [mappedSegment]: v, indices} 58 | }, {indices:0}) 59 | 60 | handler(routeCtx) 61 | } 62 | 63 | window.addEventListener('hashchange', handleRoute) 64 | window.onload = () => handleRoute() 65 | 66 | return {page} 67 | } 68 | 69 | /** 70 | * EXAMPLE USAGE 71 | */ 72 | 73 | // routes input is an object map, where routes return a function 74 | // User, Playlist, Search, and Home are React Component classes 75 | // ------------------------------------- 76 | // const routes = { 77 | // 'user/:id': () => User, 78 | // 'playlist/:id': () => Playlist, 79 | // 'search/*': () => Search, 80 | // '*': () => Home 81 | // } 82 | 83 | // when routes are handled, the routeCallback is the function/handler from the route map above, 84 | // where any route data will be passed to the function returned by the routeTransform 85 | // 86 | // in this code, I optionall pulled extra data from location.search (query params like ?test=1&name=bob), 87 | // turn it into an object with some other method unquerify(...) ---> { test: '1', name: 'bob' }, 88 | // and pass both the route options and query params as props to the React component 89 | // ------------------------------------- 90 | // const routeTransform = (routeCallback) => 91 | // (ctx) => { 92 | // let options = {...ctx, ...unquerify(window.location.search)} 93 | // ReactDOM.render( 94 | // {React.createElement(routeCallback(), options)}, 95 | // document.querySelector('.container') 96 | // ) 97 | // } 98 | 99 | // start the routing 100 | // ------------------------------------- 101 | // const myRoutes = router(routes, routeTransform) -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | // The `store()` module is a mechanism to store an immutable object that represents state of an application. Any application may have one or more active stores, so there's no limitation from how you use the data. The store itself provides four methods: `state()`, `dispatch(reducer, state)`, `to(cb)`, `remove(cb)`. 2 | // 3 | // 1. The `store.state()` returns a clone of the internal state object, which is simply a pure copy of JSON of the data. Since this uses pure JSON representation in-lieue of actual Tries and immutable data libraries, this keeps the code footprint tiny, but you can only store pure JSON data in the store. 4 | // 2. The `store.to(cb)` will register `cb` as a callback function, invoking `cb(nextState)` whenever the store's state is updated with `store.dispatch()` (`store.remove(cb)` simply does the opposite, removing the callback from the list of event listeners). 5 | // 3. The biggest method implemented by `store()` is `store.dispatch(reducer, state=store.state())`. By default, the second parameter is the existing state of the `store`, but you can override the state object input, if need be. The key here is the redux-inspired `reducer`, which is a function that **you** write that receives two arguments, `state` and `next()`. You should modify the state object somehow, or create a copy, and pass it into `next(state)` to trigger an update to be sent to listener. For example: 6 | // 7 | // ```js 8 | // const logger = (state) => console.log('state changed! -->', state) 9 | // store.to(logger) 10 | // 11 | // store.distpatch((state, next) => { 12 | // setTimeout(() => { 13 | // let timestamp = +new Date 14 | // next({ ...state, timestamp }) 15 | // }, 2000) 16 | // }) 17 | // ``` 18 | 19 | import {cancellable} from './fetch' 20 | const clone = (obj) => JSON.parse(JSON.stringify(obj)) 21 | 22 | /** 23 | * 24 | * Event-driven redux-like updates where we use 25 | * reducer functions to update a singular state object 26 | * contained within the store() 27 | * 28 | * Some limitations: You **must** use plain JS objects and arrays 29 | * with this implementation for serialization and cloning support. 30 | * This could eventually use actual immutable data-structures, but 31 | * an implementation change would be required; however if speed 32 | * or correctness is an issue we can try in the future, as immutable 33 | * libraries use data-structures like tries and the like to reduce 34 | * Garbage Collection and blatant in-memory copies with a "structural 35 | * sharing" technique. 36 | * 37 | * - state() 38 | * - dispatch() 39 | * - to() 40 | * - remove() 41 | */ 42 | 43 | export const store = (state={}) => { 44 | // might have to be changed back to Set() 45 | // if subscribers get garbage collected 46 | // 47 | // WeakSet and WeakMap items are GC'd if 48 | // they have no other reference pointing to them 49 | // other than the WeakMap/WeakSet 50 | let subscribers = new Set(), 51 | actions = {} 52 | 53 | const instance = { 54 | state: () => clone(state), 55 | dispatch: (reducer, _state=instance.state()) => 56 | new Promise((res,rej) => { 57 | const next = (newState) => { 58 | state = clone(newState) 59 | for(var s of subscribers){ 60 | s(clone(state)) 61 | } 62 | res(clone(state)) 63 | } 64 | reducer(_state, next) 65 | }), 66 | to: (sub) => subscribers.add(sub), 67 | remove: (sub) => subscribers.delete(sub) 68 | } 69 | 70 | return { ...instance, dispatch: cancellable(instance.dispatch) } 71 | } 72 | 73 | 74 | /* 75 | // Example usage: 76 | // ---------------- 77 | 78 | let photos = store({photos:[]}) 79 | log(photos.state()) 80 | 81 | const printMe = (state) => { 82 | log('-------------- subscriber called', state) 83 | } 84 | 85 | photos.to(printMe) 86 | photos.to(printMe) // can't have duplicate subscribers, printMe only called once per update 87 | photos.to((state) => log('hi')) 88 | 89 | const addRandomPhoto = (state, next) => { 90 | setTimeout(() => { 91 | state = {...state, photos: state.photos.concat('https://media0.giphy.com/media/hD52jjb1kwmlO/200w.gif')} 92 | next(state) 93 | }, 1000) 94 | } 95 | 96 | setInterval(() => photos.dispatch(addRandomPhoto), 500) 97 | 98 | /// example React Component code 99 | // 100 | // 101 | let update = (state) => this.setState(state) 102 | let componentDidMount = () => { 103 | photos.to(update) 104 | } 105 | let componentWillUnmount = () => { 106 | photos.remove(update) 107 | } 108 | */ -------------------------------------------------------------------------------- /src/vdom.js: -------------------------------------------------------------------------------- 1 | const class_id_regex = () => { 2 | return /[#\.][^#\.]+/ig 3 | }, 4 | tagName_regex = () => { 5 | return /^([^\.#]+)\b/i 6 | } 7 | 8 | const parseSelector = s => { 9 | let test = null, 10 | tagreg = tagName_regex().exec(s), 11 | tag = tagreg && tagreg.slice(1)[0], 12 | reg = class_id_regex(), 13 | vdom = Object.create(null) 14 | 15 | if(tag) s = s.substr(tag.length) 16 | vdom.className = '' 17 | vdom.tag = tag || 'div' 18 | 19 | while((test = reg.exec(s)) !== null){ 20 | test = test[0] 21 | if(test[0] === '.') 22 | vdom.className = (vdom.className+' '+test.substr(1)).trim() 23 | else if(test[0] === '#') 24 | vdom.id = test.substr(1) 25 | } 26 | return vdom 27 | } 28 | 29 | export const debounce = (func, wait, immediate, timeout) => 30 | (...args) => { 31 | let later = () => { 32 | timeout = null 33 | !immediate && func(...args) 34 | } 35 | var callNow = immediate && !timeout 36 | clearTimeout(timeout) 37 | timeout = setTimeout(later, wait || 0) 38 | callNow && func(...args) 39 | } 40 | 41 | const hash = (v,_v=JSON.stringify(v)) => { 42 | let hash = 0 43 | for (let i = 0, len = _v.length; i < len; ++i) { 44 | const c = _v.charCodeAt(i) 45 | hash = (((hash << 5) - hash) + c) | 0 46 | } 47 | return hash 48 | } 49 | 50 | export const m = (selector, attrs=Object.create(null), ...children) => { 51 | if(attrs.tag || !(typeof attrs === 'object') || attrs instanceof Array || attrs instanceof Function){ 52 | if(attrs instanceof Array) children.unshift(...attrs) 53 | else children.unshift(attrs) 54 | attrs = Object.create(null) 55 | } 56 | let vdom = parseSelector(selector) 57 | if(children.length) 58 | vdom.children = children 59 | vdom.attrs = attrs 60 | vdom.shouldUpdate = attrs.shouldUpdate 61 | vdom.unload = attrs.unload 62 | vdom.config = attrs.config 63 | vdom.__hash = hash(vdom) 64 | delete attrs.unload 65 | delete attrs.shouldUpdate 66 | delete attrs.config 67 | return vdom 68 | } 69 | 70 | export const rAF = 71 | global.document && 72 | (requestAnimationFrame || 73 | webkitRequestAnimationFrame || 74 | mozRequestAnimationFrame) || 75 | (cb => setTimeout(cb, 16.6)) 76 | 77 | // creatign html, strip events from DOM element... for now just deleting 78 | const stripEvents = ({attrs}) => { 79 | let a = Object.create(null) 80 | 81 | if(attrs){ 82 | for(var name in attrs){ 83 | if(name[0]==='o'&&name[1]==='n') { 84 | a[name] = attrs[name] 85 | delete attrs[name] 86 | } 87 | } 88 | } 89 | 90 | return a 91 | } 92 | 93 | const applyEvents = (events, el, strip_existing=true) => { 94 | strip_existing && removeEvents(el) 95 | for(var name in events){ 96 | el[name] = events[name] 97 | } 98 | } 99 | 100 | const flatten = (arr, a=[]) => { 101 | for(var i=0,len=arr.length; i 'on'+x) 113 | 114 | const removeEvents = el => { 115 | // strip away event handlers on el, if it exists 116 | if(!el) return; 117 | for(var i in EVENTS){ 118 | el[i] = null 119 | } 120 | } 121 | 122 | let mnt 123 | 124 | export const mount = (fn, el) => { 125 | mnt = [el, fn] 126 | render(fn, el) 127 | } 128 | 129 | const render = debounce((fn, el) => rAF(_ => { 130 | applyUpdates(fn, el.children[0], el) 131 | })) 132 | 133 | export const update = () => { 134 | if(!mnt) return 135 | let [el, fn] = mnt 136 | render(fn, el) 137 | } 138 | 139 | const stylify = style => { 140 | let s = '' 141 | for(var i in style){ 142 | s+=`${i}:${style[i]};` 143 | } 144 | return s 145 | } 146 | 147 | const setAttrs = ({attrs, id, className, __hash},el) => { 148 | if(attrs){ 149 | for(var attr in attrs){ 150 | if(attr === 'style') { 151 | el.style = stylify(attrs[attr]) 152 | } else if(attr === 'innerHTML'){ 153 | rAF(() => el.innerHTML = attrs[attr]) 154 | } else if(attr === 'value'){ 155 | rAF(() => el.value = attrs[attr]) 156 | } else { 157 | el.setAttribute(attr, attrs[attr]) 158 | } 159 | } 160 | } 161 | let _id = attrs.id || id 162 | if(_id) el.id = _id 163 | let _className = ((attrs.className || '') + ' ' + (className || '')).trim() 164 | if(_className) el.className = _className 165 | el.__hash = __hash 166 | } 167 | 168 | // recycle or create a new el 169 | const createTag = (vdom=Object.create(null), el, parent=el&&el.parentElement) => { 170 | let __vdom = vdom 171 | // make text nodes from primitive types 172 | if(typeof vdom !== 'object'){ 173 | let t = document.createTextNode(vdom) 174 | if(el){ 175 | parent.insertBefore(t,el) 176 | removeEl(el) 177 | } else { 178 | parent.appendChild(t) 179 | } 180 | return t 181 | } 182 | 183 | // else make an HTMLElement from "tag" types 184 | let {tag, attrs, id, className, unload, shouldUpdate, config, __hash} = vdom, 185 | shouldExchange = !el || !el.tagName || (tag && el.tagName.toLowerCase() !== tag.toLowerCase()), 186 | _shouldUpdate = !(shouldUpdate instanceof Function) || shouldUpdate(el) 187 | 188 | if(!attrs) return 189 | if(el && (!_shouldUpdate || ((!vdom instanceof Function) && el.__hash === __hash))) { 190 | return 191 | } 192 | 193 | if(shouldExchange){ 194 | let t = document.createElement(tag) 195 | el ? (parent.insertBefore(t, el), removeEl(el)) : parent.appendChild(t) 196 | el = t 197 | } 198 | 199 | setAttrs(vdom, el) 200 | if(el.unload instanceof Function) { 201 | rAF(el.unload) 202 | } 203 | if(unload instanceof Function) { 204 | el.unload = unload 205 | } 206 | applyEvents(stripEvents(vdom), el) 207 | config && rAF(_ => config(el)) 208 | return el 209 | } 210 | 211 | // find parent element, and remove the input element 212 | const removeEl = el => { 213 | if(!el) return 214 | el.parentElement.removeChild(el) 215 | removeEvents(el) 216 | // removed for now, added unload logic to the immediate draw()s 217 | if(el.unload instanceof Function) 218 | el.unload() 219 | } 220 | 221 | const insertAt = (el, parent, i) => { 222 | if(parent.children.length > i) { 223 | let next_sib = parent.children[i] 224 | parent.insertBefore(el, next_sib) 225 | } else { 226 | parent.appendChild(el) 227 | } 228 | } 229 | 230 | const applyUpdates = (vdom, el, parent=el&&el.parentElement) => { 231 | let v = vdom 232 | // if vdom is a function, execute it until it isn't 233 | while(vdom instanceof Function) 234 | vdom = vdom() 235 | 236 | if(!vdom) return 237 | 238 | if(vdom.resolve instanceof Function){ 239 | let i = parent.children.length 240 | return vdom.resolve().then(v => { 241 | if(!el) { 242 | let x = createTag(v, null, parent) 243 | insertAt(x, parent, i) 244 | applyUpdates(v, x, parent) 245 | } else { 246 | applyUpdates(v, el, parent) 247 | } 248 | }) 249 | } 250 | 251 | // create/edit el under parent 252 | let _el = vdom instanceof Array ? parent : createTag(vdom, el, parent) 253 | 254 | if(!_el) return 255 | 256 | if(vdom instanceof Array || vdom.children){ 257 | let vdom_children = flatten(vdom instanceof Array ? vdom : vdom.children), 258 | el_children = vdom instanceof Array ? parent.childNodes : _el.childNodes 259 | 260 | while(el_children.length > vdom_children.length){ 261 | removeEl(el_children[el_children.length-1]) 262 | } 263 | 264 | for(let i=0; i 0){ 269 | removeEl(_el.childNodes[_el.childNodes.length-1]) 270 | } 271 | } 272 | } 273 | 274 | export const qs = (s='body', el=document) => el.querySelector(s) 275 | 276 | const resolver = (states = {}) => { 277 | let promises = [], 278 | done = false 279 | 280 | const _await = (_promises = []) => { 281 | promises = [...promises, ..._promises] 282 | return finish() 283 | } 284 | 285 | const wait = (ms=0) => new Promise(res => setTimeout(res, ms)) 286 | 287 | const isDone = () => done 288 | 289 | const finish = () => { 290 | const total = promises.length 291 | return wait().then(_ => Promise.all(promises)).then(values => { 292 | if(promises.length > total){ 293 | return finish() 294 | } 295 | done = true 296 | return states 297 | }) 298 | } 299 | 300 | const resolve = (props) => { 301 | const keys = Object.keys(props) 302 | if (!keys.length) 303 | return Promise.resolve(true) 304 | 305 | let f = [] 306 | keys.forEach(name => { 307 | let x = props[name] 308 | 309 | while(x instanceof Function) 310 | x = x() 311 | 312 | if(x && x.then instanceof Function) 313 | f.push(x.then(d => states[name] = d)) 314 | }) 315 | 316 | return _await(f) 317 | } 318 | 319 | const getState = () => states 320 | 321 | return { finish, resolve, getState, promises, isDone } 322 | } 323 | 324 | const gs = (view, state) => { 325 | let r = view(state) 326 | while(r instanceof Function) 327 | r = view(instance.getState()) 328 | return r 329 | } 330 | 331 | export const container = (view, queries={}, instance=resolver()) => { 332 | let wrapper_view = state => 333 | instance.isDone() ? view(state) : m('span') 334 | 335 | return () => { 336 | let r = gs(wrapper_view, instance.getState()) 337 | instance.resolve(queries) 338 | 339 | if(r instanceof Array) { 340 | let d = instance.finish().then(_ => 341 | gs(wrapper_view, instance.getState())) 342 | 343 | return r.map((x,i) => { 344 | x.resolve = _ => d.then(vdom => vdom[i]) 345 | return x 346 | }) 347 | } 348 | 349 | r.resolve = _ => instance.finish().then(_ => 350 | gs(wrapper_view, instance.getState())) 351 | 352 | return r 353 | } 354 | } 355 | 356 | const reservedAttrs = ['className','id'] 357 | 358 | const toHTML = _vdom => { 359 | while(_vdom instanceof Function) _vdom = _vdom() 360 | if(_vdom instanceof Array) return new Promise(r => r(html(..._vdom))) 361 | if(!_vdom) return new Promise(r => r('')) 362 | if(typeof _vdom !== 'object') return new Promise(r => r(_vdom)) 363 | return (_vdom.resolve ? _vdom.resolve() : Promise.resolve()).then(vdom => { 364 | if(!vdom) vdom = _vdom 365 | 366 | if(vdom instanceof Array) return new Promise(r => r(html(...vdom))) 367 | 368 | const {tag, id, className, attrs, children, instance} = vdom, 369 | _id = (id || (attrs && attrs.id)) ? ` id="${(id || (attrs && attrs.id) || '')}"` : '', 370 | _class = (className || (attrs && attrs.className)) ? ` class="${((className||'') + ' ' + (attrs.className||'')).trim()}"` : '' 371 | 372 | const events = stripEvents(vdom) 373 | let _attrs = '', 374 | inner = '' 375 | for(var i in (attrs || Object.create(null))){ 376 | if(i === 'style'){ 377 | _attrs += ` style="${stylify(attrs[i])}"` 378 | } else if(i === 'innerHTML') { 379 | inner = attrs[i] 380 | } else if(reservedAttrs.indexOf(i) === -1){ 381 | _attrs += ` ${i}="${attrs[i]}"` 382 | } 383 | } 384 | 385 | if(!inner && children) 386 | return html(...children).then(str => 387 | `<${tag}${_id}${_class}${_attrs}>${str}`) 388 | 389 | if('br,input,img'.split(',').filter(x => x===tag).length === 0) 390 | return new Promise(r => r(`<${tag}${_id}${_class}${_attrs}>${inner}`)) 391 | 392 | return new Promise(r => r(`<${tag}${_id}${_class}${_attrs} />`)) 393 | }) 394 | } 395 | 396 | export const html = (...v) => Promise.all(v.map(toHTML)).then(x => x.filter(x => !!x).join('')) 397 | 398 | 399 | 400 | /* 401 | usage: 402 | 403 | let component = () => 404 | new Array(20).fill(true).map(x => 405 | m('div', {onMouseOver: e => log(e.target.innerHTML)}, range(1,100))) 406 | 407 | client-side 408 | ----- 409 | mount(component, qs()) 410 | 411 | client-side constant re-rendering 412 | ----- 413 | const run = () => { 414 | setTimeout(run, 20) 415 | update() 416 | } 417 | run() 418 | */ 419 | 420 | /* ----------------------------- CONTAINER / HTML USAGE (Server-side rendering) 421 | 422 | const name = _ => new Promise(res => setTimeout(_ => res('matt'), 1500)) 423 | 424 | let x = container(data => [ 425 | m('div.test.row', {className:'hola', 'data-name':data.name, style:{border:'1px solid black'}}), 426 | m('div', data.name), 427 | ], 428 | {name} 429 | ) 430 | 431 | html(x).then(x => log(x)).catch(e => log(e+'')) 432 | --------------------------- */ --------------------------------------------------------------------------------