window_machines 0.1.4-x86_64-linux-musl

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: d9e33ca0fae5b8290f3beb14591dd39cc89ca5e00036c7759bc056f0a13742c0
4
+ data.tar.gz: 917cb912fd766034f36f67c0f2be31ae14117d48a5b901452e3b19394ea431eb
5
+ SHA512:
6
+ metadata.gz: f6b1fa15e46de0310445a33b2470e3e42e98e4df09f58b1a40164f0afb440be0426d4699cc9519321dd0fd38521edbfd16a876bf569c959840d95d185949687a
7
+ data.tar.gz: 18a9d6f676d9e965379cb05563bb4e0ce81f620f16fe3d89873a0715762f2bbd99f9a959e053936bfcbaac1ac8d64928a9963b43303f616c13bd5fdf255c147d
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.1.4](https://github.com/seuros/window_machines/compare/window_machines/v0.1.3...window_machines/v0.1.4) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([b8098d7](https://github.com/seuros/window_machines/commit/b8098d7d280dfd4e48ba7ade3a72b7f891853ec4))
9
+
10
+ ## [0.1.3](https://github.com/seuros/window_machines/compare/window_machines/v0.1.2...window_machines/v0.1.3) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([db7ff81](https://github.com/seuros/window_machines/commit/db7ff817e11a2448c17640631c2f8a6a254447a3))
16
+
17
+ ## [0.1.2](https://github.com/seuros/window_machines/compare/window_machines/v0.1.1...window_machines/v0.1.2) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * cross builds no longer compile for the host ([ca5ad94](https://github.com/seuros/window_machines/commit/ca5ad94ba196793f6366a9a5025903bd5bd22c51))
23
+
24
+ ## [0.1.1](https://github.com/seuros/window_machines/compare/window_machines-v0.1.0...window_machines/v0.1.1) (2026-11-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * initial extraction ([3bd753e](https://github.com/seuros/window_machines/commit/3bd753ec5ae5890e4e54272e5df4523c9d685d4b))
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Abdelkader Boudih
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # WindowMachines
2
+
3
+ > Bucket events by when they happened, not by when they finally showed up. 🔭
4
+
5
+ Event-time windows for Ruby. Give it a timestamp and it tells you which tumbling or sliding bucket the event belongs in. Give it a `Watermark` and it tells you when a bucket is safe to close. Give it a `Sessions` tracker and it groups bursts of activity, merging two bursts into one when a late arrival turns out to bridge the gap.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'window_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ # Every event lands in a ten-minute bucket, keyed by its own timestamp.
15
+ WindowMachines.tumbling(event.at, 600) # => [start, end] of its bucket
16
+
17
+ # Nothing gets reported until the watermark says the bucket is done.
18
+ watermark = WindowMachines::Watermark.new
19
+ watermark.advance(latest_timestamp_seen)
20
+ watermark.closes?(start, finish, 120) # => true once start..finish is two minutes behind
21
+
22
+ # Activity bursts, split by five minutes of silence, merged if something fills the gap.
23
+ sessions = WindowMachines::Sessions.new(300)
24
+ sessions.insert(event.at) # => [first, last] of the session it joined
25
+ sessions.advance_watermark(latest_timestamp_seen)
26
+ sessions.take_closed # => the oldest finished session, or nil
27
+ ```
28
+
29
+ Timestamps are Integers in whatever unit you pick. Seconds, milliseconds, ticks. The gem never looks at a clock.
30
+
31
+ ## A Message to the Resistance
32
+
33
+ So you built the signups-per-ten-minutes chart. You bucket each event by `Time.now` as it comes off the queue, you flush the bucket when the clock crosses the boundary, product loves it.
34
+
35
+ Then the mobile app ships an offline queue. A user signs up on a plane at 14:03, the phone syncs at 16:40, and your chart puts the signup in the 16:40 bucket. The 14:00 bucket was flushed two hours ago and will never be corrected. Nobody notices until finance reconciles the month and the numbers don't add up.
36
+
37
+ The event knew when it happened. Your pipeline threw that away and used arrival time instead, because arrival time is what the clock on the wall gives you for free.
38
+
39
+ This gem is the other way around. The event's own timestamp decides the bucket. A watermark you advance decides when a bucket has waited long enough for stragglers. Late arrivals land in the right bucket or get rejected loudly, never quietly misfiled.
40
+
41
+ ## Chapter 1: Stardate 2025.186, the AMS Akidau
42
+
43
+ > **AMS Akidau**, NCC-4219, *Akidau*-class survey vessel.
44
+ > The order telemetry arrives is not the order the stars emitted it.
45
+
46
+ Light from a near star reaches the survey deck in minutes. Light from a far one takes years. The deck doesn't log a supernova under the day the photons hit the sensor. It logs it under the day it happened, and it waits a stated grace period before declaring any day's catalogue final.
47
+
48
+ Somewhere in the chart room, a cadet files:
49
+
50
+ ```ruby
51
+ def bucket_for(event)
52
+ (Time.now.to_i / 600) * 600 # close enough
53
+ end
54
+ ```
55
+
56
+ "That," says the survey officer, "is how a supernova ends up catalogued two years late."
57
+
58
+ ## The Flight Plan
59
+
60
+ Each window moves through three states as the watermark catches up with it:
61
+
62
+ ```mermaid
63
+ stateDiagram-v2
64
+ [*] --> open
65
+ open --> open: event with timestamp inside the window
66
+ open --> closable: watermark reaches window end plus allowed lateness
67
+ closable --> reported: you emit it and take_closed hands it over
68
+ reported --> [*]
69
+ open --> rejected: insert at or below the watermark
70
+ rejected --> [*]
71
+ ```
72
+
73
+ And the signup that flew in from a plane:
74
+
75
+ ```mermaid
76
+ sequenceDiagram
77
+ participant Q as Queue
78
+ participant W as Watermark
79
+ participant B as Bucket 14:00-14:10
80
+
81
+ Q->>B: signup at 14:03 (arrived 16:40)
82
+ Note over B: tumbling(14:03, 600) picks this bucket
83
+ Q->>W: advance(16:40)
84
+ W-->>B: closes?(14:00, 14:10, 120) => true
85
+ Note over B: safe to report, 14:03 included
86
+ ```
87
+
88
+ With the grace period set to two minutes, a bucket closes once the watermark is two minutes past its end. Any event that still claims to belong in a closed bucket is refused instead of miscounted.
89
+
90
+ ## How Windows Work
91
+
92
+ ### Tumbling
93
+
94
+ Fixed-size, back-to-back buckets. A timestamp belongs to exactly one.
95
+
96
+ ```ruby
97
+ WindowMachines.tumbling(12, 10) # => [10, 20]
98
+ WindowMachines.tumbling(19, 10) # => [10, 20]
99
+ WindowMachines.tumbling(20, 10) # => [20, 30]
100
+ ```
101
+
102
+ Windows are half-open. The start is inside, the end is not, so 20 begins the next bucket.
103
+
104
+ ### Sliding
105
+
106
+ Overlapping buckets of one width, started every `slide`. A timestamp can sit in several at once. You get them newest first.
107
+
108
+ ```ruby
109
+ WindowMachines.sliding(12, 10, 5) # => [[10, 20], [5, 15]]
110
+ WindowMachines.sliding(7, 10, 5) # => [[5, 15], [0, 10]]
111
+ ```
112
+
113
+ A slide wider than the width leaves holes. A timestamp that falls into a hole gets an empty list, not a guess:
114
+
115
+ ```ruby
116
+ WindowMachines.sliding(12, 1, 5) # => []
117
+ ```
118
+
119
+ ### Watermarks
120
+
121
+ A watermark is your claim that nothing older than this will still turn up. You advance it. It never moves backwards.
122
+
123
+ ```ruby
124
+ watermark = WindowMachines::Watermark.new
125
+ watermark.value # => nil, nothing seen yet
126
+ watermark.advance(22) # => true
127
+ watermark.advance(9) # => false, ignored
128
+ watermark.value # => 22
129
+ ```
130
+
131
+ `closes?` answers whether a window has waited long enough. It takes the window plus how much lateness you're willing to absorb:
132
+
133
+ ```ruby
134
+ watermark.closes?(10, 20, 2) # => true, 20 + 2 is not past 22
135
+ watermark.closes?(10, 20, 3) # => false, give it one more tick
136
+ watermark.closes?(20, 30, 0) # => false, still open
137
+ ```
138
+
139
+ ### Sessions
140
+
141
+ Sessions have no fixed size. A session is a run of events where no two neighbours are more than `gap` apart. Insert returns the span of whichever session the event ended up in.
142
+
143
+ ```ruby
144
+ sessions = WindowMachines::Sessions.new(5)
145
+ sessions.insert(10) # => [10, 10], a new session
146
+ sessions.insert(15) # => [10, 15], five apart, still one session
147
+ sessions.insert(30) # => [30, 30], too far, a second session
148
+ sessions.insert(21) # => [21, 21], nine from 30, a third
149
+ ```
150
+
151
+ Then the late one shows up:
152
+
153
+ ```ruby
154
+ sessions.insert(26) # => [21, 30], bridges the gap and merges two sessions into one
155
+ ```
156
+
157
+ A session is finished once the watermark is a full `gap` past its last event. `take_closed` hands you the oldest finished session and forgets it. Call it until it returns `nil`.
158
+
159
+ ```ruby
160
+ sessions.advance_watermark(25)
161
+ sessions.take_closed # => [10, 15], 15 + 5 is behind 25
162
+ sessions.take_closed # => nil, [21, 30] is still open
163
+
164
+ sessions.advance_watermark(40)
165
+ sessions.take_closed # => [21, 30]
166
+ ```
167
+
168
+ Anything at or below the watermark is late by definition and raises, leaving the sessions untouched:
169
+
170
+ ```ruby
171
+ sessions.insert(40) # => raises ArgumentError
172
+ ```
173
+
174
+ ## The Rules of the Survey
175
+
176
+ - **Everything is an Integer** in `0..2**64-1`. Floats, strings, and anything else raise `TypeError`. Negative or oversized values raise `RangeError`.
177
+ - **Zero widths, slides, and gaps are rejected** with `ArgumentError`. A bucket of size zero holds nothing.
178
+ - **Sliding is capped at 32 windows per timestamp.** A width-to-slide ratio that would produce more raises `RangeError` rather than truncating the list.
179
+ - **Sessions hold at most 32 open sessions at once.** A 33rd raises `RangeError` and nothing is dropped to make room. Close some first.
180
+ - **Late inserts raise.** An event at or below the sessions watermark is refused, not silently folded into something.
181
+ - **Frozen means frozen.** `freeze` a watermark or a sessions tracker and every mutation raises `FrozenError`.
182
+
183
+ ## What You Think You're Doing vs Reality
184
+
185
+ ### You Think: "I'll just add a small delay before flushing. Late events are rare."
186
+ ### Reality: Late is a distribution, not an edge case.
187
+
188
+ Mobile sync, retried webhooks, a consumer that fell behind for an hour. A fixed delay picks one point on that curve and misfiles everything past it. A watermark you advance from the data itself moves with the curve, and `closes?` with explicit lateness makes the trade-off a number you chose, not one you guessed.
189
+
190
+ ## What's Yours
191
+
192
+ The gem decides boundaries. It never stores a payload. Keep your own counts or rows per `[start, end]` pair, add to them when an event is bucketed, and emit them when `closes?` says so or `take_closed` returns a span. Advancing the watermark is also your call. Drive it from the timestamps you've actually seen, with whatever lag your sources deserve.
193
+
194
+ ## License
195
+
196
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
197
+
198
+ **Remember: The signup happened at 14:03. The phone only admitted it at 16:40.**
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [0.2.2](https://github.com/seuros/window_machines/compare/window-machines-v0.2.1...window-machines-v0.2.2) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([b8098d7](https://github.com/seuros/window_machines/commit/b8098d7d280dfd4e48ba7ade3a72b7f891853ec4))
9
+
10
+ ## [0.2.1](https://github.com/seuros/window_machines/compare/window-machines-v0.2.0...window-machines-v0.2.1) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([db7ff81](https://github.com/seuros/window_machines/commit/db7ff817e11a2448c17640631c2f8a6a254447a3))
16
+
17
+ ## [0.2.0](https://github.com/seuros/window_machines/compare/window-machines-v0.1.0...window-machines-v0.2.0) (2026-11-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * initial extraction ([3bd753e](https://github.com/seuros/window_machines/commit/3bd753ec5ae5890e4e54272e5df4523c9d685d4b))
@@ -0,0 +1,22 @@
1
+ # window-machines
2
+
3
+ > **AMS Akidau**, NCC-4219, *Akidau*-class survey vessel.
4
+
5
+ Engine-room flight manual: divide observation time into bounded survey windows.
6
+
7
+ Allocation-free `no_std` window boundaries and one-key bounded session tracking.
8
+ All timestamps and widths use one caller-selected unsigned unit and origin.
9
+ Tumbling/sliding windows are aligned to zero and half-open `[start, end)`.
10
+ Sliding results are newest-start-first; too-small output capacity rejects rather
11
+ than silently truncating. Overflowing boundaries reject.
12
+
13
+ Watermarks never regress. A window closes when watermark reaches its end plus
14
+ allowed lateness; arithmetic overflow means it cannot yet close.
15
+
16
+ Sessions merge events whose inactivity gaps are **at most** the configured gap,
17
+ including out-of-order bridging events. Events at/below the current watermark
18
+ are late and rejected; session allowed lateness is zero in this version. Advancing
19
+ a watermark does not discard sessions: explicitly drain `take_closed()`.
20
+ Full tables reject events that cannot merge. Callers retain aggregation payloads
21
+ and apply the same merges; the core stores boundaries only. Multi-key indexing,
22
+ retractions, and distributed watermark coordination are not implemented.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WindowMachines
4
+ VERSION = '0.1.4'
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'window_machines/version'
4
+ require 'window_machines_native/window_machines_native'
@@ -0,0 +1,17 @@
1
+ module WindowMachines
2
+ VERSION: String
3
+ def self.tumbling: (Integer timestamp, Integer width) -> [Integer, Integer]
4
+ def self.sliding: (Integer timestamp, Integer width, Integer slide) -> Array[[Integer, Integer]]
5
+ class Watermark
6
+ def self.new: () -> instance
7
+ def advance: (Integer value) -> bool
8
+ def value: () -> Integer?
9
+ def closes?: (Integer start, Integer end, Integer lateness) -> bool
10
+ end
11
+ class Sessions
12
+ def self.new: (Integer gap) -> instance
13
+ def insert: (Integer timestamp) -> [Integer, Integer]
14
+ def advance_watermark: (Integer value) -> bool
15
+ def take_closed: () -> [Integer, Integer]?
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: window_machines
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: x86_64-linux-musl
6
+ authors:
7
+ - Abdelkader Boudih
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Initial allocation-free window policy core and thin native Ruby binding.
14
+ Caller owns payloads, clocks, and execution.
15
+ email:
16
+ - oss@seuros.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - ext/window_machines_native/core/CHANGELOG.md
25
+ - ext/window_machines_native/core/README.md
26
+ - ext/window_machines_native/extconf.rb
27
+ - lib/window_machines.rb
28
+ - lib/window_machines/version.rb
29
+ - lib/window_machines_native/window_machines_native.so
30
+ - sig/window_machines.rbs
31
+ homepage: https://github.com/seuros/window_machines
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ source_code_uri: https://github.com/seuros/window_machines
36
+ rubygems_mfa_required: 'true'
37
+ cargo_crate_name: window_machines_native
38
+ cargo_manifest_path: ext/window_machines_native/ffi/Cargo.toml
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 4.1.dev
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 3.3.22
56
+ requirements: []
57
+ rubygems_version: 3.5.23
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Native event-time window boundaries for Ruby
61
+ test_files: []