debounced 0.1.16 → 0.1.17

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: beb111cd5043d97b903d9e89278437564f65fd5794f4531d6e5c6e57dca79859
4
+ data.tar.gz: b872450de202d28e51d32eac774b27238a328972a7b3363c0ca77d5b137897f4
5
5
  SHA512:
6
- metadata.gz: f758d65446f5fe6268be7e5cb1d90501188705b1fe55c378bfcc0012343b4971f57f6cc92de67d5fa44459628cb9d7e97f25c63b45b520027974cea9943f49c4
7
- data.tar.gz: 350077a5ca3cea461e0746b4fabc5cc43d92c2ce347f3fe316768bfe7bd6c1aa75eeb53d9b44993b7ea1fe59ab1d5be390be04caf39ca630de4e6c43a3b4f63f
6
+ metadata.gz: f70040e9197b72f7e8b91b6bae9d736a3f840c6da34817722949f23a4d82c6b798a9a0011345ca623ba7bd4fc2bab48fa498d41cebf70dd59a9d7459d277416d
7
+ data.tar.gz: af31341ddf36fa276c5ad79db3cecdf975795bd1ac130005f18b9cbbab880b157645aa7e2d5c63199a98b999eae1b85e9daee061a2717162c27567d1d11c2564
@@ -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, data) {
106
+ console.log(`Debounce period expired for ${descriptor}, publishing ${data.klass} event`);
107
+ const message = JSON.stringify({
108
+ type: 'publishEvent',
109
+ data: data
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, attributes, klass, timeout }) {
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, { attributes, klass });
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, object, timeout)
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
+ trigger_callback(object)
37
40
  else
38
41
  log_debug("Sending #{object.inspect} to #{server_name}")
39
- transmit(debounce_request(descriptor, object, timeout))
42
+ transmit(build_request(activity_descriptor, object, timeout))
40
43
  end
41
44
  end
42
45
 
@@ -47,25 +50,27 @@ 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
+ trigger_callback(instantiate_debounced_object(payload['data']))
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}")
@@ -81,24 +86,24 @@ module Debounced
81
86
  @socket = nil
82
87
  end
83
88
 
84
- def debounce_request(descriptor, event, timeout)
89
+ def build_request(descriptor, object, timeout)
85
90
  {
86
91
  type: 'debounceEvent',
87
92
  data: {
88
- timeout: timeout,
89
- descriptor: descriptor,
90
- klass: event.class.name,
91
- attributes: event_attributes(event)
93
+ timeout:,
94
+ descriptor:,
95
+ klass: object.class.name,
96
+ attributes: extract_attributes(object)
92
97
  }
93
98
  }
94
99
  end
95
100
 
96
- def event_attributes(event)
97
- if event.respond_to?(:attributes)
98
- event.attributes
101
+ def extract_attributes(object)
102
+ if object.respond_to?(:attributes)
103
+ object.attributes
99
104
  else
100
- event.instance_variables.each_with_object({}) do |var, hash|
101
- hash[var.to_s.delete('@')] = event.instance_variable_get(var)
105
+ object.instance_variables.each_with_object({}) do |var, hash|
106
+ hash[var.to_s.delete('@')] = object.instance_variable_get(var)
102
107
  end
103
108
  end
104
109
  end
@@ -112,21 +117,21 @@ module Debounced
112
117
  end
113
118
 
114
119
  def serialize_payload(payload)
115
- "#{ActiveSupport::JSON.encode(payload)}#{DELIMITER}" # inject EOM delimiter (form feed character)
120
+ "#{JSON.generate(payload)}#{DELIMITER}" # inject EOM delimiter (form feed character)
116
121
  end
117
122
 
118
123
  def deserialize_payload(payload)
119
- ActiveSupport::JSON.decode(payload)
124
+ JSON.parse(payload)
120
125
  end
121
126
 
122
- def object_callback(object)
123
- object.send(Debounced.configuration.callback_method, args)
127
+ def trigger_callback(object)
128
+ object.send(Debounced.configuration.callback_method)
124
129
  end
125
130
 
126
- def class_callback(data)
127
- klass = data['klass'].constantize
128
- data = data['attributes']
129
- klass.send(Debounced.configuration.callback_method, data)
131
+ def instantiate_debounced_object(data)
132
+ klass = Object.const_get(data['klass'])
133
+ data = data['attributes'].transform_keys(&:to_sym)
134
+ klass.new(**data)
130
135
  end
131
136
 
132
137
  def socket_descriptor
@@ -134,40 +139,28 @@ module Debounced
134
139
  end
135
140
 
136
141
  def socket
137
- @socket ||= UNIXSocket.new(socket_descriptor)
138
- .tap do |socket|
139
- socket.timeout = @wait_timeout
142
+ @socket ||= begin
143
+ log_debug("Connecting to #{server_name} at #{socket_descriptor}")
144
+ UNIXSocket.new(socket_descriptor).tap { it.timeout = @wait_timeout }
140
145
  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
146
  rescue Errno::ECONNREFUSED, Errno::ENOENT
147
+ ###
148
+ # Errno::ENOENT is raised if the socket file does not exist.
149
+ # Errno::ECONNREFUSED is raised if the socket file exists but no process is listening on it.
145
150
  log_debug("#{server_name} is not running")
146
151
  nil
147
152
  end
148
153
 
149
154
  def log_debug(message)
150
- if defined?(Rails)
151
- Rails.logger.debug { message }
152
- else
153
- puts "[DEBUG] #{message}" if ENV['DEBUG']
154
- end
155
+ Debounced.configuration.logger.debug { message }
155
156
  end
156
157
 
157
158
  def log_info(message)
158
- if defined?(Rails)
159
- Rails.logger.info(message)
160
- else
161
- puts "[INFO] #{message}"
162
- end
159
+ Debounced.configuration.logger.info(message)
163
160
  end
164
161
 
165
162
  def log_warn(message)
166
- if defined?(Rails)
167
- Rails.logger.warn(message)
168
- else
169
- puts "[WARNING] #{message}"
170
- end
163
+ Debounced.configuration.logger.warn(message)
171
164
  end
172
165
  end
173
166
  end
@@ -1,3 +1,3 @@
1
1
  module Debounced
2
- VERSION = "0.1.16"
2
+ VERSION = "0.1.17"
3
3
  end
data/lib/debounced.rb CHANGED
@@ -1,6 +1,7 @@
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 'semantic_logger'
4
5
 
5
6
  module Debounced
6
7
  class Error < StandardError; end
@@ -16,12 +17,15 @@ module Debounced
16
17
  end
17
18
 
18
19
  class Configuration
19
- attr_accessor :socket_descriptor, :wait_timeout, :callback_method
20
+ attr_accessor :socket_descriptor, :wait_timeout, :callback_method, :logger
20
21
 
21
22
  def initialize
22
23
  @socket_descriptor = ENV['DEBOUNCED_SOCKET'] || '/tmp/app.debounceEvents'
23
24
  @wait_timeout = ENV['DEBOUNCED_TIMEOUT']&.to_i || 3
24
25
  @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.17
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-21 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,8 @@ files:
97
82
  - README.md
98
83
  - lib/debounced.rb
99
84
  - lib/debounced/abort_signal.rb
100
- - lib/debounced/javascript/debounce.mjs
101
85
  - lib/debounced/javascript/server.mjs
86
+ - lib/debounced/javascript/service.mjs
102
87
  - lib/debounced/railtie.rb
103
88
  - lib/debounced/service_proxy.rb
104
89
  - 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
- }