Module: Signalize

Extended by:
API
Defined in:
lib/signalize.rb,
lib/signalize/struct.rb,
lib/signalize/version.rb

Defined Under Namespace

Modules: API Classes: Computed, Effect, Error, Node, Signal, Struct

Constant Summary collapse

RUNNING =
1 << 0
NOTIFIED =
1 << 1
OUTDATED =
1 << 2
DISPOSED =
1 << 3
HAS_ERROR =
1 << 4
TRACKING =
1 << 5
GLOBAL_MAP =
Concurrent::Map.new
VERSION =
"1.3.0"

Class Method Summary collapse

Methods included from API

batch, computed, effect, signal, untracked

Class Method Details

.add_dependency(signal) ⇒ Object

Signal-related helpers ##



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
200
201
202
203
204
205
206
207
208
# File 'lib/signalize.rb', line 131

def add_dependency(signal)
  return nil if eval_context.nil?

  node = signal._node
  if node.nil? || node._target != eval_context
    # /**
    # * `signal` is a new dependency. Create a new dependency node, and set it
    # * as the tail of the current context's dependency list. e.g:
    # *
    # * { A <-> B       }
    # *         ↑     ↑
    # *        tail  node (new)
    # *               ↓
    # * { A <-> B <-> C }
    # *               ↑
    # *              tail (evalContext._sources)
    # */
    node = Node.new(
      _version: 0,
      _source: signal,
      _prev_source: eval_context._sources,
      _next_source: nil,
      _target: eval_context,
      _prev_target: nil,
      _next_target: nil,
      _rollback_node: node,
    )

    unless eval_context._sources.nil?
      eval_context._sources._next_source = node
    end
    eval_context._sources = node
    signal._node = node

    # Subscribe to change notifications from this dependency if we're in an effect
    # OR evaluating a computed signal that in turn has subscribers.
    if (eval_context._flags & TRACKING).nonzero?
      signal.(node)
    end
    return node
  elsif node._version == -1
    # `signal` is an existing dependency from a previous evaluation. Reuse it.
    node._version = 0

    # /**
    # * If `node` is not already the current tail of the dependency list (i.e.
    # * there is a next node in the list), then make the `node` the new tail. e.g:
    # *
    # * { A <-> B <-> C <-> D }
    # *         ↑           ↑
    # *        node   ┌─── tail (evalContext._sources)
    # *         └─────│─────┐
    # *               ↓     ↓
    # * { A <-> C <-> D <-> B }
    # *                     ↑
    # *                    tail (evalContext._sources)
    # */
    unless node._next_source.nil?
      node._next_source._prev_source = node._prev_source

      unless node._prev_source.nil?
        node._prev_source._next_source = node._next_source
      end

      node._prev_source = eval_context._sources
      node._next_source = nil

      eval_context._sources._next_source = node
      eval_context._sources = node
    end

    # We can assume that the currently evaluated effect / computed signal is already
    # subscribed to change notifications from `signal` if needed.
    return node
  end

  nil
end

.batchObject



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/signalize.rb', line 117

def batch
  return yield unless batch_depth.zero?

  start_batch

  begin
    return yield
  ensure
    end_batch
  end
end

.cleanup_effect(effect) ⇒ Object

Effect-related helpers ##



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/signalize.rb', line 316

def cleanup_effect(effect)
  cleanup = effect._cleanup
  effect._cleanup = nil

  if cleanup.is_a?(Proc)
    start_batch

    # Run cleanup functions always outside of any context.
    prev_context = eval_context
    self.eval_context = nil
    begin
      cleanup.()
    rescue StandardError => err
      effect._flags &= ~RUNNING
      effect._flags |= DISPOSED
      dispose_effect(effect)
      raise err
    ensure
      self.eval_context = prev_context
      end_batch
    end
  end
end

.cleanup_sources(target) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/signalize.rb', line 260

def cleanup_sources(target)
  node = target._sources
  head = nil

  # /**
  # * At this point 'target._sources' points to the tail of the doubly-linked list.
  # * It contains all existing sources + new sources in order of use.
  # * Iterate backwards until we find the head node while dropping old dependencies.
  # */
  while node.nil?.!
    prev = node._prev_source

    # /**
    # * The node was not re-used, unsubscribe from its change notifications and remove itself
    # * from the doubly-linked list. e.g:
    # *
    # * { A <-> B <-> C }
    # *         ↓
    # *    { A <-> C }
    # */
    if node._version == -1
      node._source._unsubscribe(node)

      unless prev.nil?
        prev._next_source = node._next_source
      end
      unless node._next_source.nil?
        node._next_source._prev_source = prev
      end
    else
      # /**
      # * The new head is the last node seen which wasn't removed/unsubscribed
      # * from the doubly-linked list. e.g:
      # *
      # * { A <-> B <-> C }
      # *   ↑     ↑     ↑
      # *   │     │     └ head = node
      # *   │     └ head = node
      # *   └ head = node
      # */
      head = node
    end

    node._source._node = node._rollback_node
    unless node._rollback_node.nil?
      node._rollback_node = nil
    end

    node = prev
  end

  target._sources = head
end

.cycle_detectedObject

Raises:



20
21
22
# File 'lib/signalize.rb', line 20

def self.cycle_detected
 raise Signalize::Error, "Cycle detected"
end

.dispose_effect(effect) ⇒ Object



340
341
342
343
344
345
346
347
348
349
350
# File 'lib/signalize.rb', line 340

def dispose_effect(effect)
  node = effect._sources
  while node.nil?.!
    node._source._unsubscribe(node)
    node = node._next_source
  end
  effect._compute = nil
  effect._sources = nil

  cleanup_effect(effect)
end

.end_batchObject



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
# File 'lib/signalize.rb', line 79

def end_batch
  if batch_depth > 1
    self.batch_depth -= 1
    return
  end
  error = nil
  hasError = false

  while batched_effect.nil?.!
    effect = batched_effect
    self.batched_effect = nil

    self.batch_iteration += 1
    while effect.nil?.!
      nxt = effect._next_batched_effect
      effect._next_batched_effect = nil
      effect._flags &= ~NOTIFIED
      unless (effect._flags & DISPOSED).nonzero? && needs_to_recompute(effect)
        begin
          effect._callback
        rescue StandardError => err
          unless hasError
            error = err
            hasError = true
          end
        end
      end

      effect = nxt
    end
  end

  self.batch_iteration = 0
  self.batch_depth -= 1

  raise error if hasError
end

.end_effect(effect, prev_context, *_) ⇒ Object

allow additional args for currying

Raises:



352
353
354
355
356
357
358
359
360
361
# File 'lib/signalize.rb', line 352

def end_effect(effect, prev_context, *_) # allow additional args for currying
  raise Signalize::Error, "Out-of-order effect" if eval_context != effect

  cleanup_sources(effect)
  self.eval_context = prev_context

  effect._flags &= ~RUNNING
  dispose_effect(effect) if (effect._flags & DISPOSED).nonzero?
  end_batch
end

.global_map_accessor(name) ⇒ Object



10
11
12
13
14
15
16
17
# File 'lib/signalize.rb', line 10

def global_map_accessor(name)
  define_singleton_method "#{name}" do
    GLOBAL_MAP[name]
  end
  define_singleton_method "#{name}=" do |value|
    GLOBAL_MAP[name] = value
  end
end

.mutation_detectedObject

Raises:



24
25
26
# File 'lib/signalize.rb', line 24

def self.mutation_detected
 raise Signalize::Error, "Computed cannot have side-effects"
end

.needs_to_recompute(target) ⇒ Object

Computed/Effect-related helpers ##



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/signalize.rb', line 212

def needs_to_recompute(target)
  # Check the dependencies for changed values. The dependency list is already
  # in order of use. Therefore if multiple dependencies have changed values, only
  # the first used dependency is re-evaluated at this point.
  node = target._sources
  while node.nil?.!
    # If there's a new version of the dependency before or after refreshing,
    # or the dependency has something blocking it from refreshing at all (e.g. a
    # dependency cycle), then we need to recompute.
    if node._source._version != node._version || !node._source._refresh || node._source._version != node._version
      return true
    end
    node = node._next_source
  end
  # If none of the dependencies have changed values since last recompute then
  # there's no need to recompute.
  false
end

.prepare_sources(target) ⇒ Object



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/signalize.rb', line 231

def prepare_sources(target)
  # /**
  # * 1. Mark all current sources as re-usable nodes (version: -1)
  # * 2. Set a rollback node if the current node is being used in a different context
  # * 3. Point 'target._sources' to the tail of the doubly-linked list, e.g:
  # *
  # *    { undefined <- A <-> B <-> C -> undefined }
  # *                   ↑           ↑
  # *                   │           └──────┐
  # * target._sources = A; (node is head)  │
  # *                   ↓                  │
  # * target._sources = C; (node is tail) ─┘
  # */
  node = target._sources
  while node.nil?.!
    rollbackNode = node._source._node
    node._rollback_node = rollbackNode unless rollbackNode.nil?
    node._source._node = node
    node._version = -1

    if node._next_source.nil?
      target._sources = node
      break
    end

    node = node._next_source
  end
end

.start_batchObject

Batch-related helpers ##



75
76
77
# File 'lib/signalize.rb', line 75

def start_batch
  self.batch_depth += 1
end