prosody 0.4.0 → 0.5.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 +4 -4
- data/.cargo/config.toml +3 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +395 -0
- data/ARCHITECTURE.md +2 -2
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +660 -326
- data/Cargo.toml +2 -1
- data/README.md +290 -191
- data/examples/keyed_state.rb +15 -3
- data/examples/keyed_state_windowing.rb +9 -1
- data/ext/prosody/Cargo.toml +2 -1
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +194 -89
- data/ext/prosody/src/client/mod.rs +167 -74
- data/ext/prosody/src/client/request.rs +132 -0
- data/ext/prosody/src/client/support.rs +122 -0
- data/ext/prosody/src/handler/context.rs +24 -20
- data/ext/prosody/src/handler/message.rs +50 -0
- data/ext/prosody/src/handler/mod.rs +112 -84
- data/ext/prosody/src/handler/state/mod.rs +488 -0
- data/ext/prosody/src/handler/state/registration.rs +104 -0
- data/ext/prosody/src/handler/state/scan.rs +218 -0
- data/ext/prosody/src/lib.rs +15 -3
- data/ext/prosody/src/published.rs +273 -0
- data/ext/prosody/src/scheduler/mod.rs +2 -2
- data/ext/prosody/src/scheduler/processor.rs +2 -2
- data/ext/prosody/src/scheduler/result.rs +7 -4
- data/ext/prosody/src/util.rs +86 -5
- data/lib/prosody/configuration.rb +49 -15
- data/lib/prosody/handler.rb +63 -10
- data/lib/prosody/native_stubs.rb +197 -31
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +164 -41
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +1 -0
- data/sig/configuration.rbs +51 -15
- data/sig/handler.rbs +12 -4
- data/sig/prosody.rbs +43 -2
- data/sig/request.rbs +66 -0
- data/sig/state.rbs +165 -47
- data/steep_expectations.yml +10 -0
- data/typecheck/payload_types.rb +14 -3
- data/typecheck/payload_types.rbs +4 -2
- data/typecheck_negative/payload_types.rb +4 -0
- data/typecheck_negative/payload_types.rbs +1 -0
- metadata +12 -2
- data/ext/prosody/src/handler/state.rs +0 -1035
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 052a7e84a72066480eec4ac9ced4e9ab0df381899bda1cd9a643f6e24338863d
|
|
4
|
+
data.tar.gz: 2f853ea6a2b3e7341513b88256fec70f670c289f58ebc3e5256348816735ee49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5198b84350257b6b0a2b92e1a0a8833fcc4f8ff966b115146096352610a81110323ec78af5b1868f441aface34b6c561063c02303bb1ecb0f35e338c996ce948
|
|
7
|
+
data.tar.gz: ceebfd36b6ebd53b1462a563b423e168b028fea0fa5d1b3e36d7478bf8ca0a655ac1f8268c8152a0b4d93cb4ca438ef622cd89c7a4ca895e38bc769d529df1e7
|
data/.cargo/config.toml
CHANGED
data/AGENTS.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Development patterns and practices for prosody-rb: Ruby bindings for the
|
|
4
|
+
Prosody Kafka client library. A magnus/rb-sys extension (`ext/prosody`) wraps
|
|
5
|
+
the published `prosody` Rust crate; the Ruby library (`lib/prosody`) carries
|
|
6
|
+
the public API, with RBS signatures in `sig/`. `ARCHITECTURE.md` explains the
|
|
7
|
+
Ruby/Rust bridge in depth — read it before touching the extension.
|
|
8
|
+
|
|
9
|
+
## Design Principles
|
|
10
|
+
|
|
11
|
+
These come before everything else. Every change is judged against them.
|
|
12
|
+
|
|
13
|
+
**Write code that is simple, clear, well-factored, elegant, easy to
|
|
14
|
+
understand, correct, and idiomatic.** A reader should grasp the intent without
|
|
15
|
+
effort. If a change makes the code harder to read, the change is wrong, even
|
|
16
|
+
if it is faster or shorter. If two designs are correct, pick the one that is
|
|
17
|
+
easier to delete.
|
|
18
|
+
|
|
19
|
+
**Make invalid states unrepresentable in the type system.** When a compiler
|
|
20
|
+
or type checker can prove a contract, no test, comment, or convention has to.
|
|
21
|
+
In Rust, prefer distinct types for distinct concepts, restricted constructors,
|
|
22
|
+
and `enum` sum types over flag fields. In the RBS signatures, give the public
|
|
23
|
+
surface precise types instead of loose ones. If a bug class can be made
|
|
24
|
+
uncompilable, do that instead of writing a runtime check.
|
|
25
|
+
|
|
26
|
+
**Delete more than you add.** Every change should leave the codebase smaller,
|
|
27
|
+
simpler, or both. If you must add code, look first for duplication you can
|
|
28
|
+
fold, abstractions that no longer pay rent, dead branches, and stale comments.
|
|
29
|
+
The end-state diff should net negative whenever the task allows. Line count is
|
|
30
|
+
not the only axis: plain duplicated arms often read better than generic
|
|
31
|
+
machinery.
|
|
32
|
+
|
|
33
|
+
**Identify, document, and enforce invariants.** For every load-bearing piece
|
|
34
|
+
of state: name the invariant, write it down near the type or function that
|
|
35
|
+
owns it, enforce it in the type system if you can, otherwise assert it at the
|
|
36
|
+
boundary, and cover it with a test. If you cannot name the invariant, you do
|
|
37
|
+
not yet understand the code well enough to change it.
|
|
38
|
+
|
|
39
|
+
**Leave the codebase better than you found it.** Drive-by simplifications are
|
|
40
|
+
encouraged when they are scoped to the area you are already touching. Do not
|
|
41
|
+
sprawl — but do not walk past obvious cleanup either.
|
|
42
|
+
|
|
43
|
+
## Definition of Done
|
|
44
|
+
|
|
45
|
+
No change is complete until every line below holds. These are acts, not
|
|
46
|
+
aspirations — perform each one; do not merely agree with it:
|
|
47
|
+
|
|
48
|
+
1. `cargo clippy --manifest-path ext/prosody/Cargo.toml` and the same with
|
|
49
|
+
`--tests` — zero warnings. `cargo doc` — zero warnings. `make format-check`
|
|
50
|
+
passes (rustfmt, taplo, standardrb).
|
|
51
|
+
2. `bundle exec rake rbs` and `bundle exec rake steep` — zero errors. They
|
|
52
|
+
validate the signatures and type-check the library, the typed examples,
|
|
53
|
+
and the negative fixtures.
|
|
54
|
+
3. `make test 2>&1 | tee /tmp/rspec-output.txt` — re-running slow suites is
|
|
55
|
+
expensive; grep the file, not the pipe.
|
|
56
|
+
4. Every new or converted test was proved falsifiable once: inject the
|
|
57
|
+
failure, watch it go red, revert.
|
|
58
|
+
5. Every deleted test names its surviving stronger test in the commit message.
|
|
59
|
+
6. Everything the change replaces is gone — code, tests, signatures, doc
|
|
60
|
+
vocabulary (see Redesign hygiene). "The new thing works" is half done.
|
|
61
|
+
7. Every claim written this session — doc cross-reference, "covered by" note,
|
|
62
|
+
exemplar path — was verified to resolve, not recalled from memory.
|
|
63
|
+
8. The diff is net-negative, or each addition is individually justified.
|
|
64
|
+
|
|
65
|
+
## Development Setup
|
|
66
|
+
|
|
67
|
+
Start required services with:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
docker-compose up -d
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Services:
|
|
74
|
+
|
|
75
|
+
- Kafka: localhost:9094
|
|
76
|
+
- Cassandra: localhost:9042
|
|
77
|
+
|
|
78
|
+
Common commands (`make help` lists them):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
make compile-dev # Compile the Rust extension (development mode)
|
|
82
|
+
make compile # Compile the Rust extension (release mode)
|
|
83
|
+
make test # Run the RSpec suite (needs Kafka and Cassandra)
|
|
84
|
+
make test-tracing # Run the tracing suite against a local OTel collector
|
|
85
|
+
make format # Format Rust, TOML, and Ruby code
|
|
86
|
+
make format-check # Check formatting without modifying files
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`rake` runs the full default gate: `compile spec standard rbs steep`.
|
|
90
|
+
Recompile with `make compile-dev` after any Rust change, or the specs run
|
|
91
|
+
against stale native code.
|
|
92
|
+
|
|
93
|
+
## Critical Rules
|
|
94
|
+
|
|
95
|
+
**Error Handling (Rust):**
|
|
96
|
+
|
|
97
|
+
- Never use `expect`, `unwrap`, `panic`, or `ok()` - forbidden by lints
|
|
98
|
+
- Propagate errors with `?` unless explicitly authorized to swallow
|
|
99
|
+
- Use `thiserror` for structured errors; box only when Clippy warns
|
|
100
|
+
|
|
101
|
+
**Memory (Rust):**
|
|
102
|
+
|
|
103
|
+
- **Never leak memory.** `std::mem::forget`, `Box::leak`, and `ManuallyDrop`
|
|
104
|
+
without an explicit reclamation path are forbidden. If a test must simulate
|
|
105
|
+
"Drop never ran", seed the underlying state directly; forgetting is never
|
|
106
|
+
the shortcut.
|
|
107
|
+
- **No unbounded keyed RAM.** Any in-memory structure keyed by message key or
|
|
108
|
+
collection must have a fixed capacity bound. Every in-memory map names its
|
|
109
|
+
removal path; self-draining maps are fine, but the drain is still named.
|
|
110
|
+
|
|
111
|
+
**Allocation and layout (tiger style — https://tigerstyle.dev/):**
|
|
112
|
+
|
|
113
|
+
- No hot-path allocation that is not upfront and bounded. A steady-state path
|
|
114
|
+
(per message, per timer fire, per handler call) must not allocate a buffer
|
|
115
|
+
whose size is discovered at runtime and grown as needed.
|
|
116
|
+
- Pick the buffer by what is known about the size: compile-time constant →
|
|
117
|
+
stack array; runtime-varying but almost always small → `SmallVec` sized to
|
|
118
|
+
the common case; genuinely unbounded → `Vec::with_capacity` sized once.
|
|
119
|
+
- `with_capacity` excuses the sizing, never the allocation. A per-call heap
|
|
120
|
+
allocation on a steady-state path is the defect itself.
|
|
121
|
+
- Never add a gratuitous allocation to satisfy the borrow checker. Reach for
|
|
122
|
+
a function item, an index, or a borrow before a scratch `Vec`.
|
|
123
|
+
- No amortized resize buffers on the hot path. If a reusable scratch buffer
|
|
124
|
+
is unavoidable, allocate it once at construction with a fixed bound.
|
|
125
|
+
- **Lay data out for the access pattern.** A hot path that scans one or two
|
|
126
|
+
fields across many entries must find those fields contiguously. Reach the
|
|
127
|
+
full record only for the entry the scan selects. An array of `Option<Arc<T>>`
|
|
128
|
+
turns a two-word decision into one heap dereference per entry, and thrashes
|
|
129
|
+
the CPU cache. Memory bandwidth is the bottleneck today, so the scan decides
|
|
130
|
+
the layout, not the record. Don't thrash the cache. False sharing counts:
|
|
131
|
+
keep atomics that different threads write off one line.
|
|
132
|
+
- Simplicity is not sacrificed for this. When zero-alloc and simple genuinely
|
|
133
|
+
conflict, keep it simple and leave a comment naming the allocation.
|
|
134
|
+
|
|
135
|
+
**Code Quality:**
|
|
136
|
+
|
|
137
|
+
- Lint, doc, type, and format gates live in Definition of Done — zero
|
|
138
|
+
warnings tolerated.
|
|
139
|
+
- Never suppress warnings with `#[allow(...)]` without permission. All clippy
|
|
140
|
+
and rustc warnings are fixed properly, not suppressed.
|
|
141
|
+
- Never introduce `dyn` without permission — prefer generics and associated
|
|
142
|
+
types. The type-erased surface this binding consumes already lives in the
|
|
143
|
+
published `prosody` crate.
|
|
144
|
+
|
|
145
|
+
**Ruby/Rust boundary:**
|
|
146
|
+
|
|
147
|
+
- Rust cannot call Ruby methods from Rust-created threads. Only Ruby-created
|
|
148
|
+
threads run Ruby code safely. `ARCHITECTURE.md` documents the bridge that
|
|
149
|
+
upholds this; never bypass it.
|
|
150
|
+
- Release the GVL for blocking work in the extension so other Ruby threads
|
|
151
|
+
can run.
|
|
152
|
+
|
|
153
|
+
**JSON codec:**
|
|
154
|
+
|
|
155
|
+
- This binding never defines its own payload codec. Payload encoding and
|
|
156
|
+
decoding belong to the `prosody` crate's codec; the binding passes payload
|
|
157
|
+
bytes through it.
|
|
158
|
+
- `serde_json`, `simd_json`, and the `json!` macro are banned in Rust
|
|
159
|
+
production code here for payload handling. Tests may use `serde_json::Value`
|
|
160
|
+
as a concrete payload type.
|
|
161
|
+
|
|
162
|
+
**Redesign hygiene:**
|
|
163
|
+
|
|
164
|
+
When a design is replaced, remove *all* of it in the same change —
|
|
165
|
+
half-deleted designs are where bloat and bug re-introduction live:
|
|
166
|
+
|
|
167
|
+
- Sweep the old design's vocabulary from every doc comment, signature, and
|
|
168
|
+
example. A stale doc can instruct a reader to re-introduce a fixed bug.
|
|
169
|
+
- Code whose only caller is its own test is dead — delete both together.
|
|
170
|
+
- Struct fields threaded through configs but only read at construction are
|
|
171
|
+
residue from a superseded design — remove them end-to-end.
|
|
172
|
+
- Do not build surface ahead of a caller: delete zero-caller paths, or make
|
|
173
|
+
them owner-confirmed, tested features.
|
|
174
|
+
|
|
175
|
+
**Debugging Discipline:**
|
|
176
|
+
|
|
177
|
+
- Never claim "found the issue" without rigorous proof
|
|
178
|
+
- Evidence first (logs, tests, reproducible behavior) → hypothesis → test → verify
|
|
179
|
+
|
|
180
|
+
**Documentation:**
|
|
181
|
+
|
|
182
|
+
- **All written text for this project must conform to ASD-STE100 (Simplified
|
|
183
|
+
Technical English). No written text is exempt.** This rule applies to
|
|
184
|
+
documentation, comments, READMEs, plans, issues, reviews, chat responses,
|
|
185
|
+
commit messages, PR text, and user-facing text. Apply these primary STE rules:
|
|
186
|
+
- Use the active voice. Write instructions in the imperative.
|
|
187
|
+
- Write short sentences. Use 20 words or fewer for instructions. Use 25
|
|
188
|
+
words or fewer for descriptions.
|
|
189
|
+
- Write one instruction per sentence. Keep one topic per paragraph. Use a
|
|
190
|
+
maximum of six sentences in each paragraph.
|
|
191
|
+
- Use a word with only one meaning. Use the same word for the same thing.
|
|
192
|
+
- Use simple verb tenses. Do not use an "-ing" form as a verb when a simple
|
|
193
|
+
tense is correct.
|
|
194
|
+
- Do not use a noun cluster of more than three nouns.
|
|
195
|
+
- Use approved technical names and technical verbs consistently.
|
|
196
|
+
- Write doc comments for a reader unfamiliar with the codebase. Lead with
|
|
197
|
+
what the thing is, how to use it, and what guarantee it gives — not the
|
|
198
|
+
internal mechanism.
|
|
199
|
+
- Short declarative sentences, one idea each. At most one parenthetical aside
|
|
200
|
+
per comment, never nested.
|
|
201
|
+
- Never argue with an imagined reviewer. State what the code does and the
|
|
202
|
+
invariant it upholds. Mention a rejected alternative only when a maintainer
|
|
203
|
+
would plausibly reintroduce it, as its own plain sentence.
|
|
204
|
+
- No invented compound jargon. Spell the idea out in ordinary words;
|
|
205
|
+
established terms keep their standard form.
|
|
206
|
+
- State an invariant at the type or function that owns it, once. Reference
|
|
207
|
+
the owning type elsewhere instead of restating.
|
|
208
|
+
- Be concise. Bad or needless docs hurt readability — prefer fewer, sharper
|
|
209
|
+
words.
|
|
210
|
+
- Never cite a plan's or spec's section number, phase number, or ordinal in
|
|
211
|
+
durable docs — code comments, CLAUDE.md, PR and commit text. Name the
|
|
212
|
+
concept instead.
|
|
213
|
+
- Avoid vague metaphor filler in prose, comments, and commit/PR text ("north
|
|
214
|
+
star", "surface area", "lean into", "double-click", "first-class citizen").
|
|
215
|
+
Say the concrete thing instead.
|
|
216
|
+
|
|
217
|
+
**Style:**
|
|
218
|
+
|
|
219
|
+
- Prefer `use` statements over fully qualified prefixes
|
|
220
|
+
- Methods without `self` should be functions (except `new` and similar)
|
|
221
|
+
- Ask before large structural changes
|
|
222
|
+
- Default to `pub(crate)`/`pub(super)` in Rust; make something `pub` only as
|
|
223
|
+
a deliberate API decision.
|
|
224
|
+
- Keep trait constraints as local as possible: put a constraint on the
|
|
225
|
+
function that needs it, not the struct.
|
|
226
|
+
- When a proposed simplification is examined and rejected, record the ruling
|
|
227
|
+
in one sentence at the site so the next pass does not re-litigate it.
|
|
228
|
+
- Ruby code follows standardrb; the formatter is the arbiter.
|
|
229
|
+
|
|
230
|
+
**Git:**
|
|
231
|
+
|
|
232
|
+
- Never add self-attribution to branch names, commits, PR titles, PR
|
|
233
|
+
descriptions, or code comments.
|
|
234
|
+
- Use conventional commits for commit titles and PR titles (e.g., `fix:`,
|
|
235
|
+
`feat:`, `docs:`, `refactor:`).
|
|
236
|
+
- PR titles and descriptions are written for a reader who is not intimately
|
|
237
|
+
familiar with the project. Lead with what changed and why.
|
|
238
|
+
- Never hard-wrap paragraphs in GitHub PR descriptions, PR comments, or issue
|
|
239
|
+
text. Each prose paragraph is one single line; blank lines separate
|
|
240
|
+
paragraphs.
|
|
241
|
+
- PR descriptions never include a test plan or a list of verification steps.
|
|
242
|
+
- Do not reference internal phase numbers, task IDs, or spec sections in
|
|
243
|
+
commits or code comments.
|
|
244
|
+
- Never run `git reset` or `git checkout` that would destroy uncommitted or
|
|
245
|
+
committed changes without explicit human permission. Prefer `git stash`, an
|
|
246
|
+
explicit commit, or `git restore --staged <path>`.
|
|
247
|
+
- Use `gh` for GitHub operations (PRs, issues, API).
|
|
248
|
+
|
|
249
|
+
## Error Classification
|
|
250
|
+
|
|
251
|
+
Distinguish permanent from transient errors for retry logic:
|
|
252
|
+
|
|
253
|
+
```rust
|
|
254
|
+
#[derive(Debug, Clone, Copy)]
|
|
255
|
+
pub enum ErrorType {
|
|
256
|
+
Permanent, // Business logic - don't retry
|
|
257
|
+
Transient, // Network/timeout - retry with backoff
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
A permanent error discards the in-flight message. An error the caller's code
|
|
262
|
+
causes (bad input, wrong argument shape) classifies as transient unless the
|
|
263
|
+
caller explicitly declares it permanent — a transient error retries and stays
|
|
264
|
+
visible, so no message is silently lost.
|
|
265
|
+
|
|
266
|
+
## Concurrency Invariants (inherited from prosody)
|
|
267
|
+
|
|
268
|
+
- **One handler per key, system-wide.** The framework guarantees at most one
|
|
269
|
+
message or timer handler for a given key executes anywhere in the cluster
|
|
270
|
+
at any moment. Never design for concurrent writers on the same key — that
|
|
271
|
+
scenario cannot occur.
|
|
272
|
+
- **At most one partition owner.** Kafka partition assignment guarantees one
|
|
273
|
+
consumer group member owns each partition at a time.
|
|
274
|
+
- These invariants are why distributed locks and optimistic concurrency are
|
|
275
|
+
never needed for per-key state. The framework provides the exclusivity;
|
|
276
|
+
binding code and examples can assume it.
|
|
277
|
+
|
|
278
|
+
## Code Organization
|
|
279
|
+
|
|
280
|
+
**Maximum file size: 500 lines.** A file that exceeds it is subdivided into
|
|
281
|
+
modules. Split along a seam the code already has, and give each module a doc
|
|
282
|
+
comment naming what it owns. Re-export from the parent so the split is
|
|
283
|
+
invisible to callers. A split that only balances line counts is worse than
|
|
284
|
+
the long file; find the real seam.
|
|
285
|
+
|
|
286
|
+
**Prefer one-word module names** (`bridge`, `client`, `handler`,
|
|
287
|
+
`scheduler`). A two-word name usually means the module owns more than one
|
|
288
|
+
concern, or the name restates its parent's path. A compound name is right
|
|
289
|
+
only when the compound is the domain term.
|
|
290
|
+
|
|
291
|
+
**Order within Rust files (topological by dependencies):**
|
|
292
|
+
Constants → Statics → Types → Implementations → Functions → Errors (bottom)
|
|
293
|
+
|
|
294
|
+
## Types, Signatures, and Examples
|
|
295
|
+
|
|
296
|
+
- `sig/` — public RBS signatures; `sig-private/` — internal signatures.
|
|
297
|
+
Every API change updates the signatures in the same commit.
|
|
298
|
+
- `lib/prosody/native_stubs.rb` — documented Ruby stubs for the classes and
|
|
299
|
+
methods the Rust extension implements. Editors and documentation tools read
|
|
300
|
+
them; the runtime does not (the native extension provides the real
|
|
301
|
+
definitions), and Steep ignores the file. When the extension's public
|
|
302
|
+
surface changes, update the matching stub and its YARD doc in the same
|
|
303
|
+
commit.
|
|
304
|
+
- Steep targets (`Steepfile`): `lib` checks the implementation;
|
|
305
|
+
`consumer_types` (`typecheck/`) verifies a payload type flows through the
|
|
306
|
+
public API; `typed_examples` checks every runnable example;
|
|
307
|
+
`negative_types` (`typecheck_negative/`) holds deliberately-invalid calls
|
|
308
|
+
with committed expectations — the gate fails if an expected diagnostic
|
|
309
|
+
disappears.
|
|
310
|
+
- `examples/*.rb` each carry a sibling `.rbs` signature and are type-checked
|
|
311
|
+
by the `typed_examples` target.
|
|
312
|
+
- Run `bundle exec rake rbs` to validate signatures and
|
|
313
|
+
`bundle exec rake steep` to type-check.
|
|
314
|
+
|
|
315
|
+
## Testing
|
|
316
|
+
|
|
317
|
+
Test suites live in `spec/` (RSpec). Integration specs need the Docker
|
|
318
|
+
Compose services. Run with `make test`; write output to a file:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
make test 2>&1 | tee /tmp/rspec-output.txt
|
|
322
|
+
grep -i fail /tmp/rspec-output.txt
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Test principles:**
|
|
326
|
+
|
|
327
|
+
- Drive tests by invariants, not by paths. Name the invariant (round-trip,
|
|
328
|
+
parity, idempotence) and prefer few broad tests over many narrow example
|
|
329
|
+
tests. Use realistic inputs, not happy-path toys.
|
|
330
|
+
- A test must be able to fail. When you write or convert a test, prove it can
|
|
331
|
+
go red once: inject the failure, watch it fail, revert.
|
|
332
|
+
- Never delete a test without naming, in the commit, the surviving test that
|
|
333
|
+
covers the same invariant at least as strongly.
|
|
334
|
+
- Never use `sleep` except for backpressure simulation. Wait on events,
|
|
335
|
+
channels, or notifications with a deadline — the deadline is a hang-guard,
|
|
336
|
+
never the assertion.
|
|
337
|
+
- Root-cause every intermittent failure. A passing re-run proves nothing.
|
|
338
|
+
Extract the reproducer and land it as a deterministic regression test.
|
|
339
|
+
- In Rust tests, use `assert` or a `Result` with `?` — never
|
|
340
|
+
`expect`/`unwrap`, never swallow errors.
|
|
341
|
+
|
|
342
|
+
## Common Patterns (Rust)
|
|
343
|
+
|
|
344
|
+
- Use `parking_lot` over `std::sync`
|
|
345
|
+
- For concurrent hash sets/maps, use `scc` (`scc::HashSet` / `scc::HashMap`),
|
|
346
|
+
never a `Mutex<HashSet>` / `Mutex<HashMap>`; pair it with
|
|
347
|
+
`ahash::RandomState`. In async code prefer its async interface.
|
|
348
|
+
- Use `tokio::sync` primitives (`Notify`, channels, `select!`) for async
|
|
349
|
+
- Independent I/O runs concurrently, never serially. Drive N independent
|
|
350
|
+
reads through a bounded `buffered(N)` (order-preserving) or
|
|
351
|
+
`buffer_unordered(N)` (unordered). Reserve serial `await` for genuinely
|
|
352
|
+
dependent reads, where each result determines the next.
|
|
353
|
+
- Drive futures over non-tokio primitives through the cooperative budget:
|
|
354
|
+
wrap each per-item future with `tokio::task::coop::cooperative` inside the
|
|
355
|
+
producing closure, so a drain of ready items cannot starve the worker.
|
|
356
|
+
- Mark builders with `#[must_use]`
|
|
357
|
+
- Use `LazyLock` for expensive static initialization
|
|
358
|
+
|
|
359
|
+
## Tracing / OpenTelemetry
|
|
360
|
+
|
|
361
|
+
- Instrument with `#[instrument]`, never a hand-built `info_span!` +
|
|
362
|
+
`.instrument(...)`. Use `skip_all` plus explicit `fields`, and `err` to
|
|
363
|
+
record failures on the span.
|
|
364
|
+
- Span level is audience: spans the user's own code causes export at info;
|
|
365
|
+
framework-internal spans use `level = "debug"`.
|
|
366
|
+
- Record unsigned integers as `i64` — the OTel layer stringifies
|
|
367
|
+
`u64`/`usize`. Record attribute values with `%` (Display) where the type
|
|
368
|
+
allows.
|
|
369
|
+
- Import tracing macros from `tracing` directly — never `use tracing::log::…`;
|
|
370
|
+
no bridge is installed, so events logged through it silently vanish.
|
|
371
|
+
- Never cache a `Span` — cache an `opentelemetry::Context` and recreate spans
|
|
372
|
+
on read. Cloning a span creates another reference to the same underlying
|
|
373
|
+
span; finishing one finishes all.
|
|
374
|
+
|
|
375
|
+
## Workflows
|
|
376
|
+
|
|
377
|
+
When launching multi-agent workflows:
|
|
378
|
+
|
|
379
|
+
- Select model and effort per task by complexity — do not let every agent
|
|
380
|
+
inherit the session model. Never downgrade a stage whose output gates a
|
|
381
|
+
commit or ship decision.
|
|
382
|
+
- Disable the advisor in every agent prompt.
|
|
383
|
+
- Keep structured-output schemata trivially simple: flat objects with a few
|
|
384
|
+
short bounded fields; put detail in report files.
|
|
385
|
+
|
|
386
|
+
## Research
|
|
387
|
+
|
|
388
|
+
- Automatically use context7 for code generation and library documentation.
|
|
389
|
+
|
|
390
|
+
## CI planning
|
|
391
|
+
|
|
392
|
+
- Check Cargo Rail after each CI path or repository layout change.
|
|
393
|
+
- Confirm that README-only changes select documentation jobs only.
|
|
394
|
+
- Confirm that source changes select all required build and test jobs.
|
|
395
|
+
- Add `rail.toml` only when the default rules classify a path incorrectly.
|
data/ARCHITECTURE.md
CHANGED
|
@@ -558,7 +558,7 @@ Understanding this architecture helps you write better Prosody applications:
|
|
|
558
558
|
AuthorizationError # Permission issues need manual fixing
|
|
559
559
|
```
|
|
560
560
|
|
|
561
|
-
3. **Ensure Clean Shutdown**: Always call `
|
|
561
|
+
3. **Ensure Clean Shutdown**: Always call `shutdown` before exiting
|
|
562
562
|
```ruby
|
|
563
563
|
# Set up a shutdown queue
|
|
564
564
|
shutdown = Queue.new
|
|
@@ -575,7 +575,7 @@ Understanding this architecture helps you write better Prosody applications:
|
|
|
575
575
|
|
|
576
576
|
# Clean shutdown
|
|
577
577
|
puts "Shutting down gracefully..."
|
|
578
|
-
client.
|
|
578
|
+
client.shutdown
|
|
579
579
|
```
|
|
580
580
|
|
|
581
581
|
4. **Monitor with Traces**: Use OpenTelemetry to understand message processing
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/prosody-events/prosody-rb/compare/prosody/v0.4.0...prosody/v0.5.0) (2026-08-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add subsystem requests ([#44](https://github.com/prosody-events/prosody-rb/issues/44)) ([672c1de](https://github.com/prosody-events/prosody-rb/commit/672c1de8cbdf883c7cb41e1c82b25851057cf733))
|
|
9
|
+
* add typed excise records and requests ([#45](https://github.com/prosody-events/prosody-rb/issues/45)) ([1581b37](https://github.com/prosody-events/prosody-rb/commit/1581b3768a8ce1ae72677135aabfae04496ce943))
|
|
10
|
+
* expose published keyed state ([#36](https://github.com/prosody-events/prosody-rb/issues/36)) ([bffefa2](https://github.com/prosody-events/prosody-rb/commit/bffefa2908b0d96cc43221babfed1d5ac6554ca2))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **logging:** flush OTel telemetry on exit ([#35](https://github.com/prosody-events/prosody-rb/issues/35)) ([531d22a](https://github.com/prosody-events/prosody-rb/commit/531d22af7b0a8af0e3335b7a706884af6b1e1833))
|
|
16
|
+
* **release:** exclude source-only specs from gem validation ([#33](https://github.com/prosody-events/prosody-rb/issues/33)) ([6d8d745](https://github.com/prosody-events/prosody-rb/commit/6d8d745c0ca9950643566cb11fb21090a6fdb738))
|
|
17
|
+
|
|
3
18
|
## [0.4.0](https://github.com/prosody-events/prosody-rb/compare/prosody/v0.3.0...prosody/v0.4.0) (2026-07-21)
|
|
4
19
|
|
|
5
20
|
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
data/CONFIGURATION.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Configure via constructor options or environment variables. Options fall back to environment variables when unset.
|
|
4
|
+
|
|
5
|
+
The Ruby client reports values it cannot convert to Prosody types. Prosody validates configuration semantics when the client is built.
|
|
6
|
+
|
|
7
|
+
## Core
|
|
8
|
+
|
|
9
|
+
| Option / Environment Variable | Description | Default |
|
|
10
|
+
|-----------------------------------------|---------------------------------------------------|--------------|
|
|
11
|
+
| `bootstrap_servers` / `PROSODY_BOOTSTRAP_SERVERS` | Kafka servers to connect to | - |
|
|
12
|
+
| `group_id` / `PROSODY_GROUP_ID` | Consumer group name | - |
|
|
13
|
+
| `subscribed_topics` / `PROSODY_SUBSCRIBED_TOPICS` | Topics to read from | - |
|
|
14
|
+
| `allowed_events` / `PROSODY_ALLOWED_EVENTS` | Only process events matching these prefixes | (all) |
|
|
15
|
+
| `source_system` / `PROSODY_SOURCE_SYSTEM` | Tag for outgoing messages (prevents reprocessing)| `<group_id>` |
|
|
16
|
+
| `mock` / `PROSODY_MOCK` | Use in-memory Kafka for testing | false |
|
|
17
|
+
| `mode` / - | Processing mode: `pipeline`, `low_latency`, or `best_effort` | `pipeline` |
|
|
18
|
+
| - / `PROSODY_LOG` | Rust log filter, such as `info` or `prosody=debug` | `info` |
|
|
19
|
+
|
|
20
|
+
## Requests
|
|
21
|
+
|
|
22
|
+
Requests work with the defaults on one network. Without a network name, peers always use the direct listener address.
|
|
23
|
+
With a network name, peers with the same name use the direct address. Other peers use the advertised connect URI.
|
|
24
|
+
Use a different bind address for each client that shares a host.
|
|
25
|
+
|
|
26
|
+
| Option / Environment Variable | Description | Default |
|
|
27
|
+
|--------------------------------|-------------|---------|
|
|
28
|
+
| `peer_bind_address` / `PROSODY_PEER_BIND_ADDRESS` | Socket address for the peer gRPC listener | Default network interface address on port 9099 |
|
|
29
|
+
| `peer_advertised_connect` / `PROSODY_PEER_ADVERTISED_CONNECT` | gRPC connect URI that peers on another network use | (none) |
|
|
30
|
+
| `peer_network_name` / `PROSODY_PEER_NETWORK_NAME` | Nonempty network name for direct peer routes | (none) |
|
|
31
|
+
| `peer_cache_capacity` / `PROSODY_PEER_CACHE_CAPACITY` | Maximum channels and peer records in each peer cache | 256 |
|
|
32
|
+
| `peer_registration_ttl` / `PROSODY_PEER_REGISTRATION_TTL` | Directory lease duration; use 5 seconds through 20 years | 30s |
|
|
33
|
+
|
|
34
|
+
Set `subsystem` to make this client answer requests. Without it, the client consumes messages but does not answer requests.
|
|
35
|
+
|
|
36
|
+
## Consumer
|
|
37
|
+
|
|
38
|
+
| Option / Environment Variable | Description | Default |
|
|
39
|
+
|-----------------------------------------|------------------------------------------------------|------------------------|
|
|
40
|
+
| `max_concurrency` / `PROSODY_MAX_CONCURRENCY` | Max messages being processed simultaneously | 32 |
|
|
41
|
+
| `max_uncommitted` / `PROSODY_MAX_UNCOMMITTED` | Max queued messages before pausing consumption | 64 |
|
|
42
|
+
| `timeout` / `PROSODY_TIMEOUT` | Cancel handler if it runs longer than this | 80% of stall threshold |
|
|
43
|
+
| `commit_interval` / `PROSODY_COMMIT_INTERVAL` | How often to save progress to Kafka | 1s |
|
|
44
|
+
| `poll_interval` / `PROSODY_POLL_INTERVAL` | How often to fetch new messages from Kafka | 100ms |
|
|
45
|
+
| `shutdown_timeout` / `PROSODY_SHUTDOWN_TIMEOUT` | Shutdown budget; handlers run freely until cancellation fires near the end of the timeout | 30s |
|
|
46
|
+
| `stall_threshold` / `PROSODY_STALL_THRESHOLD` | Report unhealthy if no progress for this long | 5m |
|
|
47
|
+
| `probe_port` / `PROSODY_PROBE_PORT` | HTTP port for health checks; use `false`, `:disabled`, or the environment value `none` to disable | 8000 |
|
|
48
|
+
| - / `PROSODY_STATISTICS_INTERVAL` | How often librdkafka reports client statistics; must be between 1ms and 24h | 5s |
|
|
49
|
+
| `failure_topic` / `PROSODY_FAILURE_TOPIC` | Send unprocessable messages here (dead letter queue) | - |
|
|
50
|
+
| `idempotence_cache_size` / `PROSODY_IDEMPOTENCE_CACHE_SIZE` | Global shared cache capacity across all partitions for message deduplication. Consumer deduplication is mandatory and cannot be disabled, so this must be at least 1; setting it to 0 in the client configuration is rejected | 8192 |
|
|
51
|
+
| `idempotence_version` / `PROSODY_IDEMPOTENCE_VERSION` | Version string for cache-busting dedup hashes | 1 |
|
|
52
|
+
| `idempotence_ttl` / `PROSODY_IDEMPOTENCE_TTL` | TTL for dedup records in Cassandra | 7d (604800 seconds) |
|
|
53
|
+
| `slab_size` / `PROSODY_SLAB_SIZE` | Timer storage granularity (rarely needs changing) | 1h |
|
|
54
|
+
| `message_spans` / `PROSODY_MESSAGE_SPANS` | Span linking for message execution: `child` (child-of) or `follows_from` | `child` |
|
|
55
|
+
| `timer_spans` / `PROSODY_TIMER_SPANS` | Span linking for timer execution: `child` (child-of) or `follows_from` | `follows_from` |
|
|
56
|
+
|
|
57
|
+
## Producer
|
|
58
|
+
|
|
59
|
+
| Option / Environment Variable | Description | Default |
|
|
60
|
+
|-----------------------------------------|---------------------------------|---------|
|
|
61
|
+
| `send_timeout` / `PROSODY_SEND_TIMEOUT` | Give up sending after this long | 1s |
|
|
62
|
+
|
|
63
|
+
## Retry
|
|
64
|
+
|
|
65
|
+
Retry backoff applies in pipeline and low-latency modes. `max_retries` controls how many retries low-latency mode performs before routing the failure to `failure_topic`. Pipeline mode uses deferral and does not use this limit.
|
|
66
|
+
|
|
67
|
+
| Option / Environment Variable | Description | Default |
|
|
68
|
+
|-----------------------------------------|-----------------------------------|---------|
|
|
69
|
+
| `max_retries` / `PROSODY_MAX_RETRIES` | Low-latency retries before routing to the failure topic | 3 |
|
|
70
|
+
| `retry_base` / `PROSODY_RETRY_BASE` | Wait this long before first retry | 20ms |
|
|
71
|
+
| `max_retry_delay` / `PROSODY_RETRY_MAX_DELAY` | Never wait longer than this | 5m |
|
|
72
|
+
|
|
73
|
+
## Deferral (Pipeline Mode)
|
|
74
|
+
|
|
75
|
+
| Option / Environment Variable | Description | Default |
|
|
76
|
+
|-----------------------------------------|---------------------------------------------------|---------|
|
|
77
|
+
| `defer_enabled` / `PROSODY_DEFER_ENABLED` | Enable deferral for new messages | true |
|
|
78
|
+
| `defer_base` / `PROSODY_DEFER_BASE` | Wait this long before first deferred retry | 1s |
|
|
79
|
+
| `defer_max_delay` / `PROSODY_DEFER_MAX_DELAY` | Never wait longer than this | 24h |
|
|
80
|
+
| `defer_failure_threshold` / `PROSODY_DEFER_FAILURE_THRESHOLD` | Disable deferral when failure rate exceeds this | 0.9 |
|
|
81
|
+
| `defer_failure_window` / `PROSODY_DEFER_FAILURE_WINDOW` | Measure failure rate over this time window | 5m |
|
|
82
|
+
| `defer_store_cache_size` / `PROSODY_DEFER_STORE_CACHE_SIZE` | Maximum deferred store cache entries per Cassandra defer store | 8192 |
|
|
83
|
+
|
|
84
|
+
## Kafka Message Loader (All Modes)
|
|
85
|
+
|
|
86
|
+
The shared loader resolves Kafka messages for deferral and keyed state:
|
|
87
|
+
|
|
88
|
+
| Option / Environment Variable | Description | Default |
|
|
89
|
+
|--------------------------------|-------------|---------|
|
|
90
|
+
| `loader_cache_size` / `PROSODY_LOADER_CACHE_SIZE` | Maximum messages retained by the shared Kafka loader | 1024 |
|
|
91
|
+
| `loader_seek_timeout` / `PROSODY_LOADER_SEEK_TIMEOUT` | Timeout for Kafka loader seek operations | 30s |
|
|
92
|
+
| `loader_discard_threshold` / `PROSODY_LOADER_DISCARD_THRESHOLD` | Sequential-read distance before the loader seeks | 100 |
|
|
93
|
+
|
|
94
|
+
## Monopolization Detection (Pipeline Mode)
|
|
95
|
+
|
|
96
|
+
| Option / Environment Variable | Description | Default |
|
|
97
|
+
|-----------------------------------------|-----------------------------------------|---------|
|
|
98
|
+
| `monopolization_enabled` / `PROSODY_MONOPOLIZATION_ENABLED` | Enable hot key protection | true |
|
|
99
|
+
| `monopolization_threshold` / `PROSODY_MONOPOLIZATION_THRESHOLD` | Max handler time as fraction of window | 0.9 |
|
|
100
|
+
| `monopolization_window` / `PROSODY_MONOPOLIZATION_WINDOW` | Measurement window | 5m |
|
|
101
|
+
| `monopolization_cache_size` / `PROSODY_MONOPOLIZATION_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
102
|
+
|
|
103
|
+
## Fair Scheduling (All Modes)
|
|
104
|
+
|
|
105
|
+
| Option / Environment Variable | Description | Default |
|
|
106
|
+
|-----------------------------------------|------------------------------------------------------------------|---------|
|
|
107
|
+
| `scheduler_failure_weight` / `PROSODY_SCHEDULER_FAILURE_WEIGHT` | Fraction of processing time reserved for retries | 0.3 |
|
|
108
|
+
| `scheduler_max_wait` / `PROSODY_SCHEDULER_MAX_WAIT` | Messages waiting this long get maximum priority | 2m |
|
|
109
|
+
| `scheduler_wait_weight` / `PROSODY_SCHEDULER_WAIT_WEIGHT` | Priority boost for waiting messages (higher = more aggressive) | 200.0 |
|
|
110
|
+
| `scheduler_cache_size` / `PROSODY_SCHEDULER_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
111
|
+
|
|
112
|
+
## Telemetry
|
|
113
|
+
|
|
114
|
+
Prosody emits message, timer, and producer lifecycle events to a Kafka topic for observability:
|
|
115
|
+
|
|
116
|
+
| Option / Environment Variable | Description | Default |
|
|
117
|
+
|-----------------------------------------|------------------------------------------------|----------------------------|
|
|
118
|
+
| `telemetry_topic` / `PROSODY_TELEMETRY_TOPIC` | Kafka topic to produce telemetry events to | `prosody.telemetry-events` |
|
|
119
|
+
| `telemetry_enabled` / `PROSODY_TELEMETRY_ENABLED` | Enable or disable the telemetry emitter | true |
|
|
120
|
+
|
|
121
|
+
Mock mode disables telemetry automatically, regardless of `telemetry_enabled`.
|
|
122
|
+
|
|
123
|
+
## Cassandra
|
|
124
|
+
|
|
125
|
+
Persistent storage for timers, deferral, deduplication, and keyed state. It is not needed when `mock: true`.
|
|
126
|
+
|
|
127
|
+
| Option / Environment Variable | Description | Default |
|
|
128
|
+
|-----------------------------------------|------------------------------------|---------|
|
|
129
|
+
| `cassandra_nodes` / `PROSODY_CASSANDRA_NODES` | Servers to connect to (host:port) | - |
|
|
130
|
+
| `cassandra_keyspace` / `PROSODY_CASSANDRA_KEYSPACE` | Keyspace name | prosody |
|
|
131
|
+
| `cassandra_user` / `PROSODY_CASSANDRA_USER` | Username | - |
|
|
132
|
+
| `cassandra_password` / `PROSODY_CASSANDRA_PASSWORD` | Password | - |
|
|
133
|
+
| `cassandra_datacenter` / `PROSODY_CASSANDRA_DATACENTER` | Prefer this datacenter for queries | - |
|
|
134
|
+
| `cassandra_rack` / `PROSODY_CASSANDRA_RACK` | Prefer this rack for queries | - |
|
|
135
|
+
| `cassandra_retention` / `PROSODY_CASSANDRA_RETENTION` | Delete data older than this | 1y |
|
|
136
|
+
|
|
137
|
+
## Keyed State
|
|
138
|
+
|
|
139
|
+
Register keyed-state collections before you subscribe. Persistence is backed by Cassandra and is not needed when `mock: true`. See [Keyed State](README.md#keyed-state) for handler usage. Where an option and an environment variable are paired, an explicitly set option wins. Otherwise, the environment variable applies, then the default.
|
|
140
|
+
|
|
141
|
+
| Option / Environment Variable | Description | Default |
|
|
142
|
+
|-------------------------------|-------------|---------|
|
|
143
|
+
| `state_collections` / - | Keyed-state collections to register before subscribe (array of definitions or config hashes; duplicate names rejected) | (none) |
|
|
144
|
+
| `subsystem` / `PROSODY_SUBSYSTEM` | Subsystem name used to advertise JSON collections whose definitions set `published: true` | (none) |
|
|
145
|
+
| `state_cache_dir` / `PROSODY_STATE_CACHE_DIR` | Disk workspace for the local keyed-state cache; each live client needs its own directory. Set a mounted path in production | per-client temp dir |
|
|
146
|
+
| `state_owned_cache_size` / `PROSODY_STATE_OWNED_CACHE_SIZE` | Capacity of the owning keyed-state cache; accepts sizes such as `64 MiB` or `500 MB` | storage-engine default |
|
|
147
|
+
| `state_read_cache_size` / `PROSODY_STATE_READ_CACHE_SIZE` | Capacity of the published-state read cache; accepts sizes such as `1 MiB` | `state_owned_cache_size` or `PROSODY_STATE_OWNED_CACHE_SIZE` when set; otherwise 1 MiB |
|
|
148
|
+
| `state_read_cache` / `PROSODY_STATE_READ_CACHE_TTL` | Default published-read cache TTL. Use `false` or the environment value `none` to bypass the cache | 5s |
|
|
149
|
+
| `state_recovery_delay` / `PROSODY_STATE_RECOVERY_DELAY` | Whole-second delay between staging a provisional cell and the recovery sweep; every collection TTL must strictly exceed it | 30s |
|
|
150
|
+
|
|
151
|
+
Prefer the definition constructors from the [API reference](README.md#api-reference). They serialize into `state_collections`, so you can reuse the same object with `context.state`. Each entry has these fields:
|
|
152
|
+
|
|
153
|
+
Published collections require `subsystem`. Keep it configured for one deployment after removing `published: true` so readers can observe the collection's retirement.
|
|
154
|
+
|
|
155
|
+
| Field | Description | Default |
|
|
156
|
+
|-------|-------------|---------|
|
|
157
|
+
| `name` | Collection name; non-empty and unique within the client | (required) |
|
|
158
|
+
| `kind` | `"value"`, `"map"`, or `"deque"` | (required) |
|
|
159
|
+
| `payload` | `"json"` (JSON values) or `"message"` (the full Kafka message the handler received) | (required) |
|
|
160
|
+
| `ttl_seconds` | Per-write TTL in whole seconds (at least 1; must exceed the recovery delay) | (none) |
|
|
161
|
+
| `read_uncommitted` | Opt out of transactional staging | false |
|
|
162
|
+
| `published` | Allow read-only access from other consumer groups; JSON collections only | false |
|
|
163
|
+
| `read_cache` | Published-read cache override: a positive duration, `false`, or inherit when omitted | inherit |
|
|
164
|
+
| `keyset_limit` | Map-only; ordered-scan bound in `0..=4096` (`0` disables ordered-scan tracking) | 128 |
|
|
165
|
+
| `capacity` | Deque-only window bound (at least 1); keeps at most N slots, enforced lazily on push. Runtime-only and mutable across deploys — not persisted | unbounded |
|
|
166
|
+
|
|
167
|
+
Constructors set these via keyword arguments (`ttl:`, `keyset_limit:`, `capacity:`, `read_uncommitted:`, `published:`, `read_cache:`). `read_cache` is a positive duration in seconds, `false` to bypass the cache, or `nil` to inherit the client default.
|