UNPKG

mreframe

Version:

A reagent/re-frame imitation that uses Mithril instead

247 lines (206 loc) 11.4 kB
{identical, eq, eqShallow, keys, dict, entries, isArray, isDict, isFn, getIn, merge, assoc, assocIn, dissoc, update, repr, identity, chunks, flatten, chain} = require './util' {atom, deref, reset, swap} = require './atom' {_init: _initReagent, atom: ratom, cursor} = require './reagent' [_eq_, _namespaces] = [eq, new Map] _init = (opts) => _initReagent opts _eq_ = opts?.eq or _eq_ undefined inNamespace = (namespace='') => return _namespaces.get namespace if _namespaces.has namespace [_exports, _ns] = [{namespace, inNamespace}, (unless namespace then "" else "[#{namespace}]")] _namespaces.set namespace, _exports ### Application state atom ### _exports.appDb = appDb = ratom {} events = atom {} effects = atom {} coeffects = atom {} subscriptions = atom {} _noHandler = (kind, [key]) => console.error "re-frame#{_ns}: no #{kind} handler registered for: '#{key}'" _duplicate = (kind, key) => console.warn "re-frame#{_ns}: overwriting #{kind} handler for: '#{key}'" _subscriptionCache = new Map ### Removes cached subscriptions (forcing to recalculate) ### _exports.clearSubscriptionCache = => _subscriptionCache.clear() _eventQueue = new Set ### Cancels all scheduled events ### _exports.purgeEventQueue = => _eventQueue.forEach clearTimeout _eventQueue.clear() _clear = (atom) => (id) => if id then swap atom, dissoc, id else reset atom, {} undefined _invalidSignals = (key) => throw SyntaxError "re-frame#{_ns}: invalid signals specified for subscription '#{key}'" _signals = (id, signals) => _invalidSignals id unless signals.every ([k, q]) => (k is '<-') and isArray q queries = signals.map (kq) => kq[1] if queries.length is 1 then (=> subscribe queries[0]) else (=> queries.map subscribe) _deref = (ratom) => ratom._deref() # parent ratom is not to be propagated _calcSignals = (signals) => if isArray signals then signals.map _deref else unless isDict signals then _deref signals else dict entries(signals).map ([k, v]) => [k, _deref v] ### Registers a subscription function to compute view data ### _exports.regSub = (id, ...signals, computation) => signals = if signals.length is 0 then => appDb else if signals.length isnt 1 then _signals id, (chunks signals, 2) else if isFn signals[0] then signals[0] else _invalidSignals id _duplicate "subscription", id if (deref subscriptions)[id] swap subscriptions, assoc, id, [signals, computation] undefined _calcSub = (signals, computation) => (query) => input = _calcSignals signals query if _subscriptionCache.has(key = repr query) [input_, output] = _subscriptionCache.get key return output if eqShallow input, input_ x = computation input, query _subscriptionCache.set key, [input, x] x _cursors = new Map ### Returns an RCursor that derefs to subscription result (or cached value) ### _exports.subscribe = subscribe = (query) => unless (it = (deref subscriptions)[ query[0] ]) then _noHandler "subscription", query else _cursors.set key, cursor _calcSub(...it), query unless _cursors.has(key = repr query) _cursors.get key ### Unregisters one or all subscription functions ### _exports.clearSub = do (_clearSubs = _clear subscriptions) => (id) => id or _cursors.clear(); _clearSubs id ### Produces an interceptor (changed from varargs to options object). Interceptor structure: {:id :something ;; decorative only - can be ignored :before (fn [context] ...) ;; returns a possibly modified `context` :after (fn [context] ...)} ;; returns a possibly modified `context` ### _exports.toInterceptor = toInterceptor = (args) => id: args?.id before: args?.before or identity after: args?.after or identity _getX = (x, key, notFound) => unless key then x else if key of (x or {}) then x[key] else notFound ### Returns context coeffects or specified coeffect ### _exports.getCoeffect = getCoeffect = (context, key, notFound) => _getX context.coeffects, key, notFound ### Returns context effects or specified effect ### _exports.getEffect = getEffect = (context, key, notFound) => _getX context.effects, key, notFound ### Produces a copy of the context with added coeffect ### _exports.assocCoeffect = assocCoeffect = (context, key, value) => assocIn context, ['coeffects', key], value ### Produces a copy of the context with added effect ### _exports.assocEffect = assocEffect = (context, key, value) => assocIn context, ['effects', key], value ### Produces a copy of the context with interceptors added to the queue ### _exports.enqueue = (context, interceptors) => update context, 'queue', (xs) => [...xs, ...interceptors] _getDb = (context) => getEffect context, 'db', (getCoeffect context, 'db') _pathKey = 're-frame-path/db-store' ### Produces an interceptor which switches out db for its subpath ### _exports.path = (...path) => toInterceptor id: 'path' before: (context) => db = getCoeffect context, 'db' dbs = [...(context[_pathKey] or []), db] chain context, [assoc, _pathKey, dbs], [assocCoeffect, 'db', (getIn db, flatten path)] after: (context) => [...dbs, db] = context[_pathKey] chain context, [assoc, _pathKey, dbs], [assocEffect, 'db', (assocIn db, (flatten path), (_getDb context))], [assocCoeffect, 'db', db] ### Produces an interceptor which updates db effect after the event handler ### _exports.enrich = (f) => toInterceptor id: 'enrich', after: (context) => assocEffect context, 'db', (f (_getDb context), (getCoeffect context, 'event')) _replaceEvent = (f) => (context) => event = getCoeffect context, 'event' chain context, [assocCoeffect, 'originalEvent', event], [assocCoeffect, 'event', f event] _restoreEvent = (context) => assocCoeffect context, 'event', (getCoeffect context, 'originalEvent') ### An interceptor switches out event for its 1st parameter ### _exports.unwrap = toInterceptor id: 'unwrap', after: _restoreEvent, before: _replaceEvent (event) => event[1] ### An interceptor switches out event for its parameters ### _exports.trimV = toInterceptor id: 'trim-v', after: _restoreEvent, before: _replaceEvent (event) => event[1..] ### Produces an interceptor which updates runs an action on db/event after the event handler ### _exports.after = (f) => toInterceptor id: 'after', after: (context) => f (_getDb context), (getCoeffect context, 'event') context ### Produces an interceptor which recalculates db subpath if input subpaths changed ### _exports.onChanges = (f, outPath, ...inPaths) => toInterceptor id: 'on-changes' after: (context) => db0 = getCoeffect(context, 'db'); db1 = _getDb context [ins, outs] = [db0, db1].map (db) => inPaths.map (path) => getIn db, path if (outs.every (x, i) => identical x, ins[i]) then context else assocEffect context, 'db', (assocIn db1, outPath, f ...outs) ### Registers a coeffect handler (for use as an interceptor) ### _exports.regCofx = (id, handler) => _duplicate "coeffect", id if (deref coeffects)[id] swap coeffects, assoc, id, handler undefined ### Produces an interceptor which applies a coeffect handler before the event handler ### _exports.injectCofx = (key, arg) => toInterceptor id: key, before: (context) => if (it = (deref coeffects)[key]) update context, 'coeffects', (deref coeffects)[key], arg else _noHandler "coeffect", [key]; context ### Unregisters one or all coeffect handlers ### _exports.clearCofx = _clear coeffects ### Registers an event handler which calculates new application state from the old one ### _exports.regEventDb = (id, interceptors, handler) => [interceptors, handler] = [[], interceptors] unless handler regEventFx id, interceptors, (cofx, query) => db: handler cofx.db, query _ctxEvt = (handler) => (context) => merge context, {effects: handler (getCoeffect context), (getCoeffect context, 'event')} ### Registers an event handler which calculates effects from coeffects ### _exports.regEventFx = regEventFx = (id, interceptors, handler) => [interceptors, handler] = [[], interceptors] unless handler regEventCtx id, interceptors, _ctxEvt handler ### Registers an event handler which arbitrarily updates the context ### _exports.regEventCtx = regEventCtx = (id, interceptors, handler) => [interceptors, handler] = [[], interceptors] unless handler _duplicate "event", id if (deref events)[id] swap events, assoc, id, [(flatten interceptors.filter identity), handler] undefined ### Unregisters one or all event handlers ### _exports.clearEvent = clearEvent = _clear events ### Context structure: {:coeffects {:event [:some-id :some-param] :db "original contents of app-db"} :effects {:db "new value for app-db>" :dispatch [:an-event-id :param1]} :queue "a collection of further interceptors" :stack "a collection of interceptors already walked"} ### _intercept = (context, hook) => # every step is dynamic so no chains, folds or for-loops context = merge context, stack: [], queue: context.stack while context.queue.length > 0 [x, ...xs] = context.queue context = x[hook] (merge context, queue: xs) context = merge context, stack: [x, ...context.stack] context ### Dispatches an event (running back and forth through interceptor chain & handler then actions effects) ### _exports.dispatchSync = dispatchSync = (event) => unless (it = (deref events)[ event[0] ]) then _noHandler "event", event else [stack, handler] = it context = {stack, coeffects: {event, db: _deref appDb}} chain context, [_intercept, 'before'], handler, [_intercept, 'after'], getEffect, entries, _fx _dispatch = ({ms, dispatch}) => _eventQueue.add id = setTimeout (=> _eventQueue.delete id; dispatchSync dispatch), ms id ### Schedules dispatching of an event ### _exports.dispatch = dispatch = (dispatch) => _dispatch {dispatch} _fx = (fxs, fx=deref effects) => fxs.filter(identity).forEach ([k, v]) => if (it = fx[k] or _effects[k]) then it v else _noHandler "effect", [k] _effects = # builtin effects db: (value) => reset appDb, value unless _eq_ value, _deref appDb fx: _fx dispatchLater: _dispatch dispatch: (dispatch) => _dispatch {dispatch} ### Registers an effect handler (implementation of a side-effect) ### _exports.regFx = (id, handler) => _duplicate "effect", id if (deref effects)[id] swap effects, assoc, id, handler undefined ### Unregisters one or all effect handlers (excepting builtin ones) ### _exports.clearFx = _clear effects ### Convenience function (for JS); returns deref'ed result of a subscription ### _exports.dsub = (query) => deref subscribe query ### Convenience function (for fx); schedules dispatching an event (if present) with additional parameters ### _exports.disp = (evt, ...args) => evt and dispatch [...evt, ...args] Object.defineProperty _exports, 'namespace', value: namespace # read-only (just in case) module.exports = Object.assign (inNamespace ''), {_init}