debounced 1.0.1 → 1.0.3

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: c04a25987dd47d790a0b44b4529025611c441fed620a19d9346cf9823b14e82b
4
- data.tar.gz: 0e01be8b3bfe43c432eee4daee54cfbca72515039fcd07a9ee8cb2f1605a054b
3
+ metadata.gz: f90306d666ec22b89d230a9e57a589d7e3c6f6cc4344f4107958e018d3fc8dab
4
+ data.tar.gz: 0d035ebc14a9e4737ce9031484d56ecf4d46ffdf9dc97232f5d6f933cd971128
5
5
  SHA512:
6
- metadata.gz: cf5c0932ded695167ec91dfe1d5bf808bb79a37d394edf1e005511803a4e7347ee9658df2ae6c42c2fb239444f0e19ac70dcf342827aac5835e3d991994f1b39
7
- data.tar.gz: 010a2121814282d3e8e2254339953fd78397bd545ab876b5a81ad6a88b08e3febd628b03e2e9d72f6b80f689770804629739c86af95ec542acad34e06e1b9bd4
6
+ metadata.gz: 798c7ff5e42b9b610684550c5a05081d430399cd5bb138eacfd7fd92d1a9fade82a22597df5b7e5c561129522b8177fa3f948498d71b2728575d6d954fd0492e
7
+ data.tar.gz: 1b64a24b034f701d913b1d7ac0cfa7192e421028378c17a7bf51c2792e9c52facda9a0c5038c289d95f7a7959cece9dedfa77b6dae2f149baef06294bd1dcf27
data/CHANGELOG.md CHANGED
@@ -5,11 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.1.0] - 2025-03-16
8
+ ## [1.0.0] - 2025-03-21
9
9
 
10
- ### Added
11
10
  - Initial release
12
11
  - Core functionality extracted from supply-side-platform
13
12
  - Ruby wrapper for Node.js event debouncing service
14
13
  - Configuration options for socket descriptor
15
- - Rails integration via Railtie
14
+
15
+ ## [1.0.1] - 2025-03-21
16
+
17
+ - Removed dependency on 'json/add/core' that seemed to interfere with Rails JSON serialization
18
+
19
+ ## [1.0.2] - 2025-03-26
20
+
21
+ - Fixed bug where Ruby (proxy) would allow messages to be sent to Javascript (server), even if the Ruby (proxy) was not listening for messages from Javascript (server)
22
+
23
+ ## [1.0.3] - 2025-03-26
24
+
25
+ - Avoid using a trace log level, which is not supported by the default Rails loggers.
@@ -40,6 +40,7 @@ module Debounced
40
40
  end
41
41
 
42
42
  def call
43
+ Debounced.configuration.logger.debug("Invoking callback #{method_name}")
43
44
  klass = Object.const_get(class_name)
44
45
  if klass.respond_to?(method_name)
45
46
  klass.send(method_name, *args, **kwargs)
@@ -47,7 +48,9 @@ module Debounced
47
48
  instance = klass.new(*args, **kwargs)
48
49
  instance.send(method_name)
49
50
  end
51
+ rescue StandardError => e
52
+ Debounced.configuration.logger.warn("Unable to invoke callback #{as_json}: #{e.message}")
53
+ Debounced.configuration.logger.warn(e.backtrace.join("\n"))
50
54
  end
51
-
52
55
  end
53
56
  end
@@ -1,6 +1,11 @@
1
1
  import net from 'net';
2
2
  import fs from 'fs';
3
3
 
4
+ function log(message, ...args) {
5
+ const timestamp = new Date().toISOString();
6
+ console.log(`${timestamp} - ${message}`, ...args);
7
+ }
8
+
4
9
  export default class DebounceService {
5
10
  constructor(socketDescriptor) {
6
11
  this._socketDescriptor = socketDescriptor;
@@ -11,6 +16,7 @@ export default class DebounceService {
11
16
  this.reset = this.reset.bind(this);
12
17
  this.listen = this.listen.bind(this);
13
18
  this.handleError = this.handleError.bind(this);
19
+ this.sendMessage = this.sendMessage.bind(this);
14
20
  this.onClientConnected = this.onClientConnected.bind(this);
15
21
  this.onClientDisconnected = this.onClientDisconnected.bind(this);
16
22
  this.onConnectionError = this.onConnectionError.bind(this);
@@ -19,13 +25,13 @@ export default class DebounceService {
19
25
  }
20
26
 
21
27
  onConnectionError(err) {
22
- console.log('DebounceService client connection error');
28
+ log('DebounceService client connection error');
23
29
  this._client = null
24
30
  this.handleError(err);
25
31
  }
26
32
 
27
33
  onClientDisconnected() {
28
- console.log('DebounceService client disconnected');
34
+ log('DebounceService client disconnected');
29
35
  this._client = null
30
36
  }
31
37
 
@@ -37,27 +43,28 @@ export default class DebounceService {
37
43
  } else if (object.type === 'reset') {
38
44
  this.reset();
39
45
  } else {
40
- console.log('DebounceService unknown message', message);
46
+ log('DebounceService unknown message', message);
41
47
  }
42
48
  } catch (e) {
43
- console.log('unable to parse message', e);
49
+ log('unable to parse message', e);
44
50
  }
45
51
  }
46
52
 
47
53
  onClientConnected(socket) {
48
54
  if (this._client) {
49
- console.log('DebounceService rejecting connection: already has a client');
50
- socket.end();
55
+ log('DebounceService rejecting connection: client already connected');
56
+ this.sendMessage(socket, JSON.stringify({ type: 'rejectClient'}));
57
+ socket.destroy();
51
58
  return;
52
59
  }
53
60
 
54
- console.log('DebounceService client connected');
61
+ log('DebounceService client connected');
55
62
  let connectionBuffer = '';
56
63
  this._client = socket;
57
64
  socket.on('end', this.onClientDisconnected)
58
65
  socket.on('error', this.onConnectionError)
59
66
  socket.on('data', data => {
60
- console.log('DebounceService data received', data.toString());
67
+ log('DebounceService data received', data.toString());
61
68
  connectionBuffer += data.toString();
62
69
  const messages = connectionBuffer.split('\f');
63
70
  connectionBuffer = ''
@@ -73,48 +80,58 @@ export default class DebounceService {
73
80
  this.server.on('error', this.handleError);
74
81
  }
75
82
 
76
- listen() {
77
- // Remove the existing socket file if it exists
83
+ cleanupDescriptor() {
78
84
  if (fs.existsSync(this._socketDescriptor)) {
79
- console.log('DebounceEventService removing stale socket file ', this._socketDescriptor);
85
+ log('DebounceEventService removing stale socket file ', this._socketDescriptor);
80
86
  fs.unlinkSync(this._socketDescriptor);
81
87
  }
88
+ }
89
+
90
+ listen() {
91
+ // Remove the existing socket file if it exists
92
+ this.cleanupDescriptor()
82
93
 
83
94
  process.on('exit', (code) => {
84
- console.log(`Process exiting with code: ${code}`);
95
+ log(`Process exiting with code: ${code}`);
85
96
  this.server.close();
97
+ this.cleanupDescriptor()
86
98
  });
87
99
 
88
100
  process.on('SIGTERM', () => {
89
- console.log('Process received SIGTERM');
101
+ log('Process received SIGTERM');
90
102
  this.server.close();
103
+ this.cleanupDescriptor()
91
104
  process.exit(0);
92
105
  });
93
106
 
94
107
  process.on('SIGINT', () => {
95
- console.log('Process received SIGINT');
108
+ log('Process received SIGINT');
96
109
  this.server.close();
110
+ this.cleanupDescriptor()
97
111
  process.exit(0);
98
112
  });
99
113
 
100
114
  this.server.listen(this._socketDescriptor, () => {
101
- console.log('DebounceService listening on', this._socketDescriptor);
115
+ log('DebounceService listening on', this._socketDescriptor);
102
116
  });
103
117
  }
104
118
 
119
+ sendMessage(socket, message) {
120
+ try {
121
+ socket.write(message);
122
+ socket.write("\f");
123
+ } catch (err) {
124
+ this.handleError(err);
125
+ }
126
+ }
127
+
105
128
  publishEvent(descriptor, callback) {
106
- console.log(`Debounce period expired for ${descriptor}`);
129
+ log(`Debounce period expired for ${descriptor} - sending to client`);
107
130
  const message = JSON.stringify({
108
131
  type: 'publishEvent',
109
132
  callback: callback
110
133
  });
111
-
112
- try {
113
- this._client.write(message);
114
- this._client.write("\f");
115
- } catch (err) {
116
- this.handleError(err);
117
- }
134
+ this.sendMessage(this._client, message);
118
135
  }
119
136
 
120
137
  debounceEvent({ descriptor, timeout, callback }) {
@@ -122,7 +139,7 @@ export default class DebounceService {
122
139
  clearTimeout(this._timers[descriptor]);
123
140
  }
124
141
 
125
- console.log("Debouncing", descriptor);
142
+ log("Debouncing", descriptor);
126
143
  this._timers[descriptor] = setTimeout(() => {
127
144
  delete this._timers[descriptor];
128
145
  this.publishEvent(descriptor, callback);
@@ -135,6 +152,6 @@ export default class DebounceService {
135
152
  }
136
153
 
137
154
  handleError(err) {
138
- console.log('\n\n######\nERROR: ', err);
155
+ log('\n\n######\nERROR: ', err);
139
156
  }
140
157
  }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module Debounced
3
+ class NoServerError < StandardError
4
+ end
5
+ end
@@ -10,10 +10,17 @@ module Debounced
10
10
  class ServiceProxy
11
11
  DELIMITER = "\f".freeze
12
12
 
13
+ attr_reader :logger, :wait_timeout, :listening
14
+
13
15
  def initialize
14
16
  @wait_timeout = Debounced.configuration.wait_timeout
17
+ @logger = Debounced.configuration.logger
18
+ @listening = false
19
+ @mutex = Mutex.new
15
20
  end
16
21
 
22
+ ###
23
+ # @param [Concurrent::AtomicBoolean] abort_signal set to true to stop listening for messages
17
24
  def listen(abort_signal = nil)
18
25
  Thread.new do
19
26
  receive(abort_signal)
@@ -22,58 +29,61 @@ module Debounced
22
29
 
23
30
  ###
24
31
  # Send message to server to reset its state. Useful for automated testing.
25
- def reset
32
+ def reset_server
26
33
  if socket.nil?
27
- log_debug("No connection to #{server_name}; unable to reset server.")
34
+ logger.warn("No connection to #{server_name}; unable to reset server.")
28
35
  else
29
- log_debug("Resetting #{server_name}")
36
+ logger_trace { "Resetting #{server_name}" }
30
37
  transmit({ type: 'reset' })
31
38
  end
32
39
  end
33
40
 
34
41
  def debounce_activity(activity_descriptor, timeout, callback)
35
- if socket.nil?
36
- log_debug("No connection to #{server_name}; skipping debounce step.")
37
- callback.call
38
- else
39
- log_debug("Debouncing #{activity_descriptor} to #{server_name}")
40
- transmit(build_request(activity_descriptor, timeout, callback))
42
+ SemanticLogger.tagged("send") do
43
+ if !listening || socket.nil?
44
+ logger.debug { "No connection to #{server_name}; skipping debounce step." }
45
+ callback.call
46
+ else
47
+ logger_trace { "Sending #{activity_descriptor} to #{server_name}" }
48
+ transmit(build_request(activity_descriptor, timeout, callback))
49
+ end
41
50
  end
42
51
  end
43
52
 
53
+ ###
54
+ # @param [Concurrent::AtomicBoolean] abort_signal set to true to stop listening for messages
44
55
  def receive(abort_signal = nil)
45
- log_debug("Listening for messages from #{server_name}...")
56
+ @abort_signal = abort_signal || Concurrent::AtomicBoolean.new
57
+ SemanticLogger.tagged("receive") do
58
+ logger.info { "Listening for messages from #{server_name}..." }
46
59
 
47
- loop do
48
- break if abort_signal&.set?
60
+ loop do
61
+ @listening = true
62
+ break if @abort_signal.true?
49
63
 
50
- if socket.nil?
51
- log_debug("Waiting for #{server_name} to start...")
52
- sleep(@wait_timeout)
53
- next
54
- end
64
+ message = receive_message_from_server
65
+ next unless message
55
66
 
56
- log_debug("Waiting for data from #{server_name}...")
57
- message = socket.gets(DELIMITER, chomp: true)
58
- if message.nil?
59
- log_info("Server #{server_name} ended connection")
60
- close
61
- break
62
- end
67
+ payload = deserialize_message(message)
68
+ raise SocketConflictError if payload['type'] == 'rejectClient'
63
69
 
64
- log_debug("Received #{message}")
65
- payload = deserialize_payload(message)
66
- log_debug("Parsed #{payload}")
67
- next unless payload['type'] == 'publishEvent'
70
+ instantiate_callback(payload['callback']).call
71
+ rescue Debounced::NoServerError => e
72
+ logger.debug e.message
73
+ sleep wait_timeout
74
+ end
68
75
 
69
- instantiate_callback(payload['callback']).call
70
- rescue IO::TimeoutError
71
- # Ignored - normal flow of loop: check abort_signal (L48), get data (L56), timeout waiting for data (69)
76
+ close
72
77
  end
73
- rescue StandardError => e
74
- log_warn("Unable to listen for messages from #{server_name}: #{e.message}")
75
- log_warn(e.backtrace.join("\n"))
78
+ rescue SocketConflictError, StandardError => e
79
+ logger.warn("Unable to listen for messages from #{server_name}: #{e.message}")
80
+ logger.warn(e.backtrace.join("\n"))
76
81
  ensure
82
+ @listening = false
83
+ end
84
+
85
+ def stop
86
+ @abort_signal.make_true
77
87
  end
78
88
 
79
89
  private
@@ -81,11 +91,30 @@ module Debounced
81
91
  def close
82
92
  return unless @socket
83
93
 
84
- log_info("Closing connection to #{server_name}")
94
+ logger.debug("Closing connection to #{server_name}")
85
95
  @socket.close
86
96
  @socket = nil
87
97
  end
88
98
 
99
+ def receive_message_from_server
100
+ raise NoServerError, "#{server_name} at #{socket_descriptor} not running" if socket.nil?
101
+
102
+ logger_trace { "Waiting for data from #{server_name}..." }
103
+ message = socket.gets(DELIMITER, chomp: true)
104
+ unless message
105
+ close
106
+ sleep wait_timeout
107
+ end
108
+ message
109
+ rescue IO::TimeoutError
110
+ logger_trace { "Timeout waiting for data" }
111
+ sleep wait_timeout
112
+ nil
113
+ rescue Errno::EPIPE, IOError, Errno::ECONNRESET
114
+ close
115
+ raise NoServerError, "#{server_name} at #{socket_descriptor} not running"
116
+ end
117
+
89
118
  def build_request(descriptor, timeout, callback)
90
119
  {
91
120
  type: 'debounceEvent',
@@ -97,20 +126,21 @@ module Debounced
97
126
  }
98
127
  end
99
128
 
100
- def transmit(request)
101
- socket.send serialize_payload(request), 0
129
+ def transmit(message)
130
+ socket.send serialize_message(message), 0
102
131
  end
103
132
 
104
133
  def server_name
105
134
  'DebounceEventServer'
106
135
  end
107
136
 
108
- def serialize_payload(payload)
109
- "#{JSON.generate(payload)}#{DELIMITER}" # inject EOM delimiter (form feed character)
137
+ def serialize_message(message)
138
+ "#{JSON.generate(message)}#{DELIMITER}" # inject EOM delimiter (form feed character)
110
139
  end
111
140
 
112
- def deserialize_payload(payload)
113
- JSON.parse(payload)
141
+ def deserialize_message(message)
142
+ logger_trace { "Deserializing #{message}" }
143
+ JSON.parse(message)
114
144
  end
115
145
 
116
146
  def instantiate_callback(data)
@@ -122,28 +152,22 @@ module Debounced
122
152
  end
123
153
 
124
154
  def socket
125
- @socket ||= begin
126
- log_debug("Connecting to #{server_name} at #{socket_descriptor}")
127
- UNIXSocket.new(socket_descriptor).tap { |s| s.timeout = @wait_timeout }
155
+ @mutex.synchronize do
156
+ return @socket if @socket
157
+
158
+ logger_trace { "Connecting to #{server_name} at #{socket_descriptor}" }
159
+ @socket = UNIXSocket.new(socket_descriptor).tap { |s| s.timeout = wait_timeout }
128
160
  end
129
161
  rescue Errno::ECONNREFUSED, Errno::ENOENT
130
162
  ###
131
163
  # Errno::ENOENT is raised if the socket file does not exist.
132
164
  # Errno::ECONNREFUSED is raised if the socket file exists but no process is listening on it.
133
- log_debug("#{server_name} is not running")
134
165
  nil
135
166
  end
136
-
137
- def log_debug(message)
138
- Debounced.configuration.logger.debug { message }
139
- end
140
-
141
- def log_info(message)
142
- Debounced.configuration.logger.info(message)
143
- end
144
-
145
- def log_warn(message)
146
- Debounced.configuration.logger.warn(message)
167
+
168
+ def logger_trace(&block)
169
+ logger.debug(&block) if Debounced.configuration.enable_trace_logging
147
170
  end
171
+
148
172
  end
149
173
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module Debounced
3
+ ###
4
+ # When the given socket is being used by another Ruby process, this error is raised.
5
+ class SocketConflictError < StandardError
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Debounced
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.3"
3
3
  end
data/lib/debounced.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'debounced/version'
2
2
  require 'debounced/railtie' if defined?(Rails)
3
+ require 'debounced/no_server_error'
4
+ require 'debounced/socket_conflict_error'
3
5
  require 'debounced/service_proxy'
4
6
  require 'debounced/callback'
5
7
  require 'semantic_logger'
@@ -18,7 +20,7 @@ module Debounced
18
20
  end
19
21
 
20
22
  class Configuration
21
- attr_accessor :socket_descriptor, :wait_timeout, :logger
23
+ attr_accessor :socket_descriptor, :wait_timeout, :logger, :enable_trace_logging
22
24
 
23
25
  def initialize
24
26
  @socket_descriptor = ENV['DEBOUNCED_SOCKET'] || '/tmp/app.debounceEvents'
@@ -26,6 +28,7 @@ module Debounced
26
28
  SemanticLogger.add_appender(file_name: 'debounced_proxy.log', formatter: :color)
27
29
  SemanticLogger.default_level = ENV.fetch('LOG_LEVEL', 'info')
28
30
  @logger = SemanticLogger['ServiceProxy']
31
+ @enable_trace_logging = ENV['TRACE_LOGGING'] == 'true'
29
32
  end
30
33
  end
31
34
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: debounced
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gary Passero
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-22 00:00:00.000000000 Z
10
+ date: 2025-03-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: json
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: 4.15.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: logger
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: rspec
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -81,12 +95,13 @@ files:
81
95
  - CHANGELOG.md
82
96
  - README.md
83
97
  - lib/debounced.rb
84
- - lib/debounced/abort_signal.rb
85
98
  - lib/debounced/callback.rb
86
99
  - lib/debounced/javascript/server.mjs
87
100
  - lib/debounced/javascript/service.mjs
101
+ - lib/debounced/no_server_error.rb
88
102
  - lib/debounced/railtie.rb
89
103
  - lib/debounced/service_proxy.rb
104
+ - lib/debounced/socket_conflict_error.rb
90
105
  - lib/debounced/version.rb
91
106
  - lib/tasks/debounced.rake
92
107
  homepage: https://github.com/flytedesk/debounced
@@ -1,23 +0,0 @@
1
- module Debounced
2
- ###
3
- # Allow an abort signal to be passed to blocking call for graceful shutdown.
4
- #
5
- # Inspired by the AbortController in the DOM, but made for Ruby
6
- # @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
7
- #
8
- # For notes about thread safety of this approach,
9
- # @see https://stackoverflow.com/questions/9620886/is-it-safe-to-set-the-boolean-value-in-thread-from-another-one
10
- class AbortSignal
11
- def initialize
12
- @abort = false
13
- end
14
-
15
- def abort
16
- @abort = true
17
- end
18
-
19
- def set?
20
- @abort
21
- end
22
- end
23
- end