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 +4 -4
- data/CHANGELOG.md +13 -3
- data/lib/debounced/callback.rb +4 -1
- data/lib/debounced/javascript/service.mjs +42 -25
- data/lib/debounced/no_server_error.rb +5 -0
- data/lib/debounced/service_proxy.rb +80 -56
- data/lib/debounced/socket_conflict_error.rb +7 -0
- data/lib/debounced/version.rb +1 -1
- data/lib/debounced.rb +4 -1
- metadata +18 -3
- data/lib/debounced/abort_signal.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f90306d666ec22b89d230a9e57a589d7e3c6f6cc4344f4107958e018d3fc8dab
|
4
|
+
data.tar.gz: 0d035ebc14a9e4737ce9031484d56ecf4d46ffdf9dc97232f5d6f933cd971128
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
## [
|
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
|
-
|
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.
|
data/lib/debounced/callback.rb
CHANGED
@@ -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
|
-
|
28
|
+
log('DebounceService client connection error');
|
23
29
|
this._client = null
|
24
30
|
this.handleError(err);
|
25
31
|
}
|
26
32
|
|
27
33
|
onClientDisconnected() {
|
28
|
-
|
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
|
-
|
46
|
+
log('DebounceService unknown message', message);
|
41
47
|
}
|
42
48
|
} catch (e) {
|
43
|
-
|
49
|
+
log('unable to parse message', e);
|
44
50
|
}
|
45
51
|
}
|
46
52
|
|
47
53
|
onClientConnected(socket) {
|
48
54
|
if (this._client) {
|
49
|
-
|
50
|
-
socket.
|
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
|
-
|
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
|
-
|
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
|
-
|
77
|
-
// Remove the existing socket file if it exists
|
83
|
+
cleanupDescriptor() {
|
78
84
|
if (fs.existsSync(this._socketDescriptor)) {
|
79
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
155
|
+
log('\n\n######\nERROR: ', err);
|
139
156
|
}
|
140
157
|
}
|
@@ -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
|
32
|
+
def reset_server
|
26
33
|
if socket.nil?
|
27
|
-
|
34
|
+
logger.warn("No connection to #{server_name}; unable to reset server.")
|
28
35
|
else
|
29
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
60
|
+
loop do
|
61
|
+
@listening = true
|
62
|
+
break if @abort_signal.true?
|
49
63
|
|
50
|
-
|
51
|
-
|
52
|
-
sleep(@wait_timeout)
|
53
|
-
next
|
54
|
-
end
|
64
|
+
message = receive_message_from_server
|
65
|
+
next unless message
|
55
66
|
|
56
|
-
|
57
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
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(
|
101
|
-
socket.send
|
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
|
109
|
-
"#{JSON.generate(
|
137
|
+
def serialize_message(message)
|
138
|
+
"#{JSON.generate(message)}#{DELIMITER}" # inject EOM delimiter (form feed character)
|
110
139
|
end
|
111
140
|
|
112
|
-
def
|
113
|
-
|
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
|
-
@
|
126
|
-
|
127
|
-
|
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
|
138
|
-
|
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
|
data/lib/debounced/version.rb
CHANGED
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.
|
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-
|
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
|