debounced 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e4b64c7302b08f7d4134cfad294a0dc7b39203ef2f860691ce09838a8e56cc1c
4
+ data.tar.gz: 78006a905bcab3220c6968d66b244670d96a40095f212261421e46965b612799
5
+ SHA512:
6
+ metadata.gz: 954ab3aaa0b3bc092a3119c53414be10a44ce576b5f195f18ccab33786514468d9e3febe4407130bd22a87ed4de50f8897f8f7bb7c7f57da145ae6d1ba4ce8e4
7
+ data.tar.gz: a47d2fbe9fd86cf486ad79088039c9503f6c2d56cfea2d1d1e982b960635232f0bcfe44a9ced45537ad305c5ed595cd7053da068bf5a048f9dad75cc9e2e9146
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-03-16
9
+
10
+ ### Added
11
+ - Initial release
12
+ - Core functionality extracted from supply-side-platform
13
+ - Ruby wrapper for Node.js event debouncing service
14
+ - Configuration options for socket descriptor
15
+ - Rails integration via Railtie
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Debounced
2
+
3
+ A Ruby gem that provides a NodeJS-based event debouncing service for Ruby applications. It uses the JavaScript micro event loop to efficiently debounce events.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'debounced'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ $ gem install debounced
23
+ ```
24
+
25
+ ## Dependencies
26
+
27
+ This gem requires Node.js to be installed on your system, as it uses a Node.js server to handle the debouncing logic. You'll need:
28
+
29
+ - Node.js >= 14.0.0
30
+ - npm (to install the required node packages)
31
+
32
+ After installing the gem, run:
33
+
34
+ ```bash
35
+ $ cd $(bundle show debounced)
36
+ $ npm install
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### Configuration
42
+
43
+ ```ruby
44
+ # config/initializers/debounced.rb
45
+ Debounced.configure do |config|
46
+ config.socket_descriptor = '/tmp/my_app.debounceEvents'
47
+ end
48
+ ```
49
+
50
+ ### Starting the server
51
+
52
+ You can start the debounce server with:
53
+
54
+ ```bash
55
+ $ bundle exec debounced-server
56
+ ```
57
+
58
+ Or in your application code:
59
+
60
+ ```ruby
61
+ require 'debounced'
62
+
63
+ # Start the listener thread
64
+ proxy = Debounced::EventServiceProxy.new
65
+ listener_thread = proxy.listen
66
+
67
+ # Debounce an event
68
+ class MyEvent
69
+ attr_reader :attributes
70
+
71
+ def initialize(data)
72
+ @attributes = data
73
+ end
74
+
75
+ def self.publish(data)
76
+ # Publish logic here
77
+ puts "Publishing event with data: #{data.inspect}"
78
+ end
79
+ end
80
+
81
+ event = MyEvent.new({ id: 1, message: "Hello World" })
82
+ proxy.debounce_event("my-event-123", event, 5) # Debounce for 5 seconds
83
+ ```
84
+
85
+ ## How It Works
86
+
87
+ 1. The gem creates a Unix socket for communication between Ruby and Node.js
88
+ 2. When you call `debounce_event`, it sends the event to the Node.js server
89
+ 3. The Node.js server keeps track of events with the same descriptor
90
+ 4. If another event with the same descriptor arrives before the timeout, it resets the timer
91
+ 5. When the timeout expires, it sends the event back to Ruby to be published
92
+
93
+ ## License
94
+
95
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import DebounceEventService from '../lib/assets/javascript/debounced/event_service.mjs';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ // Default socket path
11
+ let socketDescriptor = process.env.DEBOUNCED_SOCKET || '/tmp/app.debounceEvents';
12
+
13
+ // If test environment is specified
14
+ if (process.env.NODE_ENV === 'test') {
15
+ socketDescriptor = process.env.DEBOUNCED_TEST_SOCKET || '/tmp/app.debounceEvents.test';
16
+ }
17
+
18
+ // Allow socket path to be specified as command line argument
19
+ if (process.argv.length > 2) {
20
+ socketDescriptor = process.argv[2];
21
+ }
22
+
23
+ new DebounceEventService(socketDescriptor).listen();
@@ -0,0 +1,68 @@
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 DebounceEventService {
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('DebounceEventService 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
+ }
@@ -0,0 +1,174 @@
1
+ require 'socket'
2
+ require 'active_support/json'
3
+
4
+ module Debounced
5
+ class EventServiceProxy
6
+ DELIMITER = "\f".freeze
7
+ WAIT_TIMEOUT = 3
8
+
9
+ def listen(abort_signal = nil)
10
+ Thread.new do
11
+ if defined?(Rails)
12
+ Rails.application.executor.wrap do
13
+ receive(abort_signal)
14
+ end
15
+ else
16
+ receive(abort_signal)
17
+ end
18
+ end
19
+ end
20
+
21
+ def reset
22
+ if socket.nil?
23
+ log_debug("No connection to #{server_name}; unable to reset server.")
24
+ else
25
+ log_debug("Resetting #{server_name}")
26
+ transmit({ type: 'reset' })
27
+ end
28
+ end
29
+
30
+ def debounce_event(descriptor, event, timeout)
31
+ if socket.nil?
32
+ log_debug("No connection to #{server_name}; publishing event immediately.")
33
+ event.publish if event.respond_to?(:publish)
34
+ else
35
+ log_debug("Sending #{event_name(event)} event to #{server_name}")
36
+ transmit(debounce_request(descriptor, event, timeout))
37
+ end
38
+ end
39
+
40
+ def receive(abort_signal = nil)
41
+ log_debug("Listening for messages from #{server_name}...")
42
+
43
+ loop do
44
+ break if abort_signal&.set?
45
+
46
+ if socket.nil?
47
+ log_debug("Waiting for #{server_name}...")
48
+ sleep(WAIT_TIMEOUT)
49
+ next
50
+ end
51
+
52
+ message = socket.gets(DELIMITER, chomp: true)
53
+
54
+ # gets => nil when server crashed.... try to reconnect
55
+ if message.nil?
56
+ close
57
+ next
58
+ end
59
+
60
+ payload = deserialize_payload(message)
61
+ next unless payload['type'] == 'publishEvent'
62
+
63
+ publish_event(payload['data'])
64
+ rescue IO::TimeoutError
65
+ # Ignored
66
+ end
67
+ rescue StandardError => e
68
+ log_warn("Unable to listen for messages from #{server_name}: #{e.message}")
69
+ end
70
+
71
+ private
72
+
73
+ def close
74
+ return unless @socket
75
+
76
+ log_info("Closing connection to #{server_name}")
77
+ @socket.close
78
+ @socket = nil
79
+ end
80
+
81
+ def publish_event(data)
82
+ event_klass = data['klass'].constantize
83
+ event_data = data['attributes']
84
+ event_klass.publish(event_data)
85
+ end
86
+
87
+ def debounce_request(descriptor, event, timeout)
88
+ {
89
+ type: 'debounceEvent',
90
+ data: {
91
+ timeout: timeout,
92
+ descriptor: descriptor,
93
+ klass: event.class.name,
94
+ attributes: event_attributes(event)
95
+ }
96
+ }
97
+ end
98
+
99
+ def event_attributes(event)
100
+ if event.respond_to?(:attributes)
101
+ event.attributes
102
+ else
103
+ event.instance_variables.each_with_object({}) do |var, hash|
104
+ hash[var.to_s.delete('@')] = event.instance_variable_get(var)
105
+ end
106
+ end
107
+ end
108
+
109
+ def event_name(event)
110
+ if event.respond_to?(:event_name)
111
+ event.event_name
112
+ else
113
+ event.class.name
114
+ end
115
+ end
116
+
117
+ def transmit(request)
118
+ socket.send serialize_payload(request), 0
119
+ end
120
+
121
+ def server_name
122
+ 'DebounceEventServer'
123
+ end
124
+
125
+ def serialize_payload(payload)
126
+ "#{ActiveSupport::JSON.encode(payload)}#{DELIMITER}" # inject EOM delimiter (form feed character)
127
+ end
128
+
129
+ def deserialize_payload(payload)
130
+ ActiveSupport::JSON.decode(payload)
131
+ end
132
+
133
+ def socket_descriptor
134
+ Debounced.configuration.socket_descriptor
135
+ end
136
+
137
+ def socket
138
+ @socket ||= UNIXSocket.new(socket_descriptor)
139
+ .tap do |socket|
140
+ socket.timeout = WAIT_TIMEOUT
141
+ end
142
+ ###
143
+ # Errno::ENOENT is raised if the socket file does not exist.
144
+ # Errno::ECONNREFUSED is raised if the socket file exists but no process is listening on it.
145
+ rescue Errno::ECONNREFUSED, Errno::ENOENT
146
+ log_debug("#{server_name} is not running")
147
+ nil
148
+ end
149
+
150
+ def log_debug(message)
151
+ if defined?(Rails)
152
+ Rails.logger.debug { message }
153
+ else
154
+ puts "[DEBUG] #{message}" if ENV['DEBUG']
155
+ end
156
+ end
157
+
158
+ def log_info(message)
159
+ if defined?(Rails)
160
+ Rails.logger.info(message)
161
+ else
162
+ puts "[INFO] #{message}"
163
+ end
164
+ end
165
+
166
+ def log_warn(message)
167
+ if defined?(Rails)
168
+ Rails.logger.warn(message)
169
+ else
170
+ puts "[WARNING] #{message}"
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,3 @@
1
+ module Debounced
2
+ VERSION = "0.1.0"
3
+ end
data/lib/debounced.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "debounced/version"
2
+ require "debounced/railtie" if defined?(Rails)
3
+ require "debounced/event_service_proxy"
4
+
5
+ module Debounced
6
+ class Error < StandardError; end
7
+
8
+ class << self
9
+ attr_accessor :configuration
10
+
11
+ def configure
12
+ self.configuration ||= Configuration.new
13
+ yield(configuration) if block_given?
14
+ end
15
+ end
16
+
17
+ class Configuration
18
+ attr_accessor :socket_descriptor
19
+
20
+ def initialize
21
+ @socket_descriptor = ENV['DEBOUNCED_SOCKET'] || '/tmp/app.debounceEvents'
22
+ end
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: debounced
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gary Passero
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ - - "~>"
21
+ - !ruby/object:Gem::Version
22
+ version: '6.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.0
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rubocop
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.21'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.21'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rubocop-rails
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.12'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.12'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rubocop-rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.4'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.4'
89
+ description: A Ruby wrapper for a NodeJS server that uses the JavaScript micro event
90
+ loop to debounce events in Ruby applications
91
+ email:
92
+ - gary@flytedesk.com
93
+ executables:
94
+ - debounced-server
95
+ extensions: []
96
+ extra_rdoc_files: []
97
+ files:
98
+ - CHANGELOG.md
99
+ - README.md
100
+ - exe/debounced-server
101
+ - lib/assets/javascript/debounced/event_service.mjs
102
+ - lib/debounced.rb
103
+ - lib/debounced/event_service_proxy.rb
104
+ - lib/debounced/version.rb
105
+ homepage: https://github.com/flytedesk/debounced
106
+ licenses:
107
+ - MIT
108
+ metadata:
109
+ homepage_uri: https://github.com/flytedesk/debounced
110
+ source_code_uri: https://github.com/flytedesk/debounced
111
+ changelog_uri: https://github.com/flytedesk/debounced/blob/main/CHANGELOG.md
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 2.6.0
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.0.3.1
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Efficient event debouncing in Ruby
131
+ test_files: []