UNPKG

subsystems

Version:
277 lines (220 loc) 9.32 kB
# Author: maartyl # I will need: # @inject : info about what it requires (nothing more) # @system : provides :start which toposorts and starts all subsystems # @start function :: System -> It'sAPI (generally map of started subsystems) # # # solve problem of defining deps on this system # HAS TO BE EXPLICIT ^ # # and cont gets map of all the started subsystems # # systems goes through all objects and asks: # Hey, mate, are you an inject? and if so, it's a dependency. # : how do I put such injects on this system itself? ... # - maybe the most simple way: just provide inject, instead of subsystem, yeah... # - I like this and I'm pretty sure it will work ^^ # //since all must be resolved, before start is called. # # each system has to provide everything for his subs, even if all he can do is explicitly state he needs it. # # if a dependency CANNOT BE FOUND: must produce nice error # if a CYCLIC dependency is found: must produce nice error # # how will I perform the INJECTION ITSELF? # : all 'done' store in a map # : keep info who needs what: take from the map, before calling start # -- (Will be there by the time it's needed; if not: missing dependency (would probably notice in toposort anyway)) # : keep 'meta' who needs what, though. (should be a function! (for simplicity, composability...)) # -- and THAT FUNCTION CAN JUST END WITH CALLING START, having everything ordered implicitly # -- takes rest of initialization as argument... - all async and good ^^ # # let's call that: next(started, cont) (== inject + call start) # : this function itself probably needs current context (loaded deps map; what to call at end; sorted deps...) # # NO DEPENDENCY CAN BE CALLED START # # # CONT used by start:: (err, api)->() topo = require 'toposort' util = require 'util' jstr = (o) -> util.inspect o, depth:5 isFn = (o) -> typeof o is 'function' OK = null # passed in error field class SubsystemInjectorStub constructor: (@dep)->@ isInjector = (obj)-> obj instanceof SubsystemInjectorStub inject = (dependency_name) -> if dependency_name is 'start' then throw new Error "No dependency may be called 'start'." new SubsystemInjectorStub dependency_name @inject = inject # retuns list of dependencies # [[to from]] (to: which fild to inject) (from: dependency name) scan = (obj) -> [k, v.dep] for k, v of obj when isInjector v # get dependencies of a subsystem # @dependencies = scan #possibly in the future, if needed # builds edges(dependencies) # FOR SINGLE NODE name make_edges = (name, scanned) -> [dep, name] for [field, dep] in scanned # returns list that can be passed topo (to get order of starting) # map_subs :: (dependency_name -> subsystem) # cont :: (err, all_dep_edges, nexts(functions to start component + inject)) -> () make_deps = (map_subs, cont) -> deps_edges = [] nexts = {} for dep, sub of map_subs scanned = scan sub #get all fields with InjectorStub and what they depend on deps_edges = deps_edges.concat make_edges dep, scanned #append all dependencies to edges unless isFn sub.start return cont new Error "Passed subsystem doesn't have start method - #{jstr dep}:#{jstr sub}." nexts[dep] = do(dep, scanned, sub, starter=sub.start)-> (started, cont) -> sub.start = (cont) -> cont OK, started[dep] #replace start method with just map lookup to cashed result if dep of started then return cont OK, started[dep] #probably always checked outside, but better safe... for [field, sub_dep] in scanned #start_map check doesn't catch some edge cases (unreal dep...) unless sub_dep of started then return cont new Error "Unmet dependency: #{jstr sub_dep}." sub[field] = started[sub_dep] #inject: set field to required from map of started deps starter.call sub, cont cont OK, deps_edges, nexts build_context = (nodes, nexts, started, final_cont) -> index: 0 #where am I on nodes (which to start next) nodes: nodes # toposorted dependency names nexts: nexts # functions to start subsystems started: started #already started subsystems cont: final_cont execute_context = (cxt) -> if cxt.index >= cxt.nodes.length # already started all subsystems return cxt.cont OK, cxt.started dep = cxt.nodes[cxt.index++] if dep of cxt.started then return execute_context cxt # already started (needs to be here, not in nexts[]) #injects + provides api: stored in started and used by following nexts cxt.nexts[dep] cxt.started, (err, sub_api) -> if err then return cxt.cont err cxt.started[dep] = sub_api execute_context cxt # map :: (dependency_name -> subsystem) # started: (dep_name -> api) #for when this system already depends on another start_map = (map, started, final_cont) -> make_deps map, (err, deps, nexts) -> if err then return final_cont err unsorted_nodes = do->(k for k, v of map) # include all nodes (even if unmentioned in depepndencies) try nodes = topo.array unsorted_nodes, deps #topologically sort dependency graph catch err then return final_cont err #cyclic dependency unmets = (jstr dep for dep in nodes when not (nexts[dep]? or dep of started)) if unmets.length isnt 0 return final_cont new Error "Unmet dependencies: #{unmets}." execute_context build_context nodes, nexts, started, final_cont # @system: something that 'just' composes it, START is what matters # + keeps track of which deps are met externally (injects) and which need to be started map2system = (map) -> unmet_keys = [] # whether system itself has some :inject values met_keys =[] for k, v of map if isInjector v unmet_keys.push k else met_keys.push k map.start = (cont) -> started = {} unstarted = {} for k in unmet_keys if isInjector map[k] then cont new Error "System with unmet dependency: #{jstr k}." started[k] = map[k] for k in met_keys unstarted[k] = map[k] start_map unstarted, started, cont map # creates subsystem which starts all subsystems in map and passes to # cont(of it's start) all APIs produced (with same keys as original subsystems) # map :: (dependency_name -> subsystem) @system = (map) -> if map.start? then throw new Error "No dependency may be called 'start'." map2system map # this system is assumed to have no unmet dependencies @start = (system, cont) -> s = scan system if s.length isnt 0 then cont new Error "Cannot start system with unmet dependencies: #{jstr dep for [k, dep] in s}." else unless isFn system.start then cont new Error 'No start method on system.' else system.start cont ## UTILS fmap = (dep, fn) -> new class value: inject dep start: (cont) => # cont OK, fn @value try v = fn @value #if fn throws: fail continuation catch err then return cont err cont OK, v # subsystem which exports some function of it's single dependency @fmap = fmap # field of a dependency (following arguments are used as gield of that and of that...) @field = (dep, fields...) -> fmap dep, (v)-> ret = v for field in fields unless field of Object ret throw new Error "Field #{jstr field} not present." ret = ret[field] ret rename = (system, map) -> s = scan system for [key, old] in s when dep = map[old] system[key] = inject dep system # rename dependencies # MUTATES system ('s injects) # map (old(inside(require))name -> new(outside)name) @rename = (system, map) -> if map.start? then throw new Error "No dependency may be called 'start'." rename system, map wrap = (api) -> start: (cont) -> cont OK, api #just wrap value into a system structure @wrap = wrap api_diff = (ss) -> reference = {} #store what is curretnlty in ss, so one can do diff at end and only export changes for k, v of ss reference[k] = v () -> #returns function that creats api from diff compared to first call api = {} for k, v of ss unless reference[k] is v #if already present in reference: not new: don't export api[k] = v #put all diffs on ss to api api subsystem = (core) -> (deps, ctor) -> if deps.start? then throw new Error "No dependency field may be called 'start'." -> #return creator function ss = {} #subsystem for k, v of deps ss[k] = inject v ss.start = core ctor.bind(ss), ss ss #start versions #ctor:: ()->() subsystem.sync = (ctor, ss) -> (cont) -> mk_api = api_diff ss try do ctor #update (initialize) ss, after all deps met catch err then return cont err cont OK, do mk_api #ctor:: (cont::(err)->()) -> () subsystem.async = (ctor, ss) -> (cont) -> mk_api = api_diff ss try ctor (err) -> if err then return cont err cont OK, do mk_api #ctor:: () -> api subsystem.ret = (ctor, ss) -> (cont) -> cont.apply undefined, try [OK, do ctor] catch err then [err] #ctor:: (cont) -> () #cont:: (err, api) -> () # subsystem.ret.async = (ctor, ss) -> (cont) -> ctor cont subsystem.ret.async = (ctor, ss) -> ctor #allows very simple creation of (lowest level) subsystems #exports diff of `this` created by ctor # :: (deps, ctor) -> (args...) -> system # deps :: {field -> dep_name} # ctor :: ()->() - updates `this` @subsystem = subsystem subsystem.sync @subsystem.async = subsystem subsystem.async @subsystem.ret = subsystem subsystem.ret @subsystem.ret.async = subsystem subsystem.ret.async