inform-runtime 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +623 -0
  3. data/README.md +185 -0
  4. data/Rakefile +65 -0
  5. data/config/database.yml +37 -0
  6. data/exe/inform.rb +6 -0
  7. data/game/config.yml +5 -0
  8. data/game/example.inf +76 -0
  9. data/game/example.rb +90 -0
  10. data/game/forms/example_form.rb +2 -0
  11. data/game/grammar/game_grammar.inf.rb +11 -0
  12. data/game/languages/english.rb +2 -0
  13. data/game/models/example_model.rb +2 -0
  14. data/game/modules/example_module.rb +9 -0
  15. data/game/rules/example_state.rb +2 -0
  16. data/game/scripts/example_script.rb +2 -0
  17. data/game/topics/example_topic.rb +2 -0
  18. data/game/verbs/game_verbs.rb +15 -0
  19. data/game/verbs/metaverbs.rb +2028 -0
  20. data/lib/runtime/articles.rb +138 -0
  21. data/lib/runtime/builtins.rb +359 -0
  22. data/lib/runtime/color.rb +145 -0
  23. data/lib/runtime/command.rb +470 -0
  24. data/lib/runtime/config.rb +48 -0
  25. data/lib/runtime/context.rb +78 -0
  26. data/lib/runtime/daemon.rb +266 -0
  27. data/lib/runtime/database.rb +500 -0
  28. data/lib/runtime/events.rb +771 -0
  29. data/lib/runtime/experimental/handler_dsl.rb +175 -0
  30. data/lib/runtime/game.rb +74 -0
  31. data/lib/runtime/game_loader.rb +132 -0
  32. data/lib/runtime/grammar_parser.rb +553 -0
  33. data/lib/runtime/helpers.rb +177 -0
  34. data/lib/runtime/history.rb +45 -0
  35. data/lib/runtime/inflector.rb +195 -0
  36. data/lib/runtime/io.rb +174 -0
  37. data/lib/runtime/kernel.rb +450 -0
  38. data/lib/runtime/library.rb +59 -0
  39. data/lib/runtime/library_loader.rb +135 -0
  40. data/lib/runtime/link.rb +158 -0
  41. data/lib/runtime/logging.rb +197 -0
  42. data/lib/runtime/mixins.rb +570 -0
  43. data/lib/runtime/module.rb +202 -0
  44. data/lib/runtime/object.rb +761 -0
  45. data/lib/runtime/options.rb +104 -0
  46. data/lib/runtime/persistence.rb +292 -0
  47. data/lib/runtime/plurals.rb +60 -0
  48. data/lib/runtime/prototype.rb +307 -0
  49. data/lib/runtime/publication.rb +92 -0
  50. data/lib/runtime/runtime.rb +321 -0
  51. data/lib/runtime/session.rb +202 -0
  52. data/lib/runtime/stdlib.rb +604 -0
  53. data/lib/runtime/subscription.rb +47 -0
  54. data/lib/runtime/tag.rb +287 -0
  55. data/lib/runtime/tree.rb +204 -0
  56. data/lib/runtime/version.rb +24 -0
  57. data/lib/runtime/world_tree.rb +69 -0
  58. data/lib/runtime.rb +35 -0
  59. metadata +199 -0
@@ -0,0 +1,771 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # Copyright Nels Nelson 2008-2023 but freely usable (see license)
5
+ #
6
+ # This file is part of the Inform Runtime.
7
+ #
8
+ # The Inform Runtime is free software: you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License as published
10
+ # by the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # The Inform Runtime is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
20
+
21
+ # The Time class
22
+ class Time
23
+ def to_ms
24
+ (self.to_f * 1000.0).to_i
25
+ end
26
+
27
+ def self.ns
28
+ self.now.to_f
29
+ end
30
+
31
+ def self.ms
32
+ self.now.to_ms
33
+ end
34
+
35
+ def self.s
36
+ self.now.to_i
37
+ end
38
+ end
39
+
40
+ # Re-open the Proc class to define #to_lambda
41
+ class Proc
42
+ WRONG_NUMBER_ARGUMENTS_MESSAGE =
43
+ "Wrong number of arguments (given %<args>s, expected %<arity>s)".freeze
44
+
45
+ # rubocop: disable Metrics/MethodLength
46
+ # rubocop: disable Style/Lambda
47
+ def to_lambda
48
+ return self if lambda?
49
+ # Save local reference to self so we can use it in the following lambda scope
50
+ source_proc = self
51
+ ->(*args, **kwargs, &block) do
52
+ # Enforce strict arity like a lambda
53
+ source_arity = source_proc.arity
54
+ if source_arity >= 0 && args.length != source_arity
55
+ raise ArgumentError, format(
56
+ WRONG_NUMBER_ARGUMENTS_MESSAGE, args: args.length, arity: source_arity)
57
+ elsif args.length < (min = -source_arity - 1)
58
+ raise ArgumentError, format(
59
+ WRONG_NUMBER_ARGUMENTS_MESSAGE, args: args.length, arity: "#{min}+")
60
+ end
61
+ # Preserve original binding/locals/self
62
+ source_proc.call(*args, **kwargs, &block)
63
+ end
64
+ end
65
+ # rubocop: enable Metrics/MethodLength
66
+ # rubocop: enable Style/Lambda
67
+ end
68
+
69
+ # The Inform module
70
+ module Inform
71
+ # The EventCauseRecordNotFoundError class
72
+ class EventCauseRecordNotFoundError < StandardError; end
73
+ end
74
+
75
+ module EventConstants
76
+ EventNameTemplate = 'event:%<time>s'.freeze
77
+ EventCauseIdentityTemplate = '%<identity>s:%<name>s'.freeze
78
+ EventSourceTemplate = ':%<source_location>s:%<source_line_number>s'.freeze
79
+ EventSourceFullTemplate = ':%<method_name>s:%<source_location>s:%<source_line_number>s'.freeze
80
+ EventActivityTemplate = ':%<activity>s'.freeze
81
+ EventAntecedantTemplate = ' < %<antecedent>s'.freeze
82
+ CallerPattern = %r{([^:]+):([^:]+):in `([^']+)'}.freeze
83
+ end
84
+
85
+ # The PublicEventMethods module
86
+ module PublicEventMethods
87
+ include EventConstants
88
+
89
+ def contextualize(from)
90
+ # log.info "Initializing context for event #{self} from #{from} (#{from.class})"
91
+ Inform::Context.each_attribute(from) do |attribute, value|
92
+ warn_when_nilling_noun(from, self, attribute, value)
93
+ send(Inform::Context.setter_method(attribute), value)
94
+ end
95
+ end
96
+
97
+ # TODO: Remove
98
+ def warn_when_nilling_noun(from, to, attribute, value)
99
+ return unless attribute == :noun
100
+ return unless value.nil?
101
+ return if (current_value = to.send(attribute)).nil?
102
+ return if attribute == :parameters && current_value <= 1
103
+ log.debug "Overwriting #{to}.#{attribute} (#{current_value}) with #{from}.#{attribute} (nil)!"
104
+ log.debug " #{to}.context: #{to.context.inspect}"
105
+ maybe_log_nilling_warning(to.callstack)
106
+ end
107
+
108
+ # TODO: Remove
109
+ def maybe_log_nilling_warning(callstack)
110
+ return if callstack.grep('channel_read')
111
+ ['===========', (caller[-4...] + callstack), '==========='].each { |t| log.warn t unless t.nil? }
112
+ end
113
+
114
+ def <<(event)
115
+ @successors.add event
116
+ event.antecedent = self
117
+ end
118
+
119
+ def origin(x = self)
120
+ x = origin x.antecedent if x.antecedent
121
+ x.object? ? x : x.cause
122
+ end
123
+
124
+ def termination(x = self)
125
+ event = x.successors.first
126
+ event || x.terminus
127
+ end
128
+
129
+ def elemental?
130
+ @type == :elemental
131
+ end
132
+
133
+ def integral?
134
+ @type == :integral
135
+ end
136
+
137
+ def immediately?
138
+ @when == :immediately
139
+ end
140
+
141
+ def cancelled?
142
+ return true if future.nil?
143
+ future.cancelled?
144
+ end
145
+
146
+ def antecedent_cancelled?
147
+ return false if self.antecedent.nil?
148
+ return false unless self.antecedent.cancelled?
149
+ return false if self.terminus.nil?
150
+ log.warn "Event antecedent #{self.antecedent} was cancelled"
151
+ true
152
+ end
153
+
154
+ def concluded?
155
+ semaphore.synchronize { concluded == true }
156
+ end
157
+
158
+ def get
159
+ if future.respond_to?(:get)
160
+ future.get
161
+ elsif future.respond_to?(:call)
162
+ future.call
163
+ end
164
+ future
165
+ rescue SystemExit => e
166
+ raise e
167
+ # rescue EventCancelled => e
168
+ # log.warn "Cancelled #{event}"
169
+ # log.debug "Reason for event cancellation: #{e}"
170
+ rescue StandardError => e
171
+ log.error "Event execution exception: #{e.message}"
172
+ end
173
+
174
+ def java_get
175
+ self.future.get
176
+ self.future
177
+ rescue SystemExit => e
178
+ raise e
179
+ rescue Java::JavaUtilConcurrent::CancellationException => e
180
+ log.warn "Cancelled #{event}"
181
+ log.debug "Reason for event cancellation: #{e}"
182
+ rescue Java::JavaUtilConcurrent::ExecutionException => e
183
+ log.error "Event execution exception: #{e.message}"
184
+ end
185
+
186
+ def now(params = {}, &block)
187
+ # TODO: Determine if :cause should be overwritten
188
+ # by any :cause params given by the params Hash
189
+ Inform::Event.new({ cause: cause, antecedent: self }.merge(params), &block)
190
+ end
191
+ alias add_event now
192
+ alias event now
193
+ alias and_then now
194
+ alias chain now
195
+ alias eventually now
196
+ alias later now
197
+
198
+ # Shortcut for using delays
199
+ def delay(delay, &block)
200
+ Inform::Event.new({ cause: cause, antecedent: self, delay: delay }, &block)
201
+ end
202
+ alias after_delay delay
203
+
204
+ def finally(&block)
205
+ Inform::Event.new({ cause: cause, antecedent: self, terminus: true }, &block)
206
+ end
207
+ alias on_cancel finally
208
+ alias when_completed finally
209
+ alias when_done finally
210
+ alias when_finished finally
211
+ alias when_over finally
212
+ alias ultimately finally
213
+
214
+ def parse_activity
215
+ self.cause.parse(self.activity)
216
+ end
217
+
218
+ def call_activity
219
+ return self.activity.call(self) if self.activity.arity == 1
220
+ self.activity.call(*self.args)
221
+ end
222
+
223
+ # java_signature 'V call()'
224
+ def call
225
+ process(self)
226
+ end
227
+
228
+ # java_signature 'void run()'
229
+ def run
230
+ call
231
+ end
232
+
233
+ def on_failure(_cause)
234
+ cancelled
235
+ end
236
+
237
+ def on_success(_result)
238
+ completed
239
+ end
240
+
241
+ def on_cancelled_antecedent
242
+ schedule self unless terminus.nil?
243
+ successors.each(&:on_cancelled_antecedent)
244
+ end
245
+
246
+ def to_s
247
+ self.identity
248
+ end
249
+
250
+ def inspect
251
+ format(EventCauseIdentityTemplate, identity: @cause.identity, name: @cause.name)
252
+ end
253
+
254
+ RecordNotFoundErrorPattern = %r{Record not found}.freeze
255
+
256
+ def mandate_cause_record_exists!
257
+ self.cause.refresh
258
+ rescue StandardError => e
259
+ if RecordNotFoundErrorPattern.match?(e.message)
260
+ message = e.message + ' for cause of event ' + self.to_s
261
+ raise Inform::EventCauseRecordNotFoundError, message
262
+ end
263
+ log.warn "Unexpected error refreshing event cause object: #{e.message}"
264
+ end
265
+ end
266
+ # module PublicEventMethods
267
+
268
+ # The PrivateEventMethods module
269
+ module PrivateEventMethods
270
+ private
271
+
272
+ def to_java_future(future)
273
+ future.to_java(com.google.common.util.concurrent.ListenableFuture)
274
+ end
275
+
276
+ # TODO: Implement a mechanism by which new event scheduling may
277
+ # be deferred or discarded entirely. The reason for this is that
278
+ # during shutdown, there will certainly be ongoing events which
279
+ # require some form of state resolution. The spinning top, for
280
+ # instance -- once spun, is given the spinning attribute. Now,
281
+ # perhaps this is unnecessary, but the case remains that the state
282
+ # of the top must be returned to its resting state even though the
283
+ # game is being shutdown. Perhaps a special on_cancel hook could
284
+ # be provided for such events which require such state resolution.
285
+ # Otherwise, at this point, some way of deciding if incoming
286
+ # events are newly spawned, or if they are part of a chain of
287
+ # events which must be allowed to transpire for the sake of an
288
+ # object's state consistency.
289
+ # rubocop: disable Metrics/AbcSize
290
+ def schedule(event, delay = 0)
291
+ raise "The event parameter may not be nil" if event.nil?
292
+ delay = event.time_delay.to_f * 1000 if event.time_delay > 0
293
+ schedule_event(event, delay)
294
+ add_future_callback(event, Inform::Events.scheduler)
295
+ event.cause.events << event
296
+ Inform::Events.active_objects << event.cause unless active?(event.cause)
297
+ event.future
298
+ end
299
+ # rubocop: enable Metrics/AbcSize
300
+
301
+ def active?(obj)
302
+ Inform::Events.active_objects.include?(obj)
303
+ end
304
+
305
+ def schedule_event(event, delay)
306
+ event.future = if defined?(Java)
307
+ Inform::Events.scheduler.schedule(event, delay, TimeUnit::MILLISECONDS)
308
+ else
309
+ # TODO: Implement
310
+ Inform::Events.scheduler.schedule(event, delay)
311
+ end
312
+ end
313
+
314
+ def add_future_callback(event, executor)
315
+ if defined?(java)
316
+ Futures.addCallback(to_java_future(event.future), event, executor)
317
+ else
318
+ # TODO: Implement
319
+ event.future
320
+ end
321
+ end
322
+
323
+ # rubocop: disable Metrics/AbcSize
324
+ # rubocop: disable Metrics/CyclomaticComplexity
325
+ # rubocop: disable Metrics/MethodLength
326
+ def process(event)
327
+ raise "The event parameter may not be nil" if event.nil?
328
+ Thread.current[:event] = event
329
+ event.mandate_cause_record_exists!
330
+ # TODO: This is probably racey so consider wrapping this in a semaphore.
331
+ return if event.antecedent_cancelled?
332
+ case event.activity
333
+ when String
334
+ result = event.parse_activity
335
+ # event.cause.inflib&.println(result)
336
+ # event.cause.println(result)
337
+ handle(event, result)
338
+ when Proc
339
+ result = event.call_activity
340
+ # event.cause.inflib&.print_evented_zmachine_result(event.call_activity, isolate: event.elemental?)
341
+ # event.cause.print_evented_zmachine_result(result, isolate: event.elemental?)
342
+ handle(event, result)
343
+ else
344
+ log.error "Event activity type is unknown: #{event.activity} (#{event.activity.class})"
345
+ end
346
+ rescue Inform::EventCauseRecordNotFoundError => e
347
+ log.error "Error refreshing cause object: #{e.message}"
348
+ rescue LocalJumpError => e
349
+ # It is a shame that this happens. It used to not happen, so long
350
+ # as event.activity was wrapped in lambda(event.activity).
351
+ # Then one day, this exception began to be thrown again.
352
+ # TODO: Either use this kludgey hack to fix it:
353
+ # https://stackoverflow.com/questions/2946603/ruby-convert-proc-to-lambda
354
+ # https://stackoverflow.com/a/24965981/328469
355
+ # Or, try to get jruby fixed:
356
+ # https://github.com/jruby/jruby/issues/4369 (2016)
357
+ # https://github.com/jruby/jruby/pull/4423 (2017)
358
+ # https://github.com/jruby/jruby/issues/4577 (2017)
359
+ log.warn "Event block used return keyword: #{e.message}"
360
+ log.debug "Local jump error: #{e.message}"
361
+ e.backtrace.each { |t| log.warn t }
362
+ rescue SystemExit => e
363
+ raise e
364
+ rescue StandardError => e
365
+ log << "\n"
366
+ log.error "Unexpected error processing #{event}: #{e.message}", e
367
+ event.callstack.each { |t| log.error t }
368
+ ensure
369
+ Thread.current[:event] = nil
370
+ end
371
+ # rubocop: enable Metrics/AbcSize
372
+ # rubocop: enable Metrics/CyclomaticComplexity
373
+ # rubocop: enable Metrics/MethodLength
374
+
375
+ # rubocop: disable Metrics/AbcSize
376
+ # rubocop: disable Metrics/CyclomaticComplexity
377
+ # rubocop: disable Metrics/MethodLength
378
+ def invoke(event)
379
+ raise "The event parameter may not be nil" if event.nil?
380
+ event.antecedent << event if !event.antecedent.nil? && !event.antecedent.concluded?
381
+ se = Thread.current[:event]
382
+ Thread.current[:event] = event
383
+ log.debug "Queueing #{event}"
384
+ invoke_event(event)
385
+ add_future_callback(event, Inform::Events.pool)
386
+ event.cause.events << event
387
+ Inform::Events.active_objects << event.cause unless Inform::Events.active_objects.include?(event.cause)
388
+ return defined?(Java) ? event.java_get : event.get
389
+ rescue StandardError => e
390
+ log.error "Unexpected error invoking #{event}: #{e.message}", e
391
+ event.callstack.each { |t| log.error t }
392
+ ensure
393
+ Thread.current[:event] = se
394
+ end
395
+ # rubocop: enable Metrics/AbcSize
396
+ # rubocop: enable Metrics/CyclomaticComplexity
397
+ # rubocop: enable Metrics/MethodLength
398
+
399
+ def invoke_event(event)
400
+ event.future = if defined?(Java)
401
+ Inform::Events.pool.java_send(:submit, [java.lang.Runnable], event)
402
+ else
403
+ -> { event.call }
404
+ end
405
+ end
406
+
407
+ def handle(event, result)
408
+ case result
409
+ when String
410
+ event.cause.println result
411
+ end
412
+ end
413
+
414
+ def deactivate(obj)
415
+ raise "The obj parameter may not be nil" if obj.nil?
416
+ obj.events.remove self
417
+ Inform::Events.active_objects.remove obj if obj.events.empty?
418
+ end
419
+
420
+ def cancelled
421
+ successors.each(&:on_cancelled_antecedent)
422
+ rescue StandardError => e
423
+ log.error "Error cancelling #{self}: #{e.message}", e
424
+ ensure
425
+ deactivate cause
426
+ end
427
+
428
+ # rubocop: disable Metrics/AbcSize
429
+ # rubocop: disable Metrics/CyclomaticComplexity
430
+ def completed
431
+ semaphore.synchronize { self.concluded = true }
432
+ return if !antecedent.nil? && antecedent.cancelled? && terminus.nil?
433
+ return if cancelled? && terminus.nil?
434
+ successors.each { |event| schedule event }
435
+ rescue StandardError => e
436
+ log.error "Error completing #{self}: #{e.message}", e
437
+ ensure
438
+ deactivate cause
439
+ end
440
+ # rubocop: enable Metrics/AbcSize
441
+ # rubocop: enable Metrics/CyclomaticComplexity
442
+ end
443
+ # module PrivateEventMethods
444
+
445
+ # The EventInitializationMethods module
446
+ module EventInitializationMethods
447
+ include EventConstants
448
+
449
+ # rubocop: disable Metrics/AbcSize
450
+ # rubocop: disable Metrics/MethodLength
451
+ def init_fields(params, &block)
452
+ @successors = defined?(Java) ? java.util.concurrent.CopyOnWriteArrayList.new : []
453
+ @time = Time.ms
454
+ @cause = params[:cause]
455
+ @name = params.fetch(:name, format(EventNameTemplate, time: @time))
456
+ @args = params[:args]
457
+ # Converting a given block to a lambda supposedly enables the use of return
458
+ # statements inside the event activity blocks.
459
+ # TODO: Cite documentation source.
460
+ # TODO: Implement tests.
461
+ # TODO: Verify this change prevents the error:
462
+ # Event block used return keyword: unexpected return
463
+ @activity = params.fetch(:activity, block_given? ? block.to_lambda : @name)
464
+ @antecedent = params[:antecedent]
465
+ @time_delay = params.fetch(:delay, 0)
466
+ @terminus = params[:terminus] ? self : nil
467
+ @type = params.fetch(:type, :integral)
468
+ @when = params.fetch(:when, :eventually)
469
+ @callstack = caller.slice((%i[immediately elementally].include?(@when) ? 4 : 3)..-1)
470
+ @concluded = false
471
+ # @cause = @cause.player if @cause.is_a? InformLibrary # TODO: Test
472
+ @identity = generate_identity
473
+ @context = params[:context]
474
+ end
475
+ # rubocop: enable Metrics/AbcSize
476
+ # rubocop: enable Metrics/MethodLength
477
+
478
+ # rubocop: disable Metrics/AbcSize
479
+ # rubocop: disable Metrics/MethodLength
480
+ def generate_identity
481
+ s = format(EventCauseIdentityTemplate, identity: @cause.identity, name: @cause.name)
482
+ if !@callstack.empty?
483
+ trace = @callstack.first.gsub(Inform::Runtime.project_dir_path, '')
484
+ location, line_number, method_name = CallerPattern.match(trace)&.captures
485
+ s << format(EventSourceFullTemplate,
486
+ method_name: method_name, source_location: location, source_line_number: line_number)
487
+ elsif @activity.respond_to?(:source_location) || @activity.is_a?(Proc)
488
+ activity_source_location_enumerator = @activity.source_location.each
489
+ source_location = activity_source_location_enumerator.next.gsub(Inform::Runtime.project_dir_path, '')
490
+ source_line_number = activity_source_location_enumerator.next
491
+ s << format(
492
+ EventSourceTemplate, source_location: source_location, source_line_number: source_line_number)
493
+ elsif name != @activity.to_s
494
+ s << format(EventActivityTemplate, activity: @activity)
495
+ end
496
+ s << format(EventAntecedantTemplate, antecedent: @antecedent) unless @antecedent.nil?
497
+ s
498
+ end
499
+ # rubocop: enable Metrics/AbcSize
500
+ # rubocop: enable Metrics/MethodLength
501
+
502
+ def init_context(context, antecedent, cause)
503
+ return contextualize(context) unless context.nil?
504
+ if antecedent&.cause.respond_to?(:inflib)
505
+ return contextualize(antecedent.cause.inflib) unless antecedent&.cause&.inflib.nil?
506
+ end
507
+ contextualize(cause) unless cause.nil? # TODO: Maybe remove
508
+ end
509
+
510
+ def defer(event)
511
+ event.antecedent << event
512
+ end
513
+
514
+ def schedule_or_defer(event)
515
+ return defer(event) if !event.antecedent.nil? && !event.antecedent.concluded?
516
+ schedule(event) if event.antecedent.nil? || event.antecedent.cancelled?
517
+ end
518
+ end
519
+ # module EventInitializationMethods
520
+
521
+ # The Inform module
522
+ module Inform
523
+ # The Inform::Event class
524
+ class Event
525
+ include PublicEventMethods
526
+ include PrivateEventMethods
527
+
528
+ attr_reader :cause, :time, :time_delay, :name, :args, :type, :context,
529
+ :activity, :terminus, :semaphore, :identity
530
+ attr_accessor :antecedent, :successors, :future, :concluded, :callstack
531
+
532
+ def initialize(params = {}, &block)
533
+ @semaphore = Mutex.new
534
+ init_fields(params, &block)
535
+ init_context(@context, @antecedent, @cause)
536
+ invoke(self)
537
+ end
538
+
539
+ private
540
+
541
+ include EventInitializationMethods
542
+ end
543
+
544
+ # The Inform::Events module
545
+ module Events
546
+ REGISTRY = Struct.new(:memo).new({})
547
+ THREADS_PER_PROCESSOR = 10
548
+
549
+ def active_objects
550
+ @active_objects ||= defined?(Java) ? java.util.concurrent.ConcurrentLinkedQueue.new : []
551
+ end
552
+ module_function :active_objects
553
+
554
+ def possibilities
555
+ @possibilities ||= defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {}
556
+ end
557
+ module_function :possibilities
558
+
559
+ def object_events
560
+ Inform::Events.possibilities[identity] ||=
561
+ defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {}
562
+ Inform::Events.possibilities[identity]
563
+ end
564
+ module_function :object_events
565
+
566
+ def events(klass = Inform::Event)
567
+ object_events[klass] ||= defined?(ConcurrentLinkedQueue) ? ConcurrentLinkedQueue.new : []
568
+ end
569
+
570
+ def each(event_class = Inform::Event, &block)
571
+ events[event_class].each { |event| block.call(event) } if block_given?
572
+ end
573
+ module_function :each
574
+
575
+ # rubocop: disable Metrics/AbcSize
576
+ # rubocop: disable Metrics/CyclomaticComplexity
577
+ # rubocop: disable Metrics/MethodLength
578
+ def available_processors
579
+ return java.lang.Runtime.runtime.available_processors if defined?(Java)
580
+ case RbConfig::CONFIG['host_os']
581
+ when /darwin9/
582
+ if File.exist?(`which hwprefs`)
583
+ `hwprefs cpu_count`.strip.to_i
584
+ else
585
+ # Better than nothing
586
+ require 'etc'
587
+ Etc.nprocessors
588
+ end
589
+ when /darwin/
590
+ if File.exist?(`which hwprefs`)
591
+ `hwprefs thread_count`.strip.to_i
592
+ else
593
+ `sysctl -n hw.ncpu`.strip.to_i
594
+ end
595
+ when /linux/
596
+ `grep -c processor /proc/cpuinfo`.strip.to_i
597
+ when /freebsd/
598
+ `sysctl -n hw.ncpu`.strip.to_i
599
+ when /mswin|mingw/
600
+ require 'win32ole'
601
+ cpu = WIN32OLE.connect("winmgmts://").ExecQuery("select NumberOfCores from Win32_Processor")
602
+ cpu.to_enum.first.NumberOfCores
603
+ else
604
+ # Better than nothing
605
+ require 'etc'
606
+ Etc.nprocessors
607
+ end
608
+ end
609
+ # rubocop: enable Metrics/AbcSize
610
+ # rubocop: enable Metrics/CyclomaticComplexity
611
+ # rubocop: enable Metrics/MethodLength
612
+ module_function :available_processors
613
+
614
+ def scheduled_executor
615
+ REGISTRY.memo[:scheduled_executor] ||=
616
+ defined?(Java) ? init_java_scheduled_executor : init_scheduled_executor
617
+ end
618
+ module_function :scheduled_executor
619
+
620
+ def init_scheduled_executor
621
+ # TODO: Implement
622
+ # require 'ruby-concurrency'
623
+ # Concurrent::ScheduledThreadPoolExecutor.new(available_processors * THREADS_PER_PROCESSOR)
624
+ end
625
+ module_function :init_scheduled_executor
626
+
627
+ def init_java_scheduled_executor
628
+ executor = java.util.concurrent.Executors.newScheduledThreadPool(
629
+ available_processors * THREADS_PER_PROCESSOR)
630
+ executor.setRemoveOnCancelPolicy(true)
631
+ executor
632
+ end
633
+ module_function :init_java_scheduled_executor
634
+
635
+ def pooled_executor
636
+ REGISTRY.memo[:pooled_executor] ||=
637
+ defined?(Java) ? init_java_pooled_executor : init_pooled_executor
638
+ end
639
+ module_function :pooled_executor
640
+
641
+ def init_java_pooled_executor
642
+ Executors.newFixedThreadPool(available_processors * THREADS_PER_PROCESSOR)
643
+ end
644
+ module_function :init_java_pooled_executor
645
+
646
+ def scheduler
647
+ REGISTRY.memo[:scheduler] ||=
648
+ defined?(Java) ? init_java_scheduler : init_scheduler
649
+ end
650
+ module_function :scheduler
651
+
652
+ def init_scheduler
653
+ # TODO: Implement
654
+ # require 'ruby-concurrency'
655
+ # Concurrent::ThreadPoolExecutor.new(available_processors * THREADS_PER_PROCESSOR)
656
+ end
657
+ module_function :init_scheduler
658
+
659
+ def init_java_scheduler
660
+ com.google.common.util.concurrent.MoreExecutors.listeningDecorator(ScheduledExecutor)
661
+ end
662
+ module_function :init_java_scheduler
663
+
664
+ def pool
665
+ REGISTRY.memo[:pool] ||= defined?(Java) ? init_java_pool : init_pool
666
+ end
667
+ module_function :pool
668
+
669
+ def init_pool
670
+ # TODO: Implement
671
+ # require 'ruby-concurrency'
672
+ # Concurrent::ThreadPoolExecutor.new(available_processors * THREADS_PER_PROCESSOR)
673
+ end
674
+ module_function :init_pool
675
+
676
+ def init_java_pool
677
+ com.google.common.util.concurrent.MoreExecutors.listeningDecorator(pooled_executor)
678
+ end
679
+ module_function :init_java_pool
680
+ end
681
+ # module Events
682
+ end
683
+ # module Inform
684
+
685
+ # The Inform module
686
+ module Inform
687
+ # The Inform::Events module
688
+ module Events
689
+ DEFAULT_ADD_EVENT_PARAMS = { delay: 0 }.freeze
690
+
691
+ def register_callback(ctx, *args, &callback)
692
+ e = Thread.current[:event]
693
+ return true if e.nil?
694
+ e.chain({ context: ctx, args: args }, &callback)
695
+ true
696
+ end
697
+
698
+ def add_event(params = {}, event_params = { cause: self }, &block)
699
+ if (e = Thread.current[:event]).nil?
700
+ params = { command: params } unless params.respond_to?(:merge)
701
+ event_params[:name] = params.delete(:command) if params.include?(:command)
702
+ Inform::Event.new(event_params.merge(params), &block)
703
+ else
704
+ event_params[:name] = params.delete(:command) if params.include?(:command)
705
+ e.chain(event_params.merge(DEFAULT_ADD_EVENT_PARAMS.merge(event_params)), &block)
706
+ end
707
+ end
708
+ alias eventually add_event
709
+ alias later add_event
710
+ alias queue add_event
711
+ alias enqueue add_event
712
+
713
+ # TODO: Figure out a way to determine if an existing event is
714
+ # already finished. That is, find a way to implicitly
715
+ # determine if any existing event is in the react_after phase
716
+ # instead of the react_before phase.
717
+ def event(params = {}, &block)
718
+ params = { command: params } unless params.respond_to?(:merge)
719
+ event_params = { cause: self }
720
+ event_params[:name] = params.delete(:command) if params.include?(:command)
721
+ Inform::Event.new(event_params.merge(params), &block)
722
+ end
723
+ alias new_event event
724
+ alias now event
725
+ alias react event
726
+ alias respond event
727
+
728
+ def delay(delay, &block)
729
+ Inform::Event.new({ cause: self, delay: delay }, &block)
730
+ end
731
+ alias after_delay delay
732
+ alias delayed_event delay
733
+
734
+ def immediately(ctx, &block)
735
+ Inform::Event.new({ cause: self, context: ctx, when: :immediately }, &block)
736
+ end
737
+
738
+ def elementally(ctx, &block)
739
+ Inform::Event.new({ cause: self, context: ctx, type: :elemental, when: :immediately }, &block)
740
+ end
741
+
742
+ def contextualize
743
+ Thread.current[:event]&.contextualize(self)
744
+ end
745
+
746
+ def active?
747
+ !events.empty?
748
+ end
749
+ alias already_active? active?
750
+
751
+ def interrupt(event_class = nil)
752
+ interruption = Thread.current[:event]
753
+ Inform::Events.each(event_class) do |event|
754
+ # Do not artificially terminate the source of an interruption.
755
+ next if event == interruption
756
+
757
+ # Parameterizing Future#cancel with false is supposed to
758
+ # abort the future without interrupting the executing thread.
759
+ # Not interrupting the event future's thread may have the
760
+ # consequence of a variety of data consistency issues.
761
+ # event.future.cancel false if event != interruption
762
+
763
+ # TODO: Test
764
+ event.future.cancel false
765
+ end
766
+ false
767
+ end
768
+ end
769
+ # module Events
770
+ end
771
+ # module Inform