debounced 0.1.16 → 0.1.19

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e67d6eb56f33212d1f33d07015a9b3b5ea913f8c614b49a45586bfa65c9877d
4
- data.tar.gz: fe8305b86ae64080de06614a0249b92f92dbac9e29e58b97c3250ad4a78063fd
3
+ metadata.gz: 5469038458279f90317b22acd88274c9a9cd5ddfe7d932e0fba6141230da1f65
4
+ data.tar.gz: 2776dc85b634f88d5f9ec7f4903de124a092455e25cffba08b91aaab903d7667
5
5
  SHA512:
6
- metadata.gz: f758d65446f5fe6268be7e5cb1d90501188705b1fe55c378bfcc0012343b4971f57f6cc92de67d5fa44459628cb9d7e97f25c63b45b520027974cea9943f49c4
7
- data.tar.gz: 350077a5ca3cea461e0746b4fabc5cc43d92c2ce347f3fe316768bfe7bd6c1aa75eeb53d9b44993b7ea1fe59ab1d5be390be04caf39ca630de4e6c43a3b4f63f
6
+ metadata.gz: 9559170057bc4e13012e8d3d7234d1d2e2d1ed9eaa7100c8b2120b5c5b7b0d57cebfd4aa7e881b059bb9a6475b1dd45c4854467a883d09f7e88e159636faea4f
7
+ data.tar.gz: 5dadd3b4312c8985a2e3ad44b7aceb948250031bd9ca0e26da903211a42dc7a9ff0b26f57fa3e30062a05947c7738af6b999c809548a1781ccfe08176be0fb13
@@ -0,0 +1,38 @@
1
+ require 'debug'
2
+
3
+ module Debounced
4
+ class Callback
5
+
6
+ attr_reader :class_name, :params, :method_name, :method_params
7
+ def initialize(class_name:, params:, method_name:, method_params:)
8
+ @class_name = class_name.to_s
9
+ @params = params || {}
10
+ @method_name = method_name.to_s
11
+ @method_params = method_params || []
12
+ end
13
+
14
+ def self.json_create(data)
15
+ new(
16
+ class_name: data['class_name'],
17
+ params: data['params'],
18
+ method_name: data['method_name'],
19
+ method_params: data['method_params']
20
+ )
21
+ end
22
+
23
+ def as_json
24
+ {
25
+ class_name:,
26
+ params:,
27
+ method_name:,
28
+ method_params:
29
+ }
30
+ end
31
+
32
+ def call
33
+ Object.const_get(class_name)
34
+ .new(**params.transform_keys(&:to_sym))
35
+ .send(method_name, *method_params)
36
+ end
37
+ end
38
+ end
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import DebounceEventService from './debounce.mjs'
3
+ import DebounceEventService from './service.mjs'
4
4
  let socketDescriptor = '/tmp/app.debounceEvents';
5
5
  if (process.argv.length > 2) {
6
6
  socketDescriptor = process.argv[2];
7
7
  }
8
+
8
9
  new DebounceEventService(socketDescriptor).listen();
@@ -0,0 +1,140 @@
1
+ import net from 'net';
2
+ import fs from 'fs';
3
+
4
+ export default class DebounceService {
5
+ constructor(socketDescriptor) {
6
+ this._socketDescriptor = socketDescriptor;
7
+ this._timers = {};
8
+ this._client = null;
9
+ this.publishEvent = this.publishEvent.bind(this);
10
+ this.debounceEvent = this.debounceEvent.bind(this);
11
+ this.reset = this.reset.bind(this);
12
+ this.listen = this.listen.bind(this);
13
+ this.handleError = this.handleError.bind(this);
14
+ this.onClientConnected = this.onClientConnected.bind(this);
15
+ this.onClientDisconnected = this.onClientDisconnected.bind(this);
16
+ this.onConnectionError = this.onConnectionError.bind(this);
17
+ this.handleMessage = this.handleMessage.bind(this);
18
+ this.configureServer();
19
+ }
20
+
21
+ onConnectionError(err) {
22
+ console.log('DebounceService client connection error');
23
+ this._client = null
24
+ this.handleError(err);
25
+ }
26
+
27
+ onClientDisconnected() {
28
+ console.log('DebounceService client disconnected');
29
+ this._client = null
30
+ }
31
+
32
+ handleMessage(message) {
33
+ try {
34
+ const object = JSON.parse(message);
35
+ if (object.type === 'debounceEvent') {
36
+ this.debounceEvent(object.data);
37
+ } else if (object.type === 'reset') {
38
+ this.reset();
39
+ } else {
40
+ console.log('DebounceService unknown message', message);
41
+ }
42
+ } catch (e) {
43
+ console.log('unable to parse message', e);
44
+ }
45
+ }
46
+
47
+ onClientConnected(socket) {
48
+ if (this._client) {
49
+ console.log('DebounceService rejecting connection: already has a client');
50
+ socket.end();
51
+ return;
52
+ }
53
+
54
+ console.log('DebounceService client connected');
55
+ let connectionBuffer = '';
56
+ this._client = socket;
57
+ socket.on('end', this.onClientDisconnected)
58
+ socket.on('error', this.onConnectionError)
59
+ socket.on('data', data => {
60
+ console.log('DebounceService data received', data.toString());
61
+ connectionBuffer += data.toString();
62
+ const messages = connectionBuffer.split('\f');
63
+ connectionBuffer = ''
64
+ if (!connectionBuffer.endsWith('\f')) {
65
+ connectionBuffer = messages.pop();
66
+ }
67
+ messages.forEach(this.handleMessage);
68
+ })
69
+ }
70
+
71
+ configureServer() {
72
+ this.server = net.createServer(this.onClientConnected)
73
+ this.server.on('error', this.handleError);
74
+ }
75
+
76
+ listen() {
77
+ // Remove the existing socket file if it exists
78
+ if (fs.existsSync(this._socketDescriptor)) {
79
+ console.log('DebounceEventService removing stale socket file ', this._socketDescriptor);
80
+ fs.unlinkSync(this._socketDescriptor);
81
+ }
82
+
83
+ process.on('exit', (code) => {
84
+ console.log(`Process exiting with code: ${code}`);
85
+ this.server.close();
86
+ });
87
+
88
+ process.on('SIGTERM', () => {
89
+ console.log('Process received SIGTERM');
90
+ this.server.close();
91
+ process.exit(0);
92
+ });
93
+
94
+ process.on('SIGINT', () => {
95
+ console.log('Process received SIGINT');
96
+ this.server.close();
97
+ process.exit(0);
98
+ });
99
+
100
+ this.server.listen(this._socketDescriptor, () => {
101
+ console.log('DebounceService listening on', this._socketDescriptor);
102
+ });
103
+ }
104
+
105
+ publishEvent(descriptor, callback) {
106
+ console.log(`Debounce period expired for ${descriptor}`);
107
+ const message = JSON.stringify({
108
+ type: 'publishEvent',
109
+ callback: callback
110
+ });
111
+
112
+ try {
113
+ this._client.write(message);
114
+ this._client.write("\f");
115
+ } catch (err) {
116
+ this.handleError(err);
117
+ }
118
+ }
119
+
120
+ debounceEvent({ descriptor, timeout, callback }) {
121
+ if (this._timers[descriptor]) {
122
+ clearTimeout(this._timers[descriptor]);
123
+ }
124
+
125
+ console.log("Debouncing", descriptor);
126
+ this._timers[descriptor] = setTimeout(() => {
127
+ delete this._timers[descriptor];
128
+ this.publishEvent(descriptor, callback);
129
+ }, timeout * 1000);
130
+ }
131
+
132
+ reset() {
133
+ Object.values(this._timers).forEach(timerID => clearTimeout(timerID));
134
+ this._timers = {};
135
+ }
136
+
137
+ handleError(err) {
138
+ console.log('\n\n######\nERROR: ', err);
139
+ }
140
+ }
@@ -1,7 +1,14 @@
1
1
  require 'socket'
2
- require 'active_support/json'
2
+ require 'json'
3
+ require 'json/add/core'
4
+ require 'debug'
3
5
 
4
6
  module Debounced
7
+ ###
8
+ # Ruby interface to the debounce service
9
+ # Input is an activity descriptor, and an object.
10
+ # When the activity is debounced, a callback method is invoked on the object.
11
+ # Assumes the object class has an initializer that accepts a hash of attributes, which are the instance variables
5
12
  class ServiceProxy
6
13
  DELIMITER = "\f".freeze
7
14
 
@@ -11,16 +18,12 @@ module Debounced
11
18
 
12
19
  def listen(abort_signal = nil)
13
20
  Thread.new do
14
- if defined?(Rails)
15
- Rails.application.executor.wrap do
16
- receive(abort_signal)
17
- end
18
- else
19
- receive(abort_signal)
20
- end
21
+ receive(abort_signal)
21
22
  end
22
23
  end
23
24
 
25
+ ###
26
+ # Send message to server to reset its state. Useful for automated testing.
24
27
  def reset
25
28
  if socket.nil?
26
29
  log_debug("No connection to #{server_name}; unable to reset server.")
@@ -30,13 +33,13 @@ module Debounced
30
33
  end
31
34
  end
32
35
 
33
- def debounce(descriptor, object, timeout)
36
+ def debounce_activity(activity_descriptor, timeout, callback)
34
37
  if socket.nil?
35
- log_debug("No connection to #{server_name}; publishing event immediately.")
36
- object_callback(object)
38
+ log_debug("No connection to #{server_name}; skipping debounce step.")
39
+ callback.call
37
40
  else
38
- log_debug("Sending #{object.inspect} to #{server_name}")
39
- transmit(debounce_request(descriptor, object, timeout))
41
+ log_debug("Debouncing #{activity_descriptor} to #{server_name}")
42
+ transmit(build_request(activity_descriptor, timeout, callback))
40
43
  end
41
44
  end
42
45
 
@@ -47,28 +50,32 @@ module Debounced
47
50
  break if abort_signal&.set?
48
51
 
49
52
  if socket.nil?
50
- log_debug("Waiting for #{server_name}...")
53
+ log_debug("Waiting for #{server_name} to start...")
51
54
  sleep(@wait_timeout)
52
55
  next
53
56
  end
54
57
 
58
+ log_debug("Waiting for data from #{server_name}...")
55
59
  message = socket.gets(DELIMITER, chomp: true)
56
-
57
- # gets => nil when server crashed.... try to reconnect
58
60
  if message.nil?
61
+ log_info("Server #{server_name} ended connection")
59
62
  close
60
- next
63
+ break
61
64
  end
62
65
 
66
+ log_debug("Received #{message}")
63
67
  payload = deserialize_payload(message)
68
+ log_debug("Parsed #{payload}")
64
69
  next unless payload['type'] == 'publishEvent'
65
70
 
66
- class_callback(payload['data'])
71
+ instantiate_callback(payload['callback']).call
67
72
  rescue IO::TimeoutError
68
- # Ignored
73
+ # Ignored - normal flow of loop: check abort_signal (L48), get data (L56), timeout waiting for data (69)
69
74
  end
70
75
  rescue StandardError => e
71
76
  log_warn("Unable to listen for messages from #{server_name}: #{e.message}")
77
+ log_warn(e.backtrace.join("\n"))
78
+ ensure
72
79
  end
73
80
 
74
81
  private
@@ -81,28 +88,17 @@ module Debounced
81
88
  @socket = nil
82
89
  end
83
90
 
84
- def debounce_request(descriptor, event, timeout)
91
+ def build_request(descriptor, timeout, callback)
85
92
  {
86
93
  type: 'debounceEvent',
87
94
  data: {
88
- timeout: timeout,
89
- descriptor: descriptor,
90
- klass: event.class.name,
91
- attributes: event_attributes(event)
95
+ descriptor:,
96
+ timeout:,
97
+ callback: callback.as_json
92
98
  }
93
99
  }
94
100
  end
95
101
 
96
- def event_attributes(event)
97
- if event.respond_to?(:attributes)
98
- event.attributes
99
- else
100
- event.instance_variables.each_with_object({}) do |var, hash|
101
- hash[var.to_s.delete('@')] = event.instance_variable_get(var)
102
- end
103
- end
104
- end
105
-
106
102
  def transmit(request)
107
103
  socket.send serialize_payload(request), 0
108
104
  end
@@ -112,21 +108,15 @@ module Debounced
112
108
  end
113
109
 
114
110
  def serialize_payload(payload)
115
- "#{ActiveSupport::JSON.encode(payload)}#{DELIMITER}" # inject EOM delimiter (form feed character)
111
+ "#{JSON.generate(payload)}#{DELIMITER}" # inject EOM delimiter (form feed character)
116
112
  end
117
113
 
118
114
  def deserialize_payload(payload)
119
- ActiveSupport::JSON.decode(payload)
115
+ JSON.parse(payload)
120
116
  end
121
117
 
122
- def object_callback(object)
123
- object.send(Debounced.configuration.callback_method, args)
124
- end
125
-
126
- def class_callback(data)
127
- klass = data['klass'].constantize
128
- data = data['attributes']
129
- klass.send(Debounced.configuration.callback_method, data)
118
+ def instantiate_callback(data)
119
+ Callback.json_create(data)
130
120
  end
131
121
 
132
122
  def socket_descriptor
@@ -134,40 +124,28 @@ module Debounced
134
124
  end
135
125
 
136
126
  def socket
137
- @socket ||= UNIXSocket.new(socket_descriptor)
138
- .tap do |socket|
139
- socket.timeout = @wait_timeout
127
+ @socket ||= begin
128
+ log_debug("Connecting to #{server_name} at #{socket_descriptor}")
129
+ UNIXSocket.new(socket_descriptor).tap { |s| s.timeout = @wait_timeout }
140
130
  end
141
- ###
142
- # Errno::ENOENT is raised if the socket file does not exist.
143
- # Errno::ECONNREFUSED is raised if the socket file exists but no process is listening on it.
144
131
  rescue Errno::ECONNREFUSED, Errno::ENOENT
132
+ ###
133
+ # Errno::ENOENT is raised if the socket file does not exist.
134
+ # Errno::ECONNREFUSED is raised if the socket file exists but no process is listening on it.
145
135
  log_debug("#{server_name} is not running")
146
136
  nil
147
137
  end
148
138
 
149
139
  def log_debug(message)
150
- if defined?(Rails)
151
- Rails.logger.debug { message }
152
- else
153
- puts "[DEBUG] #{message}" if ENV['DEBUG']
154
- end
140
+ Debounced.configuration.logger.debug { message }
155
141
  end
156
142
 
157
143
  def log_info(message)
158
- if defined?(Rails)
159
- Rails.logger.info(message)
160
- else
161
- puts "[INFO] #{message}"
162
- end
144
+ Debounced.configuration.logger.info(message)
163
145
  end
164
146
 
165
147
  def log_warn(message)
166
- if defined?(Rails)
167
- Rails.logger.warn(message)
168
- else
169
- puts "[WARNING] #{message}"
170
- end
148
+ Debounced.configuration.logger.warn(message)
171
149
  end
172
150
  end
173
151
  end
@@ -1,3 +1,3 @@
1
1
  module Debounced
2
- VERSION = "0.1.16"
2
+ VERSION = "0.1.19"
3
3
  end
data/lib/debounced.rb CHANGED
@@ -1,6 +1,8 @@
1
- require "debounced/version"
2
- require "debounced/railtie" if defined?(Rails)
3
- require "debounced/service_proxy"
1
+ require 'debounced/version'
2
+ require 'debounced/railtie' if defined?(Rails)
3
+ require 'debounced/service_proxy'
4
+ require 'debounced/callback'
5
+ require 'semantic_logger'
4
6
 
5
7
  module Debounced
6
8
  class Error < StandardError; end
@@ -16,12 +18,14 @@ module Debounced
16
18
  end
17
19
 
18
20
  class Configuration
19
- attr_accessor :socket_descriptor, :wait_timeout, :callback_method
21
+ attr_accessor :socket_descriptor, :wait_timeout, :logger
20
22
 
21
23
  def initialize
22
24
  @socket_descriptor = ENV['DEBOUNCED_SOCKET'] || '/tmp/app.debounceEvents'
23
25
  @wait_timeout = ENV['DEBOUNCED_TIMEOUT']&.to_i || 3
24
- @callback_method = :publish
26
+ SemanticLogger.add_appender(file_name: 'debounced_proxy.log', formatter: :color)
27
+ SemanticLogger.default_level = ENV.fetch('LOG_LEVEL', 'info')
28
+ @logger = SemanticLogger['ServiceProxy']
25
29
  end
26
30
  end
27
31
  end
metadata CHANGED
@@ -1,92 +1,77 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: debounced
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.16
4
+ version: 0.1.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gary Passero
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-17 00:00:00.000000000 Z
10
+ date: 2025-03-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rails
13
+ name: json
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '7.0'
19
- - - ">="
20
- - !ruby/object:Gem::Version
21
- version: 7.0.0
18
+ version: 2.10.2
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - "~>"
27
24
  - !ruby/object:Gem::Version
28
- version: '7.0'
29
- - - ">="
30
- - !ruby/object:Gem::Version
31
- version: 7.0.0
25
+ version: 2.10.2
32
26
  - !ruby/object:Gem::Dependency
33
- name: rspec
27
+ name: semantic_logger
34
28
  requirement: !ruby/object:Gem::Requirement
35
29
  requirements:
36
30
  - - "~>"
37
31
  - !ruby/object:Gem::Version
38
- version: '3.0'
39
- type: :development
32
+ version: 4.15.0
33
+ type: :runtime
40
34
  prerelease: false
41
35
  version_requirements: !ruby/object:Gem::Requirement
42
36
  requirements:
43
37
  - - "~>"
44
38
  - !ruby/object:Gem::Version
45
- version: '3.0'
39
+ version: 4.15.0
46
40
  - !ruby/object:Gem::Dependency
47
- name: rubocop
41
+ name: rspec
48
42
  requirement: !ruby/object:Gem::Requirement
49
43
  requirements:
50
44
  - - "~>"
51
45
  - !ruby/object:Gem::Version
52
- version: '1.21'
46
+ version: '3.0'
53
47
  type: :development
54
48
  prerelease: false
55
49
  version_requirements: !ruby/object:Gem::Requirement
56
50
  requirements:
57
51
  - - "~>"
58
52
  - !ruby/object:Gem::Version
59
- version: '1.21'
53
+ version: '3.0'
60
54
  - !ruby/object:Gem::Dependency
61
- name: rubocop-rails
55
+ name: debug
62
56
  requirement: !ruby/object:Gem::Requirement
63
57
  requirements:
64
58
  - - "~>"
65
59
  - !ruby/object:Gem::Version
66
- version: '2.12'
67
- type: :development
68
- prerelease: false
69
- version_requirements: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - "~>"
72
- - !ruby/object:Gem::Version
73
- version: '2.12'
74
- - !ruby/object:Gem::Dependency
75
- name: rubocop-rspec
76
- requirement: !ruby/object:Gem::Requirement
77
- requirements:
78
- - - "~>"
60
+ version: '1.0'
61
+ - - ">="
79
62
  - !ruby/object:Gem::Version
80
- version: '2.4'
63
+ version: 1.0.0
81
64
  type: :development
82
65
  prerelease: false
83
66
  version_requirements: !ruby/object:Gem::Requirement
84
67
  requirements:
85
68
  - - "~>"
86
69
  - !ruby/object:Gem::Version
87
- version: '2.4'
88
- description: A Ruby wrapper for a NodeJS server that uses the JavaScript micro event
89
- loop to debounce events in Ruby applications
70
+ version: '1.0'
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 1.0.0
74
+ description: Leverage JavaScript micro-event loop to debounce events in Ruby applications
90
75
  email:
91
76
  - gary@flytedesk.com
92
77
  executables: []
@@ -97,8 +82,9 @@ files:
97
82
  - README.md
98
83
  - lib/debounced.rb
99
84
  - lib/debounced/abort_signal.rb
100
- - lib/debounced/javascript/debounce.mjs
85
+ - lib/debounced/callback.rb
101
86
  - lib/debounced/javascript/server.mjs
87
+ - lib/debounced/javascript/service.mjs
102
88
  - lib/debounced/railtie.rb
103
89
  - lib/debounced/service_proxy.rb
104
90
  - lib/debounced/version.rb
@@ -1,68 +0,0 @@
1
- import ipc from 'node-ipc';
2
- import fs from 'fs';
3
-
4
- const logLevel = process.env.LOG_LEVEL || 'info';
5
-
6
- export default class DebounceService {
7
- constructor(socketDescriptor) {
8
- this._socketDescriptor = socketDescriptor;
9
- this._timers = {};
10
- this.publishEvent = this.publishEvent.bind(this);
11
- this.debounceEvent = this.debounceEvent.bind(this);
12
- this.reset = this.reset.bind(this);
13
- this.listen = this.listen.bind(this);
14
- this.handleError = this.handleError.bind(this);
15
- this.configureServer();
16
- this.registerMessageTypes();
17
- }
18
-
19
- registerMessageTypes() {
20
- ipc.server.on('debounceEvent', this.debounceEvent);
21
- ipc.server.on('reset', this.reset);
22
- ipc.server.on('error', this.handleError);
23
- }
24
-
25
- configureServer() {
26
- // Remove the existing socket file if it exists
27
- if (fs.existsSync(this._socketDescriptor)) {
28
- console.log('DebounceEventService removing stale socket file ', this._socketDescriptor);
29
- fs.unlinkSync(this._socketDescriptor);
30
- }
31
-
32
- ipc.config.delimiter = '\f'; // incoming messages are terminated by a form feed character
33
- ipc.config.encoding = 'utf8';
34
- ipc.config.silent = (logLevel !== 'debug');
35
-
36
- ipc.serve(this._socketDescriptor);
37
- }
38
-
39
- listen() {
40
- console.log('DebounceService listening on', this._socketDescriptor);
41
- ipc.server.start();
42
- }
43
-
44
- publishEvent(descriptor, data) {
45
- console.log(`Debounce period expired for ${descriptor}, publishing ${data.klass} event`);
46
- ipc.server.broadcast("publishEvent", data);
47
- }
48
-
49
- debounceEvent({ descriptor, attributes, klass, timeout }) {
50
- if (this._timers[descriptor]) {
51
- clearTimeout(this._timers[descriptor]);
52
- }
53
-
54
- this._timers[descriptor] = setTimeout(() => {
55
- delete this._timers[descriptor];
56
- this.publishEvent(descriptor, { attributes, klass });
57
- }, timeout * 1000);
58
- }
59
-
60
- reset() {
61
- Object.values(this._timers).forEach(timerID => clearTimeout(timerID));
62
- this._timers = {};
63
- }
64
-
65
- handleError(err) {
66
- console.log('\n\n######\nERROR: ', err);
67
- }
68
- }