-
Notifications
You must be signed in to change notification settings - Fork 5
/
reactor.coffee
199 lines (159 loc) · 7.61 KB
/
reactor.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
{ EventEmitter } = require 'events'
# I'll give $US 5,000 to the person who fucking *fixes* how Node handles globals inside modules. ಠ_ಠ
Paws = require './Paws.coffee'
{ Thing, Label, Execution, Native
, Relation, Combination, Position, Mask
, debugging, utilities: _ } = Paws
{ constructify, parameterizable, delegated
, terminal: term } = _
{ ENV, verbosity, is_silent, colour
, emergency, alert, critical, error, warning, notice, info, debug, verbose, wtf } = debugging
module.exports =
reactor = new Object
# This acts as a `Unit`'s store of access knowledge: `Executions` are matched to the `Mask`s they've
# been given responsibility for.
#
# I'd *really* like to see a better data-structure; but my knowledge of such things is insufficient
# to apply a truly appropriate one. For now, just a simple mapping of `Mask`s to accessors
# (`Executions`).
reactor.Table = Table = class Table
constructor: ->
@content = new Array
give: (accessor, masks...)->
entry = _(@content).find accessor: accessor
if not entry?
@content.push(entry = { accessor: accessor, masks: new Array })
entry.masks.push masks...
return entry
get: (accessor)-> _(@content).find(accessor: accessor)?.masks ? new Array
# FIXME: Test the remove-conflicting-masks functionality
remove: ({accessor, mask})->
return unless accessor? or mask?
_.remove @content, (entry)->
return false if accessor? and entry.accessor != accessor
return true unless mask
_.remove entry.masks, (m)-> m.conflictsWith mask
entry.masks.length == 0
# Returns `true` if a given `Mask` is fully contained by the set of responsibility that a given
# `accessor` has already been given.
has: (accessor, mask)->
mask.containedBy @get(accessor)...
# Returns `true` if a given `Mask` conflicts with any of the responsibility given to *other*
# accessors.
canHave: (accessor, mask)->
not _(@content).reject(accessor: accessor).some (entry)->
mask.conflictsWith entry.masks...
allowsStagingOf: ({stagee, _, requestedMask})-> not requestedMask? or
@has(stagee, requestedMask) or
@canHave(stagee, requestedMask)
reactor.Staging = Staging = class Staging
constructor: constructify (@stagee, @result, @requestedMask)->
# The Unitary design (i.e. distribution) isn't complete, at all. At the moment, a `Unit` is just a
# place to store the action-queue and access-table.
#
# Theoretically, this should be enough to, at least, run two Units *at once*, even if there's
# currently no design for the ways I want to allow them to interact.
# More on that later.
reactor.Unit = Unit = parameterizable class Unit extends EventEmitter
constructor: constructify(return:@) ->
@queue = new Array
@table = new Table
# `stage`ing is the core operation of a `Unit` as a whole, the only one that requires
# simultaneous access to the `queue` and `table`.
stage: (execution, result, requestedMask)->
@queue.push new Staging execution, result, requestedMask
@schedule() if @_?.immediate != no
# This method looks for the foremost staging of the queue that either:
#
# 1. doesn’t have an associated `requestedMask`,
# 2. is already responsible for a mask equivalent to the one requested,
# 3. or whose requested mask doesn’t conflict with any existing ones, excluding its own.
#
# If no request is currently valid, it returns undefined.
#---
# FIXME: Ugly.
next: ->
idx = _(@queue).findIndex (staging)=> @table.allowsStagingOf staging
@queue.splice(idx, 1)[0] if idx != -1
upcoming: ->
results = _.filter @queue, (staging)=> @table.allowsStagingOf staging
return if results.length then results else undefined
#---
# XXX: Exists soely for debugging purposes. Could just emit *inside* `realize`.
flushed: ->
if process.env['TRACE_REACTOR']
debug "~~ Queue flushed#{if @queue.length then ' @ '+@queue.length else ''}."
@emit 'flushed', @queue.length
# Generate the form of object passed to receivers.
@receiver_parameters: (stagee, subject, message)->
new Thing(stagee, subject, message).rename '<receiver params>'
# The core reactor of this implementation, `#realize` will ‘process’ a single `Staging` from this
# `Unit`'s queue. Returns `true` if a `Staging` was acquired and in some way processed, and
# `false` if no processing was possible.
#
# Emits a 'flushed' event if there's no executions in the queue (at least, none that are valid
# for staging.) Listeners will be passed the number of unrealizable executions still in the
# queue, if any are present.
realize: ->
unless staging = @next()
@awaitingTicks = 0
return no
{stagee, result, requestedMask} = staging
if process.env['TRACE_REACTOR']
warning ">> #{stagee} ← #{result}"
if stagee.current() instanceof Position
body = stagee.current().expression().with context: 3, tag: no
.toString focus: stagee.current().valueOf()
debug term.block body, (line)-> ' │ ' + line.slice 0, -4
else if stagee.current() instanceof Function
body = stagee.current().toString()
wtf term.block body, (line)-> ' │ ' + line.slice 0, -4
# Remove completed stagees from the queue, with no further action.
if stagee.complete()
warning ' ╰┄ complete!' if process.env['TRACE_REACTOR']
@flushed() unless @upcoming()
return yes
combo = stagee.advance result
@current = stagee
# If the staging has passed #next, then it's safe to grant it the ownership it's requesting
@table.give stagee, requestedMask if requestedMask
# If we're looking at a native, then we received a bit-function from #advance
if typeof combo == 'function'
combo.apply stagee, [result, this]
else
subject = combo.subject ? stagee.locals
message = combo.message ? stagee.locals
if process.env['TRACE_REACTOR']
warning " ╰┈ ⇢ combo: #{combo.subject} × #{combo.message}"
@stage subject.receiver.clone(),
Unit.receiver_parameters stagee, subject, message
@table.remove accessor: stagee if stagee.complete()
@flushed() unless @upcoming()
delete @current
return yes
# Every time `schedule()` is called on a `Unit`, the implementation is informed that there's at
# least one more combination that needs to be processed. As long as the implementation *knows*
# there's combinations waiting to be processed, it will attempt to process them actively, on the
# stack (that is, non-asynchronously.) This is often quite a bit faster than waiting for the next
# iteration of the underlying event-loop.
#
# Immediately after incrementing the count of awaiting-combinations, this will start the reactor
# attempting to process those combinations
#---
# FIXME: I'm pretty sure `@awaitingTicks` can be replaced with `@queue.length`...
schedule: ->
++@awaitingTicks
return if @current?
while @awaitingTicks
if @realize() then --@awaitingTicks else return
awaitingTicks: 0
interval: 50 # in milliseconds; default of 1/20th of a second.
interval = 0
start: ->
interval ||= setInterval @schedule.bind(this), @interval
@schedule()
stop: ->
if interval
clearInterval(interval)
interval = undefined
info "++ Reactor available"